@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.
@@ -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
+ }