@algosuite/vo-mcp 0.2.0-beta.22 → 0.2.0-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-auth-probe-cli.mjs +1386 -0
- package/dist/autostart-cli.js +29 -3
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +47142 -42080
- package/dist/cli.js +3 -10
- package/dist/cli.js.map +2 -2
- package/dist/index.js +3 -10
- package/dist/index.js.map +2 -2
- package/dist/install-cli.js +28 -2
- package/dist/install-cli.js.map +2 -2
- package/dist/runner-cli.js +3535 -1046
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +218 -64
- package/dist/runner-supervisor.js.map +4 -4
- package/dist/thresholds.json +62 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
// ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
// ../../scripts/virtual-office/code-runner/windows-claude-launch.mjs
|
|
11
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
12
|
+
import { win32 as path } from "node:path";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
var NATIVE_CLAUDE_PARTS = [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"@anthropic-ai",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"bin",
|
|
19
|
+
"claude.exe"
|
|
20
|
+
];
|
|
21
|
+
function pathValue(env) {
|
|
22
|
+
for (const key of ["Path", "PATH", "path"]) {
|
|
23
|
+
if (typeof env?.[key] === "string") return env[key];
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
function cleanPathSegment(value) {
|
|
28
|
+
const trimmed = String(value || "").trim();
|
|
29
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
30
|
+
}
|
|
31
|
+
function envValue(env, name) {
|
|
32
|
+
const exact = env?.[name];
|
|
33
|
+
if (typeof exact === "string") return exact.trim();
|
|
34
|
+
const key = Object.keys(env || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
35
|
+
return typeof env?.[key] === "string" ? env[key].trim() : "";
|
|
36
|
+
}
|
|
37
|
+
function userClaudeCandidates(bin, env) {
|
|
38
|
+
if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
|
|
39
|
+
const userProfile = envValue(env, "USERPROFILE");
|
|
40
|
+
const appData = envValue(env, "APPDATA") || (userProfile ? path.join(userProfile, "AppData", "Roaming") : "");
|
|
41
|
+
const localAppData = envValue(env, "LOCALAPPDATA") || (userProfile ? path.join(userProfile, "AppData", "Local") : "");
|
|
42
|
+
const candidates = [];
|
|
43
|
+
if (appData) {
|
|
44
|
+
const npmBin = path.join(appData, "npm");
|
|
45
|
+
candidates.push(
|
|
46
|
+
path.join(npmBin, "claude.exe"),
|
|
47
|
+
path.join(npmBin, "claude.cmd"),
|
|
48
|
+
path.join(npmBin, "claude.ps1"),
|
|
49
|
+
path.join(npmBin, "claude"),
|
|
50
|
+
path.join(npmBin, ...NATIVE_CLAUDE_PARTS)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (userProfile) candidates.push(path.join(userProfile, ".local", "bin", "claude.exe"));
|
|
54
|
+
if (localAppData) {
|
|
55
|
+
candidates.push(
|
|
56
|
+
path.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
57
|
+
path.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return candidates;
|
|
61
|
+
}
|
|
62
|
+
function pathCandidates(bin, env) {
|
|
63
|
+
if (path.isAbsolute(bin) || /[\\/]/u.test(bin)) {
|
|
64
|
+
return [path.resolve(bin)];
|
|
65
|
+
}
|
|
66
|
+
const extension = path.extname(bin);
|
|
67
|
+
const fromPath = pathValue(env).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path.join(directory, bin)] : [
|
|
68
|
+
path.join(directory, `${bin}.exe`),
|
|
69
|
+
path.join(directory, `${bin}.cmd`),
|
|
70
|
+
path.join(directory, `${bin}.ps1`),
|
|
71
|
+
path.join(directory, bin)
|
|
72
|
+
]);
|
|
73
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74
|
+
return [...fromPath, ...userClaudeCandidates(bin, env)].filter((candidate) => {
|
|
75
|
+
const key = candidate.toLowerCase();
|
|
76
|
+
if (seen.has(key)) return false;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function canonicalExistingPath(candidate, exists, canonicalize) {
|
|
82
|
+
if (!exists(candidate)) return null;
|
|
83
|
+
try {
|
|
84
|
+
return canonicalize(candidate);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function resolveWindowsClaudeExecutable({
|
|
90
|
+
bin = "claude",
|
|
91
|
+
env = process.env,
|
|
92
|
+
exists = existsSync,
|
|
93
|
+
canonicalize = realpathSync
|
|
94
|
+
} = {}) {
|
|
95
|
+
const requested = String(bin || "").trim();
|
|
96
|
+
if (!requested || requested.includes("\0")) {
|
|
97
|
+
throw new TypeError("Claude executable must be a non-empty path without NUL bytes");
|
|
98
|
+
}
|
|
99
|
+
for (const candidate of pathCandidates(requested, env)) {
|
|
100
|
+
const found = canonicalExistingPath(candidate, exists, canonicalize);
|
|
101
|
+
if (!found) continue;
|
|
102
|
+
if (path.extname(found).toLowerCase() === ".exe") return found;
|
|
103
|
+
const native = path.join(path.dirname(found), ...NATIVE_CLAUDE_PARTS);
|
|
104
|
+
const resolvedNative = canonicalExistingPath(native, exists, canonicalize);
|
|
105
|
+
if (resolvedNative) return resolvedNative;
|
|
106
|
+
}
|
|
107
|
+
const error = new Error(
|
|
108
|
+
`Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
|
|
109
|
+
);
|
|
110
|
+
error.code = "ENOENT";
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
function buildWindowsClaudeLaunch({
|
|
114
|
+
bin = "claude",
|
|
115
|
+
args = [],
|
|
116
|
+
env = process.env
|
|
117
|
+
} = {}) {
|
|
118
|
+
return {
|
|
119
|
+
bin: resolveWindowsClaudeExecutable({ bin, env }),
|
|
120
|
+
args: Array.from(args, (value) => String(value)),
|
|
121
|
+
spawnOptions: {
|
|
122
|
+
shell: false,
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
windowsVerbatimArguments: false
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function spawnClaudeSync(args = [], options = {}) {
|
|
129
|
+
if (process.platform !== "win32") {
|
|
130
|
+
return spawnSync("claude", args, { windowsHide: true, ...options });
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const launch = buildWindowsClaudeLaunch({
|
|
134
|
+
bin: "claude",
|
|
135
|
+
args,
|
|
136
|
+
env: options.env || process.env
|
|
137
|
+
});
|
|
138
|
+
return spawnSync(launch.bin, launch.args, {
|
|
139
|
+
...options,
|
|
140
|
+
...launch.spawnOptions
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
error,
|
|
145
|
+
status: null,
|
|
146
|
+
signal: null,
|
|
147
|
+
output: null,
|
|
148
|
+
stdout: null,
|
|
149
|
+
stderr: null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
|
|
155
|
+
var require2 = createRequire(import.meta.url);
|
|
156
|
+
var KEY_SERVICE = "algosuite-vo";
|
|
157
|
+
var KEY_ACCOUNT = "anthropic-api-key";
|
|
158
|
+
var _entryCtor;
|
|
159
|
+
var _loadTried = false;
|
|
160
|
+
function defaultEntryCtor() {
|
|
161
|
+
if (_loadTried) return _entryCtor;
|
|
162
|
+
_loadTried = true;
|
|
163
|
+
try {
|
|
164
|
+
_entryCtor = require2("@napi-rs/keyring").Entry;
|
|
165
|
+
} catch {
|
|
166
|
+
_entryCtor = null;
|
|
167
|
+
}
|
|
168
|
+
return _entryCtor;
|
|
169
|
+
}
|
|
170
|
+
function getAnthropicKey({ EntryCtor = defaultEntryCtor() } = {}) {
|
|
171
|
+
if (!EntryCtor) return null;
|
|
172
|
+
try {
|
|
173
|
+
return new EntryCtor(KEY_SERVICE, KEY_ACCOUNT).getPassword() || null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
var PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
|
|
179
|
+
var CLAUDE_PREFER_LOGIN_ENV = "VO_RUNNER_CLAUDE_PREFER_LOGIN";
|
|
180
|
+
function isTruthyFlag(v) {
|
|
181
|
+
const s = String(v ?? "").trim().toLowerCase();
|
|
182
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
183
|
+
}
|
|
184
|
+
function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
|
|
185
|
+
if (isTruthyFlag(baseEnv[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
|
|
186
|
+
const next = { ...baseEnv };
|
|
187
|
+
delete next.ANTHROPIC_API_KEY;
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
if (baseEnv.ANTHROPIC_API_KEY) return { ...baseEnv };
|
|
191
|
+
const key = getKey();
|
|
192
|
+
return key ? { ...baseEnv, ANTHROPIC_API_KEY: key } : { ...baseEnv };
|
|
193
|
+
}
|
|
194
|
+
function describeAnthropicAuthSource(baseEnv = {}, { getKey = getAnthropicKey } = {}) {
|
|
195
|
+
if (isTruthyFlag(baseEnv[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(baseEnv[PREFER_LOGIN_ENV])) {
|
|
196
|
+
return "claude auth login (VO_RUNNER_PREFER_LOGIN set \u2014 any API key ignored)";
|
|
197
|
+
}
|
|
198
|
+
if (baseEnv.ANTHROPIC_API_KEY) return "ANTHROPIC_API_KEY from environment";
|
|
199
|
+
if (getKey()) return "ANTHROPIC_API_KEY from OS keychain";
|
|
200
|
+
return "claude auth login session (no API key set)";
|
|
201
|
+
}
|
|
202
|
+
function probeClaudeLoginState({
|
|
203
|
+
spawn: spawn2 = spawnSync2,
|
|
204
|
+
buildWindowsLaunch = buildWindowsClaudeLaunch,
|
|
205
|
+
platform = process.platform
|
|
206
|
+
} = {}) {
|
|
207
|
+
try {
|
|
208
|
+
const launch = platform === "win32" ? buildWindowsLaunch({ bin: "claude", args: ["auth", "status"] }) : { bin: "claude", args: ["auth", "status"], spawnOptions: { windowsHide: true } };
|
|
209
|
+
const st = spawn2(launch.bin, launch.args, { ...launch.spawnOptions, timeout: 5e3, encoding: "utf8" });
|
|
210
|
+
const parsed = JSON.parse(String(st.stdout || "").trim() || "{}");
|
|
211
|
+
return typeof parsed.loggedIn === "boolean" ? parsed.loggedIn : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ../../scripts/virtual-office/code-runner/sandbox/sandbox-docker.mjs
|
|
218
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
219
|
+
|
|
220
|
+
// ../../scripts/virtual-office/code-runner/context7-mcp.mjs
|
|
221
|
+
var CONTEXT7_URL = "https://mcp.context7.com/mcp";
|
|
222
|
+
function context7McpConfig(env = process.env) {
|
|
223
|
+
if (env.VO_ENABLE_CONTEXT7 !== "1") return null;
|
|
224
|
+
const url = env.VO_CONTEXT7_URL && env.VO_CONTEXT7_URL.trim() || CONTEXT7_URL;
|
|
225
|
+
const server = { type: "http", url };
|
|
226
|
+
if (env.CONTEXT7_API_KEY && env.CONTEXT7_API_KEY.trim()) {
|
|
227
|
+
server.headers = { CONTEXT7_API_KEY: env.CONTEXT7_API_KEY.trim() };
|
|
228
|
+
}
|
|
229
|
+
return { mcpServers: { context7: server } };
|
|
230
|
+
}
|
|
231
|
+
function context7McpArgs(env = process.env) {
|
|
232
|
+
const cfg = context7McpConfig(env);
|
|
233
|
+
return cfg ? ["--mcp-config", JSON.stringify(cfg)] : [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ../../scripts/virtual-office/code-runner/claude-args.mjs
|
|
237
|
+
var DEFAULT_PERMISSION_MODE = "acceptEdits";
|
|
238
|
+
var VO_SESSION_STATE_TOOL = "mcp__vo-mcp__vo_report_session_state";
|
|
239
|
+
var VO_HEADLESS_PNPM_TOOL = "Bash(pnpm *)";
|
|
240
|
+
var VO_HEADLESS_PNPM_FROM_DIR_TOOL = "Bash(pnpm --dir *)";
|
|
241
|
+
var SAFE_PERMISSION_MODES = /* @__PURE__ */ new Set(["acceptEdits", "plan", "default", "dontAsk", "delegate"]);
|
|
242
|
+
function normalizeClaudePermissionMode(value) {
|
|
243
|
+
const normalized = String(value ?? "").trim() || DEFAULT_PERMISSION_MODE;
|
|
244
|
+
if (!SAFE_PERMISSION_MODES.has(normalized)) {
|
|
245
|
+
throw new Error(`unsafe Claude permission mode "${normalized}"`);
|
|
246
|
+
}
|
|
247
|
+
return normalized;
|
|
248
|
+
}
|
|
249
|
+
function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, env = process.env } = {}) {
|
|
250
|
+
const effectivePermissionMode = normalizeClaudePermissionMode(permissionMode);
|
|
251
|
+
const allowedTools = effectivePermissionMode === DEFAULT_PERMISSION_MODE ? `${VO_SESSION_STATE_TOOL},${VO_HEADLESS_PNPM_TOOL},${VO_HEADLESS_PNPM_FROM_DIR_TOOL}` : VO_SESSION_STATE_TOOL;
|
|
252
|
+
const args = [
|
|
253
|
+
"-p",
|
|
254
|
+
"--output-format",
|
|
255
|
+
"stream-json",
|
|
256
|
+
"--verbose",
|
|
257
|
+
"--permission-mode",
|
|
258
|
+
effectivePermissionMode,
|
|
259
|
+
"--allowedTools",
|
|
260
|
+
allowedTools
|
|
261
|
+
];
|
|
262
|
+
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
263
|
+
args.push("--max-turns", String(maxTurns));
|
|
264
|
+
}
|
|
265
|
+
if (model) {
|
|
266
|
+
args.push("--model", String(model));
|
|
267
|
+
}
|
|
268
|
+
if (effort) {
|
|
269
|
+
args.push("--effort", String(effort));
|
|
270
|
+
}
|
|
271
|
+
if (typeof maxBudgetUsd === "number" && maxBudgetUsd > 0) {
|
|
272
|
+
args.push("--max-budget-usd", String(maxBudgetUsd));
|
|
273
|
+
}
|
|
274
|
+
args.push(...context7McpArgs(env));
|
|
275
|
+
return args;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ../../scripts/virtual-office/code-runner/terminal-process-cleanup.mjs
|
|
279
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
280
|
+
|
|
281
|
+
// ../../scripts/virtual-office/code-runner/orphan-agent-reaper.mjs
|
|
282
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
283
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
284
|
+
import os from "node:os";
|
|
285
|
+
import path2 from "node:path";
|
|
286
|
+
|
|
287
|
+
// ../../scripts/virtual-office/code-runner/agent-token-usage.mjs
|
|
288
|
+
var MAX_TOKEN_COUNT = 1e9;
|
|
289
|
+
var MAX_COST_USD = 1e4;
|
|
290
|
+
function count(value) {
|
|
291
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
292
|
+
return Math.min(MAX_TOKEN_COUNT, Math.round(value));
|
|
293
|
+
}
|
|
294
|
+
function money(value) {
|
|
295
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
296
|
+
return Math.min(MAX_COST_USD, value);
|
|
297
|
+
}
|
|
298
|
+
function extractTokenUsage(evt) {
|
|
299
|
+
const models = extractModelUsage(evt);
|
|
300
|
+
if (models) {
|
|
301
|
+
const out2 = models.reduce((sum, row) => ({
|
|
302
|
+
input_tokens: Math.min(MAX_TOKEN_COUNT, sum.input_tokens + row.input_tokens),
|
|
303
|
+
output_tokens: Math.min(MAX_TOKEN_COUNT, sum.output_tokens + row.output_tokens),
|
|
304
|
+
cache_creation_tokens: Math.min(MAX_TOKEN_COUNT, sum.cache_creation_tokens + row.cache_creation_tokens),
|
|
305
|
+
cache_read_tokens: Math.min(MAX_TOKEN_COUNT, sum.cache_read_tokens + row.cache_read_tokens)
|
|
306
|
+
}), { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0 });
|
|
307
|
+
return Object.values(out2).some((value) => value > 0) ? out2 : null;
|
|
308
|
+
}
|
|
309
|
+
const u = evt?.usage;
|
|
310
|
+
if (!u || typeof u !== "object") return null;
|
|
311
|
+
const out = {
|
|
312
|
+
input_tokens: count(u.input_tokens) ?? 0,
|
|
313
|
+
output_tokens: count(u.output_tokens) ?? 0,
|
|
314
|
+
cache_creation_tokens: count(u.cache_creation_input_tokens) ?? 0,
|
|
315
|
+
cache_read_tokens: count(u.cache_read_input_tokens) ?? 0
|
|
316
|
+
};
|
|
317
|
+
const total = out.input_tokens + out.output_tokens + out.cache_creation_tokens + out.cache_read_tokens;
|
|
318
|
+
return total > 0 ? out : null;
|
|
319
|
+
}
|
|
320
|
+
var MAX_MODELS = 20;
|
|
321
|
+
function extractModelUsage(evt) {
|
|
322
|
+
const m = evt?.modelUsage;
|
|
323
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) return null;
|
|
324
|
+
const rows = [];
|
|
325
|
+
for (const [model, raw] of Object.entries(m)) {
|
|
326
|
+
if (rows.length >= MAX_MODELS) break;
|
|
327
|
+
if (!model || typeof raw !== "object" || raw === null) continue;
|
|
328
|
+
rows.push({
|
|
329
|
+
model: String(model).slice(0, 120),
|
|
330
|
+
input_tokens: count(raw.inputTokens) ?? 0,
|
|
331
|
+
output_tokens: count(raw.outputTokens) ?? 0,
|
|
332
|
+
cache_read_tokens: count(raw.cacheReadInputTokens) ?? 0,
|
|
333
|
+
cache_creation_tokens: count(raw.cacheCreationInputTokens) ?? 0,
|
|
334
|
+
cost_usd: money(raw.costUSD) ?? 0
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return rows.length > 0 ? rows : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ../../scripts/virtual-office/code-runner/claude-result-event.mjs
|
|
341
|
+
function buildResultEvent(evt) {
|
|
342
|
+
const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
|
|
343
|
+
return {
|
|
344
|
+
kind: "result",
|
|
345
|
+
isError,
|
|
346
|
+
costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
|
|
347
|
+
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
348
|
+
numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null,
|
|
349
|
+
tokenUsage: extractTokenUsage(evt),
|
|
350
|
+
modelUsage: extractModelUsage(evt)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ../../scripts/virtual-office/code-runner/claude-stream-event.mjs
|
|
355
|
+
function extractText(content) {
|
|
356
|
+
if (typeof content === "string") return content.trim();
|
|
357
|
+
if (!Array.isArray(content)) return "";
|
|
358
|
+
return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
|
|
359
|
+
}
|
|
360
|
+
function parseClaudeStreamEvent(line) {
|
|
361
|
+
const trimmed = String(line || "").trim();
|
|
362
|
+
if (!trimmed) return null;
|
|
363
|
+
let event;
|
|
364
|
+
try {
|
|
365
|
+
event = JSON.parse(trimmed);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (!event || typeof event !== "object") return null;
|
|
370
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
371
|
+
const text = extractText(event.message.content);
|
|
372
|
+
const tokenUsage = extractTokenUsage({ usage: event.message.usage });
|
|
373
|
+
return text || tokenUsage ? { kind: "progress", text, ...tokenUsage ? { tokenUsage } : {} } : null;
|
|
374
|
+
}
|
|
375
|
+
return event.type === "result" ? buildResultEvent(event) : null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ../../scripts/virtual-office/code-runner/cli-version-floor.mjs
|
|
379
|
+
var MIN_CLAUDE_CLI_VERSION = "2.1.218";
|
|
380
|
+
var SECURITY_RATIONALE = "Claude Code 2.1.211/2.1.213 fixed a PreToolUse-hook bypass on unsandboxed Bash (our destructive-fs/git/cloud tripwires DO NOT FIRE on older CLIs) and worktree-subagents mutating the main checkout. 2.1.218 fixed Windows paths with a lowercase-\\u segment (e.g. ...\\utils\\, ...\\ui\\) being corrupted into CJK in tool inputs, making those files silently inaccessible \u2014 the fleet is Windows and 1,376 tracked files sit under utils/ alone. Update: npm install -g @anthropic-ai/claude-code (or the native installer).";
|
|
381
|
+
function parseCliVersion(output) {
|
|
382
|
+
const match = /\b(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?\b/.exec(String(output ?? ""));
|
|
383
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
|
|
384
|
+
}
|
|
385
|
+
function compareSemver(a, b) {
|
|
386
|
+
const pa = a.split(".").map(Number);
|
|
387
|
+
const pb = b.split(".").map(Number);
|
|
388
|
+
for (let i = 0; i < 3; i += 1) {
|
|
389
|
+
if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
390
|
+
}
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
function checkCliVersionFloor(versionOutput, { floor = MIN_CLAUDE_CLI_VERSION } = {}) {
|
|
394
|
+
const version = parseCliVersion(versionOutput);
|
|
395
|
+
if (!version) {
|
|
396
|
+
const seen = String(versionOutput ?? "").trim().slice(0, 120) || "<empty>";
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
version: null,
|
|
400
|
+
floor,
|
|
401
|
+
message: `could not parse a semver from \`claude --version\` output ("${seen}") \u2014 cannot prove the CLI meets the ${floor} security floor. ${SECURITY_RATIONALE}`
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (compareSemver(version, floor) < 0) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
version,
|
|
408
|
+
floor,
|
|
409
|
+
message: `claude CLI ${version} is BELOW the minimum security floor ${floor}. ` + SECURITY_RATIONALE
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
version,
|
|
415
|
+
floor,
|
|
416
|
+
message: `claude CLI ${version} meets the minimum security floor ${floor}`
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function applyCliVersionFloor({ versionOutput, env = process.env, log = console.error } = {}) {
|
|
420
|
+
const check = checkCliVersionFloor(versionOutput);
|
|
421
|
+
if (check.ok) return { refused: false, check, message: check.message };
|
|
422
|
+
const allowUnsafe = String(env?.VO_CLI_FLOOR_ALLOW_UNSAFE ?? "") === "1";
|
|
423
|
+
const message = `[cli-version-floor] ${allowUnsafe ? "WARNING (unsafe emergency override)" : "REFUSING"}: ` + check.message;
|
|
424
|
+
try {
|
|
425
|
+
log(message);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
return { refused: !allowUnsafe, check, message };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ../../scripts/virtual-office/code-runner/claude-auth-check.mjs
|
|
432
|
+
var FIRST_VERSION_TIMEOUT_MS = 4500;
|
|
433
|
+
var RETRY_VERSION_TIMEOUT_MS = 2e3;
|
|
434
|
+
function errorCode(error) {
|
|
435
|
+
return String(error?.code || "").toUpperCase();
|
|
436
|
+
}
|
|
437
|
+
function isTimeout(probe) {
|
|
438
|
+
return errorCode(probe?.error) === "ETIMEDOUT" || String(probe?.signal || "").toUpperCase() === "SIGTERM";
|
|
439
|
+
}
|
|
440
|
+
function notFound(probe) {
|
|
441
|
+
return errorCode(probe?.error) === "ENOENT";
|
|
442
|
+
}
|
|
443
|
+
async function checkClaudeAuth({
|
|
444
|
+
spawnVersion = spawnClaudeSync,
|
|
445
|
+
probeLogin = probeClaudeLoginState,
|
|
446
|
+
env = process.env
|
|
447
|
+
} = {}) {
|
|
448
|
+
try {
|
|
449
|
+
let probe = spawnVersion(["--version"], {
|
|
450
|
+
timeout: FIRST_VERSION_TIMEOUT_MS,
|
|
451
|
+
encoding: "utf8",
|
|
452
|
+
env
|
|
453
|
+
});
|
|
454
|
+
let retriedAfterTimeout = false;
|
|
455
|
+
if (isTimeout(probe)) {
|
|
456
|
+
retriedAfterTimeout = true;
|
|
457
|
+
probe = spawnVersion(["--version"], {
|
|
458
|
+
timeout: RETRY_VERSION_TIMEOUT_MS,
|
|
459
|
+
encoding: "utf8",
|
|
460
|
+
env
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (probe.error) {
|
|
464
|
+
if (notFound(probe)) {
|
|
465
|
+
return {
|
|
466
|
+
installed: false,
|
|
467
|
+
authenticated: false,
|
|
468
|
+
message: "claude CLI not found on PATH \u2014 it is a SEPARATE install from the Claude Desktop app and the Claude Code IDE extension. Install: npm install -g @anthropic-ai/claude-code, then sign in: claude auth login."
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
installed: true,
|
|
473
|
+
authenticated: false,
|
|
474
|
+
message: isTimeout(probe) ? "claude CLI executable was found, but its cold-start version probe timed out twice; availability will be retried without misreporting it as uninstalled." : `claude CLI executable was found, but its version probe failed: ${probe.error.message}`
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (probe.status !== 0) {
|
|
478
|
+
return { installed: true, authenticated: false, message: "claude binary exists but --version failed (auth unclear)" };
|
|
479
|
+
}
|
|
480
|
+
const floorGate = applyCliVersionFloor({ versionOutput: probe.stdout, env });
|
|
481
|
+
if (floorGate.refused) {
|
|
482
|
+
return { installed: true, authenticated: false, message: floorGate.message };
|
|
483
|
+
}
|
|
484
|
+
const loggedIn = retriedAfterTimeout ? null : probeLogin();
|
|
485
|
+
if (loggedIn === false) {
|
|
486
|
+
return {
|
|
487
|
+
installed: true,
|
|
488
|
+
authenticated: false,
|
|
489
|
+
message: "claude CLI is installed but NOT logged in \u2014 its login is SEPARATE from the Claude Desktop app and the Claude Code IDE extension. Run: claude auth login (Claude subscription), then restart the runner."
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
installed: true,
|
|
494
|
+
authenticated: true,
|
|
495
|
+
message: loggedIn === true ? "claude CLI installed and logged in (claude auth status)" : "claude binary found (login state unknown \u2014 auth check is best-effort)"
|
|
496
|
+
};
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
installed: false,
|
|
500
|
+
authenticated: false,
|
|
501
|
+
message: `checkAuth probe failed: ${error.message}`
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
507
|
+
function parseStreamEvent(line) {
|
|
508
|
+
return parseClaudeStreamEvent(line);
|
|
509
|
+
}
|
|
510
|
+
var ClaudeRunner = class {
|
|
511
|
+
get enforcesBudgetCap() {
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
get binary() {
|
|
515
|
+
return "claude";
|
|
516
|
+
}
|
|
517
|
+
buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd } = {}) {
|
|
518
|
+
return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd });
|
|
519
|
+
}
|
|
520
|
+
parseEvent(line) {
|
|
521
|
+
return parseStreamEvent(line);
|
|
522
|
+
}
|
|
523
|
+
getSpawnOptions() {
|
|
524
|
+
return { shell: false, windowsHide: true };
|
|
525
|
+
}
|
|
526
|
+
prepareSpawn({ bin, args, spawnOptions, env = process.env } = {}) {
|
|
527
|
+
if (process.platform !== "win32") return { bin, args, spawnOptions: spawnOptions ?? this.getSpawnOptions() };
|
|
528
|
+
return buildWindowsClaudeLaunch({ bin, args, env });
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Fill ANTHROPIC_API_KEY from the OS keychain when not already set (M4 BYO),
|
|
532
|
+
* so a friend who ran `vo-mcp set-key` authenticates without an env var.
|
|
533
|
+
* Explicit env wins; no key stored → unchanged (Claude Code login as before).
|
|
534
|
+
*/
|
|
535
|
+
applyAuthEnv(env = process.env) {
|
|
536
|
+
return withAnthropicKey(env);
|
|
537
|
+
}
|
|
538
|
+
costBasis(env = process.env) {
|
|
539
|
+
return String(env.ANTHROPIC_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
|
|
540
|
+
}
|
|
541
|
+
/** Describe which Anthropic auth source the spawn will use (for runner logs). */
|
|
542
|
+
describeAuth(env = process.env) {
|
|
543
|
+
return describeAnthropicAuthSource(env);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Best-effort auth check: is `claude` on PATH and can we verify login?
|
|
547
|
+
* Never throws. If we can't cheaply detect auth, we return installed:true
|
|
548
|
+
* and let the real spawn fail with a clearer error from the CLI itself.
|
|
549
|
+
*/
|
|
550
|
+
async checkAuth() {
|
|
551
|
+
return checkClaudeAuth();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var claudeRunner = new ClaudeRunner();
|
|
555
|
+
|
|
556
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
557
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
558
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
559
|
+
import { win32 } from "node:path";
|
|
560
|
+
|
|
561
|
+
// ../../scripts/virtual-office/code-runner/agent-key-store.mjs
|
|
562
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
563
|
+
var require3 = createRequire2(import.meta.url);
|
|
564
|
+
var KEY_SERVICE2 = "algosuite-vo";
|
|
565
|
+
var PROVIDER_ENV = {
|
|
566
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
567
|
+
openai: ["OPENAI_API_KEY", "CODEX_API_KEY"],
|
|
568
|
+
cursor: ["CURSOR_API_KEY"],
|
|
569
|
+
meta: ["MODEL_API_KEY"],
|
|
570
|
+
// Generic OpenAI-compatible runner (bring-your-own model + endpoint): its key
|
|
571
|
+
// is a dedicated var so it never collides with a real OpenAI/Codex key.
|
|
572
|
+
"oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"],
|
|
573
|
+
// Sovereign local inference (Ollama / LM Studio). The key is OPTIONAL — most
|
|
574
|
+
// local servers need none — and exists for locally secured endpoints only.
|
|
575
|
+
local: ["VO_CODE_RUNNER_LOCAL_API_KEY"]
|
|
576
|
+
};
|
|
577
|
+
var PROVIDER_ALIAS = {
|
|
578
|
+
claude: "anthropic",
|
|
579
|
+
anthropic: "anthropic",
|
|
580
|
+
codex: "openai",
|
|
581
|
+
openai: "openai",
|
|
582
|
+
cursor: "cursor",
|
|
583
|
+
meta: "meta",
|
|
584
|
+
muse: "meta",
|
|
585
|
+
spark: "meta",
|
|
586
|
+
"muse-spark": "meta",
|
|
587
|
+
oai: "oai-compat",
|
|
588
|
+
"oai-compat": "oai-compat",
|
|
589
|
+
local: "local",
|
|
590
|
+
ollama: "local",
|
|
591
|
+
lmstudio: "local"
|
|
592
|
+
};
|
|
593
|
+
function resolveProvider(name) {
|
|
594
|
+
const key = String(name || "").trim().toLowerCase();
|
|
595
|
+
return PROVIDER_ALIAS[key] || null;
|
|
596
|
+
}
|
|
597
|
+
function accountFor(provider) {
|
|
598
|
+
return `${provider}-api-key`;
|
|
599
|
+
}
|
|
600
|
+
var _entryCtor2;
|
|
601
|
+
var _loadTried2 = false;
|
|
602
|
+
function defaultEntryCtor2() {
|
|
603
|
+
if (_loadTried2) return _entryCtor2;
|
|
604
|
+
_loadTried2 = true;
|
|
605
|
+
try {
|
|
606
|
+
_entryCtor2 = require3("@napi-rs/keyring").Entry;
|
|
607
|
+
} catch {
|
|
608
|
+
_entryCtor2 = null;
|
|
609
|
+
}
|
|
610
|
+
return _entryCtor2;
|
|
611
|
+
}
|
|
612
|
+
function getAgentKey(provider, { EntryCtor = defaultEntryCtor2() } = {}) {
|
|
613
|
+
const p = resolveProvider(provider);
|
|
614
|
+
if (!p || !EntryCtor) return null;
|
|
615
|
+
try {
|
|
616
|
+
return new EntryCtor(KEY_SERVICE2, accountFor(p)).getPassword() || null;
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function withAgentKey(provider, baseEnv = {}, { getKey = getAgentKey } = {}) {
|
|
622
|
+
const p = resolveProvider(provider);
|
|
623
|
+
const vars = p && PROVIDER_ENV[p] || [];
|
|
624
|
+
const out = { ...baseEnv };
|
|
625
|
+
if (!p || vars.length === 0) return out;
|
|
626
|
+
if (vars.some((v) => out[v])) return out;
|
|
627
|
+
const key = getKey(p);
|
|
628
|
+
if (!key) return out;
|
|
629
|
+
for (const v of vars) out[v] = key;
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ../../scripts/virtual-office/code-runner/flat-token-usage.mjs
|
|
634
|
+
var MAX_TOKEN_COUNT2 = 1e9;
|
|
635
|
+
function count2(value) {
|
|
636
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0;
|
|
637
|
+
return Math.min(MAX_TOKEN_COUNT2, Math.round(value));
|
|
638
|
+
}
|
|
639
|
+
function extractFlatTokenUsage(usage) {
|
|
640
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
|
|
641
|
+
const rawInput = count2(usage.input_tokens ?? usage.prompt_tokens);
|
|
642
|
+
const cached = count2(
|
|
643
|
+
usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? usage.cache_read_tokens
|
|
644
|
+
);
|
|
645
|
+
const hasInclusiveCache = usage.cached_input_tokens !== void 0;
|
|
646
|
+
const out = {
|
|
647
|
+
input_tokens: hasInclusiveCache ? Math.max(0, rawInput - cached) : rawInput,
|
|
648
|
+
output_tokens: count2(usage.output_tokens ?? usage.completion_tokens),
|
|
649
|
+
cache_creation_tokens: count2(
|
|
650
|
+
usage.cache_creation_input_tokens ?? usage.cache_creation_tokens
|
|
651
|
+
),
|
|
652
|
+
cache_read_tokens: cached
|
|
653
|
+
};
|
|
654
|
+
return Object.values(out).some((value) => value > 0) ? out : null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
658
|
+
var CODEX_PREFER_LOGIN_ENV = "VO_RUNNER_CODEX_PREFER_LOGIN";
|
|
659
|
+
var LEGACY_PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
|
|
660
|
+
function isTruthyFlag2(value) {
|
|
661
|
+
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
662
|
+
}
|
|
663
|
+
function resolveCodexBinary({
|
|
664
|
+
env = process.env,
|
|
665
|
+
platform = process.platform,
|
|
666
|
+
exists = existsSync3
|
|
667
|
+
} = {}) {
|
|
668
|
+
if (platform !== "win32") return "codex";
|
|
669
|
+
const appData = String(env.APPDATA || "").trim();
|
|
670
|
+
const userProfile = String(env.USERPROFILE || "").trim();
|
|
671
|
+
const localAppData = String(env.LOCALAPPDATA || "").trim();
|
|
672
|
+
const candidates = [];
|
|
673
|
+
if (appData) {
|
|
674
|
+
candidates.push(win32.join(
|
|
675
|
+
appData,
|
|
676
|
+
"npm",
|
|
677
|
+
"node_modules",
|
|
678
|
+
"@openai",
|
|
679
|
+
"codex",
|
|
680
|
+
"node_modules",
|
|
681
|
+
"@openai",
|
|
682
|
+
"codex-win32-x64",
|
|
683
|
+
"vendor",
|
|
684
|
+
"x86_64-pc-windows-msvc",
|
|
685
|
+
"bin",
|
|
686
|
+
"codex.exe"
|
|
687
|
+
));
|
|
688
|
+
}
|
|
689
|
+
if (userProfile) {
|
|
690
|
+
candidates.push(win32.join(userProfile, ".local", "bin", "codex.exe"));
|
|
691
|
+
candidates.push(win32.join(userProfile, ".codex", "bin", "codex.exe"));
|
|
692
|
+
}
|
|
693
|
+
if (localAppData) {
|
|
694
|
+
candidates.push(win32.join(localAppData, "Microsoft", "WindowsApps", "codex.exe"));
|
|
695
|
+
}
|
|
696
|
+
const absolute = candidates.find((candidate) => exists(candidate));
|
|
697
|
+
if (absolute) return absolute;
|
|
698
|
+
return "codex";
|
|
699
|
+
}
|
|
700
|
+
function buildCodexArgs({ model, effort } = {}) {
|
|
701
|
+
const args = ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"];
|
|
702
|
+
if (model) {
|
|
703
|
+
args.push("--model", String(model));
|
|
704
|
+
}
|
|
705
|
+
if (effort) {
|
|
706
|
+
args.push("-c", `model_reasoning_effort="${String(effort)}"`);
|
|
707
|
+
}
|
|
708
|
+
args.push("-");
|
|
709
|
+
return args;
|
|
710
|
+
}
|
|
711
|
+
function itemText(item) {
|
|
712
|
+
if (!item) return "";
|
|
713
|
+
if (typeof item.text === "string") return item.text;
|
|
714
|
+
if (typeof item.message === "string") return item.message;
|
|
715
|
+
if (Array.isArray(item.content)) {
|
|
716
|
+
return item.content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
717
|
+
}
|
|
718
|
+
return "";
|
|
719
|
+
}
|
|
720
|
+
function parseCodexVersion(stdout) {
|
|
721
|
+
if (typeof stdout !== "string") return null;
|
|
722
|
+
const match = stdout.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u);
|
|
723
|
+
return match ? match[1] : null;
|
|
724
|
+
}
|
|
725
|
+
function parseCodexEvent(line) {
|
|
726
|
+
const trimmed = String(line || "").trim();
|
|
727
|
+
if (!trimmed) return null;
|
|
728
|
+
let evt;
|
|
729
|
+
try {
|
|
730
|
+
evt = JSON.parse(trimmed);
|
|
731
|
+
} catch {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
if (!evt || typeof evt !== "object") return null;
|
|
735
|
+
const type = evt.type;
|
|
736
|
+
if (type === "item.completed" && evt.item) {
|
|
737
|
+
const it = evt.item.type;
|
|
738
|
+
if (it === "agent_message" || it === "assistant_message") {
|
|
739
|
+
const text = itemText(evt.item).trim();
|
|
740
|
+
return text ? { kind: "progress", text } : null;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
if (type === "turn.completed") {
|
|
745
|
+
return {
|
|
746
|
+
kind: "result",
|
|
747
|
+
isError: false,
|
|
748
|
+
costUsd: null,
|
|
749
|
+
summary: "completed",
|
|
750
|
+
numTurns: null,
|
|
751
|
+
tokenUsage: extractFlatTokenUsage(evt.usage)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (type === "turn.failed" || type === "error") {
|
|
755
|
+
const msg = evt.error && (evt.error.message || evt.error) || evt.message || "codex run failed";
|
|
756
|
+
return { kind: "result", isError: true, costUsd: null, summary: String(msg), numTurns: null };
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
var CodexRunner = class {
|
|
761
|
+
constructor({ spawn: spawn2 = spawnSync6, resolveBinary = resolveCodexBinary, env = process.env } = {}) {
|
|
762
|
+
this.spawn = spawn2;
|
|
763
|
+
this.resolveBinary = resolveBinary;
|
|
764
|
+
this.env = env;
|
|
765
|
+
}
|
|
766
|
+
get binary() {
|
|
767
|
+
return this.resolveBinary();
|
|
768
|
+
}
|
|
769
|
+
buildArgs(opts = {}) {
|
|
770
|
+
return buildCodexArgs(opts);
|
|
771
|
+
}
|
|
772
|
+
parseEvent(line) {
|
|
773
|
+
return parseCodexEvent(line);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* SECURITY: never `shell: true` — same RCE class as cursor-runner. The old
|
|
777
|
+
* `shell: win32 && !/\.exe$/` fell back to shell mode whenever
|
|
778
|
+
* resolveCodexBinary() could not find one of its hardcoded absolute paths and
|
|
779
|
+
* returned the bare string 'codex'. Node's shell mode joins argv into
|
|
780
|
+
* `cmd /d /s /c` with windowsVerbatimArguments, and buildCodexArgs() puts the
|
|
781
|
+
* control-plane-controlled `model` into argv, so a payload of
|
|
782
|
+
* `{ agent: 'codex', model: 'gpt-5 & <cmd>' }` executed arbitrary code —
|
|
783
|
+
* including on hosts where codex is NOT installed, because cmd runs the first
|
|
784
|
+
* command, it fails, and `&` runs the rest anyway.
|
|
785
|
+
*
|
|
786
|
+
* With shell:false a `.cmd`/`.ps1` shim no longer resolves and the spawn fails
|
|
787
|
+
* closed with ENOENT, matching resolveWindowsClaudeExecutable()'s policy.
|
|
788
|
+
*/
|
|
789
|
+
getSpawnOptions() {
|
|
790
|
+
return {
|
|
791
|
+
shell: false,
|
|
792
|
+
windowsHide: true,
|
|
793
|
+
windowsVerbatimArguments: false
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Fill the OpenAI credential env var(s) (OPENAI_API_KEY / CODEX_API_KEY) from
|
|
798
|
+
* the OS keychain when not already set, so a BYO friend who ran
|
|
799
|
+
* `vo-mcp set-key --provider codex` authenticates without an env var. Explicit
|
|
800
|
+
* env wins; no key stored → unchanged (a prior `codex login` still works).
|
|
801
|
+
*/
|
|
802
|
+
applyAuthEnv(env = process.env) {
|
|
803
|
+
if (isTruthyFlag2(env[CODEX_PREFER_LOGIN_ENV]) || isTruthyFlag2(env[LEGACY_PREFER_LOGIN_ENV])) {
|
|
804
|
+
const out = { ...env };
|
|
805
|
+
delete out.OPENAI_API_KEY;
|
|
806
|
+
delete out.CODEX_API_KEY;
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
return withAgentKey("openai", env);
|
|
810
|
+
}
|
|
811
|
+
costBasis(env = process.env) {
|
|
812
|
+
return String(env.OPENAI_API_KEY || env.CODEX_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
|
|
813
|
+
}
|
|
814
|
+
/** Best-effort binary + persisted-login probe. Never throws or spends tokens. */
|
|
815
|
+
async checkAuth() {
|
|
816
|
+
try {
|
|
817
|
+
const bin = this.binary;
|
|
818
|
+
const version = this.spawn(bin, ["--version"], {
|
|
819
|
+
...this.getSpawnOptions({ bin }),
|
|
820
|
+
windowsHide: true,
|
|
821
|
+
timeout: 3e3,
|
|
822
|
+
encoding: "utf8"
|
|
823
|
+
});
|
|
824
|
+
if (version.error) {
|
|
825
|
+
return { installed: false, authenticated: false, message: `codex not found on PATH: ${version.error.message}` };
|
|
826
|
+
}
|
|
827
|
+
if (version.status !== 0) {
|
|
828
|
+
return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
|
|
829
|
+
}
|
|
830
|
+
const cliVersion = parseCodexVersion(version.stdout);
|
|
831
|
+
const versionField = cliVersion ? { version: cliVersion } : {};
|
|
832
|
+
const login = this.spawn(bin, ["login", "status"], {
|
|
833
|
+
...this.getSpawnOptions({ bin }),
|
|
834
|
+
windowsHide: true,
|
|
835
|
+
timeout: 5e3,
|
|
836
|
+
encoding: "utf8"
|
|
837
|
+
});
|
|
838
|
+
const output = `${login.stdout || ""}
|
|
839
|
+
${login.stderr || ""}`.trim();
|
|
840
|
+
if (login.error || login.status !== 0) {
|
|
841
|
+
const authEnv = this.applyAuthEnv(this.env);
|
|
842
|
+
if (authEnv.OPENAI_API_KEY || authEnv.CODEX_API_KEY) {
|
|
843
|
+
return {
|
|
844
|
+
installed: true,
|
|
845
|
+
authenticated: true,
|
|
846
|
+
...versionField,
|
|
847
|
+
message: "codex API key available (no persisted ChatGPT login)"
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
installed: true,
|
|
852
|
+
authenticated: false,
|
|
853
|
+
...versionField,
|
|
854
|
+
message: output || login.error?.message || "codex is installed but not logged in"
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
installed: true,
|
|
859
|
+
authenticated: true,
|
|
860
|
+
...versionField,
|
|
861
|
+
message: output || "codex login status succeeded"
|
|
862
|
+
};
|
|
863
|
+
} catch (err) {
|
|
864
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
var codexRunner = new CodexRunner();
|
|
869
|
+
|
|
870
|
+
// ../../scripts/virtual-office/code-runner/cursor-runner.mjs
|
|
871
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
872
|
+
function buildCursorArgs({ model, prompt } = {}) {
|
|
873
|
+
const args = ["-p", "--output-format", "stream-json", "--force"];
|
|
874
|
+
if (model) {
|
|
875
|
+
args.push("--model", String(model));
|
|
876
|
+
}
|
|
877
|
+
const p = String(prompt ?? "");
|
|
878
|
+
if (p.length > 0) {
|
|
879
|
+
args.push(p);
|
|
880
|
+
}
|
|
881
|
+
return args;
|
|
882
|
+
}
|
|
883
|
+
function messageText(message) {
|
|
884
|
+
if (!message) return "";
|
|
885
|
+
const content = message.content;
|
|
886
|
+
if (typeof content === "string") return content;
|
|
887
|
+
if (Array.isArray(content)) {
|
|
888
|
+
return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
889
|
+
}
|
|
890
|
+
return "";
|
|
891
|
+
}
|
|
892
|
+
function parseCursorEvent(line) {
|
|
893
|
+
const trimmed = String(line || "").trim();
|
|
894
|
+
if (!trimmed) return null;
|
|
895
|
+
let evt;
|
|
896
|
+
try {
|
|
897
|
+
evt = JSON.parse(trimmed);
|
|
898
|
+
} catch {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
if (!evt || typeof evt !== "object") return null;
|
|
902
|
+
if (evt.type === "assistant") {
|
|
903
|
+
const text = messageText(evt.message).trim();
|
|
904
|
+
return text ? { kind: "progress", text } : null;
|
|
905
|
+
}
|
|
906
|
+
if (evt.type === "result") {
|
|
907
|
+
const isError = Boolean(evt.is_error) || evt.subtype === "error";
|
|
908
|
+
const tokenUsage = extractFlatTokenUsage(evt.usage);
|
|
909
|
+
return {
|
|
910
|
+
kind: "result",
|
|
911
|
+
isError,
|
|
912
|
+
costUsd: null,
|
|
913
|
+
...tokenUsage ? { tokenUsage } : {},
|
|
914
|
+
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
915
|
+
numTurns: null
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
var CursorRunner = class {
|
|
921
|
+
get binary() {
|
|
922
|
+
return "cursor-agent";
|
|
923
|
+
}
|
|
924
|
+
buildArgs(opts = {}) {
|
|
925
|
+
return buildCursorArgs(opts);
|
|
926
|
+
}
|
|
927
|
+
parseEvent(line) {
|
|
928
|
+
return parseCursorEvent(line);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* SECURITY: never `shell: true`. Node's shell mode on Windows joins argv and
|
|
932
|
+
* hands it to `cmd /d /s /c` with windowsVerbatimArguments, so every cmd
|
|
933
|
+
* metacharacter (& | > ^) in an argument is interpreted by the shell. This
|
|
934
|
+
* runner puts two control-plane-controlled strings into argv — `task.model`
|
|
935
|
+
* and the composed prompt (buildCursorArgs) — so shell mode turned a task
|
|
936
|
+
* payload into arbitrary host code execution. It fired even without
|
|
937
|
+
* cursor-agent installed: cmd runs the first command, it fails, and `&` runs
|
|
938
|
+
* the rest anyway. With shell:false argv goes straight to CreateProcess and
|
|
939
|
+
* metacharacters are inert.
|
|
940
|
+
*
|
|
941
|
+
* Consequence on Windows: a `.cmd`/`.ps1` shim no longer resolves, so the
|
|
942
|
+
* runner fails closed with ENOENT rather than executing through a shell —
|
|
943
|
+
* the same policy resolveWindowsClaudeExecutable() enforces for Claude.
|
|
944
|
+
*/
|
|
945
|
+
getSpawnOptions() {
|
|
946
|
+
return {
|
|
947
|
+
shell: false,
|
|
948
|
+
windowsHide: true,
|
|
949
|
+
windowsVerbatimArguments: false
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Fill CURSOR_API_KEY from the OS keychain when not already set, so a BYO
|
|
954
|
+
* friend who ran `vo-mcp set-key --provider cursor` authenticates without an
|
|
955
|
+
* env var. Explicit env wins; no key stored → a prior `cursor-agent login`.
|
|
956
|
+
*/
|
|
957
|
+
applyAuthEnv(env = process.env) {
|
|
958
|
+
return withAgentKey("cursor", env);
|
|
959
|
+
}
|
|
960
|
+
costBasis(env = process.env) {
|
|
961
|
+
return env.CURSOR_API_KEY ? "vendor_billed" : "unknown";
|
|
962
|
+
}
|
|
963
|
+
/** Best-effort: is `cursor-agent` on PATH? Never throws. */
|
|
964
|
+
async checkAuth() {
|
|
965
|
+
try {
|
|
966
|
+
const { status, error } = spawnSync7("cursor-agent", ["--version"], {
|
|
967
|
+
shell: false,
|
|
968
|
+
windowsHide: true,
|
|
969
|
+
timeout: 3e3,
|
|
970
|
+
stdio: "ignore"
|
|
971
|
+
});
|
|
972
|
+
if (error) {
|
|
973
|
+
return { installed: false, authenticated: false, message: `cursor-agent not found on PATH: ${error.message}` };
|
|
974
|
+
}
|
|
975
|
+
if (status !== 0) {
|
|
976
|
+
return { installed: true, authenticated: false, message: "cursor-agent exists but --version failed (auth unclear)" };
|
|
977
|
+
}
|
|
978
|
+
if (process.env.VO_CODE_RUNNER_ALLOW_UNMETERED_CURSOR !== "1") {
|
|
979
|
+
return {
|
|
980
|
+
installed: true,
|
|
981
|
+
authenticated: false,
|
|
982
|
+
message: "cursor-agent disabled: its documented stream-JSON result has no token/cost fields; set VO_CODE_RUNNER_ALLOW_UNMETERED_CURSOR=1 only for an explicit unmeasured experiment"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
installed: true,
|
|
987
|
+
authenticated: true,
|
|
988
|
+
message: "cursor-agent found (EXPERIMENTAL: headless mode may need a TTY; auth check is best-effort)"
|
|
989
|
+
};
|
|
990
|
+
} catch (err) {
|
|
991
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
var cursorRunner = new CursorRunner();
|
|
996
|
+
|
|
997
|
+
// ../../scripts/virtual-office/code-runner/local-model-runner.mjs
|
|
998
|
+
var LOCAL_API_KEY_ENV = "VO_CODE_RUNNER_LOCAL_API_KEY";
|
|
999
|
+
var LOCAL_PROVIDERS = ["ollama", "lmstudio"];
|
|
1000
|
+
var DEFAULT_LOCAL_PROVIDER = "ollama";
|
|
1001
|
+
var LOCAL_PROBE_URLS = {
|
|
1002
|
+
ollama: "http://127.0.0.1:11434/api/version",
|
|
1003
|
+
lmstudio: "http://127.0.0.1:1234/v1/models"
|
|
1004
|
+
};
|
|
1005
|
+
function resolveLocalProvider(env = process.env) {
|
|
1006
|
+
return String(env.VO_CODE_RUNNER_LOCAL_PROVIDER || "").trim().toLowerCase() || DEFAULT_LOCAL_PROVIDER;
|
|
1007
|
+
}
|
|
1008
|
+
var remoteDesiredLocalModel = "";
|
|
1009
|
+
function resolveLocalModel(env = process.env) {
|
|
1010
|
+
return String(env.VO_CODE_RUNNER_LOCAL_MODEL || "").trim() || remoteDesiredLocalModel;
|
|
1011
|
+
}
|
|
1012
|
+
function resolveLocalBaseUrl(env = process.env) {
|
|
1013
|
+
return String(env.VO_CODE_RUNNER_LOCAL_BASE_URL || "").trim();
|
|
1014
|
+
}
|
|
1015
|
+
var LOCAL_MODEL_RE = new RegExp("^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$");
|
|
1016
|
+
function isValidLocalModel(model) {
|
|
1017
|
+
return LOCAL_MODEL_RE.test(String(model || ""));
|
|
1018
|
+
}
|
|
1019
|
+
function isLoopbackBaseUrl(url) {
|
|
1020
|
+
const raw = String(url || "").trim();
|
|
1021
|
+
if (!/^https?:\/\/[^\s"'`\\]+$/.test(raw)) return false;
|
|
1022
|
+
let parsed;
|
|
1023
|
+
try {
|
|
1024
|
+
parsed = new URL(raw);
|
|
1025
|
+
} catch {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const host = parsed.hostname.toLowerCase();
|
|
1029
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1030
|
+
}
|
|
1031
|
+
function buildLocalArgs(opts = {}, env = process.env) {
|
|
1032
|
+
const provider = resolveLocalProvider(env);
|
|
1033
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`local-model runner: unknown VO_CODE_RUNNER_LOCAL_PROVIDER "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")}).`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const model = resolveLocalModel(env);
|
|
1039
|
+
if (!model) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
"local-model runner: set VO_CODE_RUNNER_LOCAL_MODEL to a model your local server already has (recommended: gpt-oss:20b \u2014 codex drives its tool loop reliably; generic chat models often cannot edit files agentically), or pick an already-pulled model from the web runner settings. Refusing to run with no explicit model (fail-closed)."
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
if (!isValidLocalModel(model)) {
|
|
1045
|
+
throw new Error(`local-model runner: "${model}" is not a valid local model id.`);
|
|
1046
|
+
}
|
|
1047
|
+
const baseUrl = resolveLocalBaseUrl(env);
|
|
1048
|
+
if (baseUrl && !isLoopbackBaseUrl(baseUrl)) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
"local-model runner: VO_CODE_RUNNER_LOCAL_BASE_URL must be a loopback http(s) URL (localhost / 127.0.0.1 / [::1]). Remote endpoints are refused \u2014 use the Model Firewall lanes for hosted providers."
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const args = [
|
|
1054
|
+
"exec",
|
|
1055
|
+
"--json",
|
|
1056
|
+
"-c",
|
|
1057
|
+
'approval_policy="never"',
|
|
1058
|
+
"--sandbox",
|
|
1059
|
+
"workspace-write",
|
|
1060
|
+
"--skip-git-repo-check",
|
|
1061
|
+
"--oss",
|
|
1062
|
+
"--local-provider",
|
|
1063
|
+
provider,
|
|
1064
|
+
"--model",
|
|
1065
|
+
model
|
|
1066
|
+
];
|
|
1067
|
+
if (opts.effort) {
|
|
1068
|
+
args.push("-c", `model_reasoning_effort="${String(opts.effort)}"`);
|
|
1069
|
+
}
|
|
1070
|
+
args.push("-");
|
|
1071
|
+
return args;
|
|
1072
|
+
}
|
|
1073
|
+
function applyLocalAuthEnv(baseEnv = process.env, configEnv = process.env) {
|
|
1074
|
+
const out = withAgentKey("local", baseEnv);
|
|
1075
|
+
const baseUrl = resolveLocalBaseUrl(configEnv);
|
|
1076
|
+
if (baseUrl && isLoopbackBaseUrl(baseUrl) && resolveLocalProvider(configEnv) === "ollama" && !String(out.OLLAMA_HOST || "").trim()) {
|
|
1077
|
+
out.OLLAMA_HOST = baseUrl.replace(/\/+$/, "");
|
|
1078
|
+
}
|
|
1079
|
+
return out;
|
|
1080
|
+
}
|
|
1081
|
+
var LocalModelRunner = class {
|
|
1082
|
+
constructor({
|
|
1083
|
+
spawn: spawn2 = null,
|
|
1084
|
+
resolveBinary = resolveCodexBinary,
|
|
1085
|
+
env = process.env,
|
|
1086
|
+
fetchImpl = globalThis.fetch
|
|
1087
|
+
} = {}) {
|
|
1088
|
+
this.spawn = spawn2;
|
|
1089
|
+
this.resolveBinary = resolveBinary;
|
|
1090
|
+
this.env = env;
|
|
1091
|
+
this.fetchImpl = fetchImpl;
|
|
1092
|
+
}
|
|
1093
|
+
/** Codex is the transport binary; the model/endpoint are the user's. */
|
|
1094
|
+
get binary() {
|
|
1095
|
+
return this.resolveBinary();
|
|
1096
|
+
}
|
|
1097
|
+
buildArgs(opts = {}) {
|
|
1098
|
+
return buildLocalArgs(opts, this.env);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Codex JSONL events map identically, except a sovereign local model has no
|
|
1102
|
+
* vendor bill. Codex omits total_cost_usd for OSS runs; turn that known fact
|
|
1103
|
+
* into a measured zero at the producer so readers never have to guess.
|
|
1104
|
+
*/
|
|
1105
|
+
parseEvent(line) {
|
|
1106
|
+
const event = parseCodexEvent(line);
|
|
1107
|
+
return event?.kind === "result" ? { ...event, costUsd: 0 } : event;
|
|
1108
|
+
}
|
|
1109
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. The model id is
|
|
1110
|
+
// control-plane-influenced and validated, but shell:false is the hard floor.
|
|
1111
|
+
getSpawnOptions() {
|
|
1112
|
+
return {
|
|
1113
|
+
shell: false,
|
|
1114
|
+
windowsHide: true,
|
|
1115
|
+
windowsVerbatimArguments: false
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
applyAuthEnv(env = process.env) {
|
|
1119
|
+
return applyLocalAuthEnv(env, this.env);
|
|
1120
|
+
}
|
|
1121
|
+
costBasis() {
|
|
1122
|
+
return "local_zero";
|
|
1123
|
+
}
|
|
1124
|
+
describeAuth(env = process.env) {
|
|
1125
|
+
const authEnv = this.applyAuthEnv(env);
|
|
1126
|
+
const provider = resolveLocalProvider(this.env);
|
|
1127
|
+
const model = resolveLocalModel(this.env) || "<unset>";
|
|
1128
|
+
const hasKey = Boolean(String(authEnv[LOCAL_API_KEY_ENV] || "").trim());
|
|
1129
|
+
return `local provider=${provider} model=${model} key=${hasKey ? "set" : "none (optional)"}`;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Best-effort: codex transport present AND the local inference endpoint
|
|
1133
|
+
* answers. Never throws, never spends tokens; the endpoint probe is bounded
|
|
1134
|
+
* to 1.5s so a stopped Ollama can't hang availability checks.
|
|
1135
|
+
*/
|
|
1136
|
+
async checkAuth() {
|
|
1137
|
+
const provider = resolveLocalProvider(this.env);
|
|
1138
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
1139
|
+
return {
|
|
1140
|
+
installed: false,
|
|
1141
|
+
authenticated: false,
|
|
1142
|
+
message: `unknown local provider "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")})`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const model = resolveLocalModel(this.env);
|
|
1146
|
+
const override = resolveLocalBaseUrl(this.env);
|
|
1147
|
+
if (override && !isLoopbackBaseUrl(override)) {
|
|
1148
|
+
return {
|
|
1149
|
+
installed: true,
|
|
1150
|
+
authenticated: false,
|
|
1151
|
+
message: "VO_CODE_RUNNER_LOCAL_BASE_URL is not loopback \u2014 refused (fail-closed)"
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
const probeUrl = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/version` : LOCAL_PROBE_URLS[provider];
|
|
1155
|
+
let endpointUp = false;
|
|
1156
|
+
let probeNote = "";
|
|
1157
|
+
try {
|
|
1158
|
+
const res = await this.fetchImpl(probeUrl, { signal: AbortSignal.timeout(1500) });
|
|
1159
|
+
endpointUp = Boolean(res?.ok);
|
|
1160
|
+
if (!endpointUp) probeNote = `endpoint ${probeUrl} answered HTTP ${res?.status}`;
|
|
1161
|
+
} catch {
|
|
1162
|
+
probeNote = `no local inference server answering at ${probeUrl}`;
|
|
1163
|
+
}
|
|
1164
|
+
if (!endpointUp) {
|
|
1165
|
+
return {
|
|
1166
|
+
installed: true,
|
|
1167
|
+
authenticated: false,
|
|
1168
|
+
message: `${probeNote} \u2014 start ${provider === "ollama" ? "Ollama" : "LM Studio"} first`
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
if (!model) {
|
|
1172
|
+
return {
|
|
1173
|
+
installed: true,
|
|
1174
|
+
authenticated: false,
|
|
1175
|
+
message: `${provider} is running but VO_CODE_RUNNER_LOCAL_MODEL is not set`
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
installed: true,
|
|
1180
|
+
authenticated: true,
|
|
1181
|
+
message: `${provider} reachable; model "${model}" configured (local-only, no cloud spend)`
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
var localModelRunner = new LocalModelRunner();
|
|
1186
|
+
|
|
1187
|
+
// ../../scripts/virtual-office/code-runner/meta-runner.mjs
|
|
1188
|
+
var META_API_KEY_ENV = "MODEL_API_KEY";
|
|
1189
|
+
var META_API_KEY_ALIAS = "META_API";
|
|
1190
|
+
function applyMetaAuthEnv(baseEnv = process.env) {
|
|
1191
|
+
const out = withAgentKey("meta", baseEnv);
|
|
1192
|
+
if (!String(out[META_API_KEY_ENV] || "").trim() && String(out[META_API_KEY_ALIAS] || "").trim()) {
|
|
1193
|
+
out[META_API_KEY_ENV] = out[META_API_KEY_ALIAS];
|
|
1194
|
+
}
|
|
1195
|
+
return out;
|
|
1196
|
+
}
|
|
1197
|
+
function buildMetaArgs(opts = {}) {
|
|
1198
|
+
void opts;
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
"Muse Spark full-repository coding is disabled by the AlgoSuite Model Firewall policy. Use Muse only as a restricted reviewer through a sanitized task capsule."
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
var MetaRunner = class {
|
|
1204
|
+
get binary() {
|
|
1205
|
+
return resolveCodexBinary();
|
|
1206
|
+
}
|
|
1207
|
+
buildArgs(opts = {}) {
|
|
1208
|
+
return buildMetaArgs(opts);
|
|
1209
|
+
}
|
|
1210
|
+
parseEvent(line) {
|
|
1211
|
+
return parseCodexEvent(line);
|
|
1212
|
+
}
|
|
1213
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. Inert today (buildArgs
|
|
1214
|
+
// throws) but this goes hot the moment the transport is enabled.
|
|
1215
|
+
getSpawnOptions() {
|
|
1216
|
+
return {
|
|
1217
|
+
shell: false,
|
|
1218
|
+
windowsHide: true,
|
|
1219
|
+
windowsVerbatimArguments: false
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
applyAuthEnv(env = process.env) {
|
|
1223
|
+
return applyMetaAuthEnv(env);
|
|
1224
|
+
}
|
|
1225
|
+
describeAuth(env = process.env) {
|
|
1226
|
+
const authEnv = applyMetaAuthEnv(env);
|
|
1227
|
+
const hasKey = Boolean(String(authEnv[META_API_KEY_ENV] || "").trim());
|
|
1228
|
+
return `meta muse-spark key=${hasKey ? "set" : "MISSING"} transport=codex`;
|
|
1229
|
+
}
|
|
1230
|
+
async checkAuth() {
|
|
1231
|
+
return {
|
|
1232
|
+
installed: false,
|
|
1233
|
+
authenticated: false,
|
|
1234
|
+
message: "Muse Spark coding is disabled; sanitized Model Firewall review only"
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
var metaRunner = new MetaRunner();
|
|
1239
|
+
|
|
1240
|
+
// ../../scripts/virtual-office/code-runner/openai-compatible-runner.mjs
|
|
1241
|
+
var OAI_API_KEY_ENV = "VO_CODE_RUNNER_OAI_API_KEY";
|
|
1242
|
+
function resolveOaiBaseUrl(env = process.env) {
|
|
1243
|
+
return String(env.VO_CODE_RUNNER_OAI_BASE_URL || "").trim();
|
|
1244
|
+
}
|
|
1245
|
+
var OpenAICompatibleRunner = class {
|
|
1246
|
+
/** Codex is the transport binary. */
|
|
1247
|
+
get binary() {
|
|
1248
|
+
return resolveCodexBinary();
|
|
1249
|
+
}
|
|
1250
|
+
buildArgs(opts = {}) {
|
|
1251
|
+
void opts;
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
"OpenAI-compatible full-repository coding is disabled. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
/** Codex JSONL events map identically → reuse the proven parser. */
|
|
1257
|
+
parseEvent(line) {
|
|
1258
|
+
return parseCodexEvent(line);
|
|
1259
|
+
}
|
|
1260
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. Inert today (buildArgs
|
|
1261
|
+
// throws) but this goes hot the moment the transport is enabled.
|
|
1262
|
+
getSpawnOptions() {
|
|
1263
|
+
return {
|
|
1264
|
+
shell: false,
|
|
1265
|
+
windowsHide: true,
|
|
1266
|
+
windowsVerbatimArguments: false
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/** Fill the BYO key env var from the OS keychain when not already set. */
|
|
1270
|
+
applyAuthEnv(env = process.env) {
|
|
1271
|
+
return withAgentKey("oai-compat", env);
|
|
1272
|
+
}
|
|
1273
|
+
describeAuth(env = process.env) {
|
|
1274
|
+
const hasKey = Boolean(String(env[OAI_API_KEY_ENV] || "").trim());
|
|
1275
|
+
const baseUrl = resolveOaiBaseUrl(env);
|
|
1276
|
+
return `oai-compat endpoint=${baseUrl || "<unset>"} key=${hasKey ? "set" : "MISSING"}`;
|
|
1277
|
+
}
|
|
1278
|
+
/** Best-effort: base URL chosen AND the codex transport is installed. */
|
|
1279
|
+
async checkAuth() {
|
|
1280
|
+
return {
|
|
1281
|
+
installed: false,
|
|
1282
|
+
authenticated: false,
|
|
1283
|
+
message: "OpenAI-compatible coding is disabled; sanitized Model Firewall task capsules only"
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
var openaiCompatibleRunner = new OpenAICompatibleRunner();
|
|
1288
|
+
|
|
1289
|
+
// ../../scripts/virtual-office/code-runner/agent-runner-interface.mjs
|
|
1290
|
+
function validateAgentRunner(runner) {
|
|
1291
|
+
if (!runner || typeof runner !== "object") {
|
|
1292
|
+
throw new TypeError("AgentRunner must be an object");
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof runner.binary !== "string" || runner.binary.length === 0) {
|
|
1295
|
+
throw new TypeError("AgentRunner.binary must be a non-empty string");
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof runner.buildArgs !== "function") {
|
|
1298
|
+
throw new TypeError("AgentRunner.buildArgs must be a function");
|
|
1299
|
+
}
|
|
1300
|
+
if (typeof runner.parseEvent !== "function") {
|
|
1301
|
+
throw new TypeError("AgentRunner.parseEvent must be a function");
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof runner.getSpawnOptions !== "function") {
|
|
1304
|
+
throw new TypeError("AgentRunner.getSpawnOptions must be a function");
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof runner.checkAuth !== "function") {
|
|
1307
|
+
throw new TypeError("AgentRunner.checkAuth must be a function");
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// ../../scripts/virtual-office/code-runner/resolve-runner.mjs
|
|
1312
|
+
var DEFAULT_AGENT = "claude";
|
|
1313
|
+
var RUNNERS = {
|
|
1314
|
+
claude: claudeRunner,
|
|
1315
|
+
codex: codexRunner,
|
|
1316
|
+
cursor: cursorRunner,
|
|
1317
|
+
// `local` = sovereign local inference (Ollama / LM Studio; Mistral/Llama-class
|
|
1318
|
+
// models) — free tier of the pricing pivot; nothing leaves the user's machine.
|
|
1319
|
+
local: localModelRunner,
|
|
1320
|
+
meta: metaRunner,
|
|
1321
|
+
oai: openaiCompatibleRunner
|
|
1322
|
+
};
|
|
1323
|
+
function listAgents() {
|
|
1324
|
+
return Object.keys(RUNNERS);
|
|
1325
|
+
}
|
|
1326
|
+
function inferAgentFromBin(bin) {
|
|
1327
|
+
const raw = String(bin || "").trim().toLowerCase();
|
|
1328
|
+
if (!raw) return null;
|
|
1329
|
+
const base = raw.replace(/\\/g, "/").split("/").pop() || raw;
|
|
1330
|
+
if (base.includes("codex")) return "codex";
|
|
1331
|
+
if (base.includes("cursor-agent") || base === "cursor" || base.startsWith("cursor.")) return "cursor";
|
|
1332
|
+
if (base.includes("claude")) return "claude";
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
function inferAgentFromEnvBin(env) {
|
|
1336
|
+
for (const bin of [
|
|
1337
|
+
env.VO_CODE_RUNNER_BIN,
|
|
1338
|
+
env.VO_CODE_RUNNER_CLAUDE_BIN
|
|
1339
|
+
]) {
|
|
1340
|
+
const agent2 = inferAgentFromBin(bin);
|
|
1341
|
+
if (agent2) return { agent: agent2, bin };
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
function resolveRunner(env = process.env, { warn = () => {
|
|
1346
|
+
} } = {}) {
|
|
1347
|
+
const explicitAgent = String(env.VO_CODE_RUNNER_AGENT || env.VO_AGENT || "").trim();
|
|
1348
|
+
const inferred = explicitAgent ? null : inferAgentFromEnvBin(env);
|
|
1349
|
+
const raw = String(explicitAgent || inferred?.agent || DEFAULT_AGENT).trim().toLowerCase();
|
|
1350
|
+
let agent2 = raw;
|
|
1351
|
+
let fellBack = false;
|
|
1352
|
+
let runner = RUNNERS[agent2];
|
|
1353
|
+
if (inferred && runner) {
|
|
1354
|
+
try {
|
|
1355
|
+
warn(`VO_CODE_RUNNER_AGENT not set; inferred "${agent2}" from runner binary "${inferred.bin}"`);
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!runner) {
|
|
1360
|
+
try {
|
|
1361
|
+
warn(`unknown VO_CODE_RUNNER_AGENT "${raw}"; falling back to "${DEFAULT_AGENT}" (known: ${listAgents().join(", ")})`);
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
agent2 = DEFAULT_AGENT;
|
|
1365
|
+
runner = RUNNERS[DEFAULT_AGENT];
|
|
1366
|
+
fellBack = true;
|
|
1367
|
+
}
|
|
1368
|
+
validateAgentRunner(runner);
|
|
1369
|
+
const runnerBin = env.VO_CODE_RUNNER_BIN || (inferred && inferred.agent === agent2 ? inferred.bin : "") || (agent2 === "claude" ? env.VO_CODE_RUNNER_CLAUDE_BIN : "") || runner.binary;
|
|
1370
|
+
return { agent: agent2, runner, runnerBin, fellBack };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// ../../scripts/virtual-office/code-runner/agent-auth-probe-cli.mjs
|
|
1374
|
+
var agent = String(process.argv[2] || "");
|
|
1375
|
+
try {
|
|
1376
|
+
const result = await resolveRunner({ ...process.env, VO_CODE_RUNNER_AGENT: agent }).runner.checkAuth();
|
|
1377
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1378
|
+
`);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
process.stdout.write(`${JSON.stringify({
|
|
1381
|
+
installed: false,
|
|
1382
|
+
authenticated: false,
|
|
1383
|
+
message: String(error?.message || error)
|
|
1384
|
+
})}
|
|
1385
|
+
`);
|
|
1386
|
+
}
|