@algosuite/vo-mcp 0.2.0-beta.7 → 0.2.0-beta.71
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/README.md +29 -3
- package/bin/vo-mcp +6 -3
- package/dist/agent-auth-probe-cli.mjs +1754 -0
- package/dist/autostart-cli.js +118 -60
- package/dist/autostart-cli.js.map +1 -2
- package/dist/ci/check-local-pr-overlap.js +107644 -0
- package/dist/cli.js +2905 -515
- package/dist/cli.js.map +3 -4
- package/dist/index.js +2495 -370
- package/dist/index.js.map +3 -4
- package/dist/install-cli.js +660 -115
- package/dist/install-cli.js.map +3 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +1 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +1 -2
- package/dist/runner-cli.js +14594 -3254
- package/dist/runner-cli.js.map +3 -4
- package/dist/runner-supervisor.js +3780 -0
- package/dist/runner-supervisor.js.map +6 -0
- package/dist/set-key-cli.js +82 -8
- package/dist/set-key-cli.js.map +1 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +6 -0
- package/dist/thresholds.json +64 -0
- package/dist/update-cli.js +75 -12
- package/dist/update-cli.js.map +3 -4
- package/package.json +5 -3
|
@@ -0,0 +1,1754 @@
|
|
|
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/claude-credential-choice.mjs
|
|
155
|
+
var PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
|
|
156
|
+
var CLAUDE_PREFER_LOGIN_ENV = "VO_RUNNER_CLAUDE_PREFER_LOGIN";
|
|
157
|
+
var PREFER_KEY_ENV = "VO_RUNNER_PREFER_KEY";
|
|
158
|
+
var CLAUDE_PREFER_KEY_ENV = "VO_RUNNER_CLAUDE_PREFER_KEY";
|
|
159
|
+
var CLAUDE_CREDENTIAL_SOURCE = Object.freeze({
|
|
160
|
+
/** PREFER_LOGIN set (and not overridden): any API key is ignored. */
|
|
161
|
+
PREFER_LOGIN: "prefer_login",
|
|
162
|
+
/** An explicit ANTHROPIC_API_KEY in the environment — the manual override. */
|
|
163
|
+
ENV_KEY: "env_key",
|
|
164
|
+
/** No key anywhere; the spawn falls through to the login session. */
|
|
165
|
+
NO_KEY: "no_key",
|
|
166
|
+
/** A stored key, used because the operator explicitly opted out of tier 1. */
|
|
167
|
+
KEYCHAIN_PREFER_KEY: "keychain_prefer_key",
|
|
168
|
+
/** A stored key exists but a proven live subscription outranks it. */
|
|
169
|
+
SUBSCRIPTION_WINS: "subscription_wins",
|
|
170
|
+
/** A stored key, used because no live subscription was proven. */
|
|
171
|
+
KEYCHAIN: "keychain"
|
|
172
|
+
});
|
|
173
|
+
function isTruthyFlag(v) {
|
|
174
|
+
const s = String(v ?? "").trim().toLowerCase();
|
|
175
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
176
|
+
}
|
|
177
|
+
function wantsLogin(env) {
|
|
178
|
+
return isTruthyFlag(env[CLAUDE_PREFER_LOGIN_ENV]) || isTruthyFlag(env[PREFER_LOGIN_ENV]);
|
|
179
|
+
}
|
|
180
|
+
function wantsKey(env) {
|
|
181
|
+
return isTruthyFlag(env[CLAUDE_PREFER_KEY_ENV]) || isTruthyFlag(env[PREFER_KEY_ENV]);
|
|
182
|
+
}
|
|
183
|
+
function classifyClaudeCredential(baseEnv = {}, { getKey, probeLogin } = {}) {
|
|
184
|
+
const preferKey = wantsKey(baseEnv);
|
|
185
|
+
if (!preferKey && wantsLogin(baseEnv)) {
|
|
186
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN, key: null };
|
|
187
|
+
}
|
|
188
|
+
if (baseEnv.ANTHROPIC_API_KEY) {
|
|
189
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.ENV_KEY, key: null };
|
|
190
|
+
}
|
|
191
|
+
const key = getKey();
|
|
192
|
+
if (!key) {
|
|
193
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.NO_KEY, key: null };
|
|
194
|
+
}
|
|
195
|
+
if (preferKey) {
|
|
196
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY, key };
|
|
197
|
+
}
|
|
198
|
+
if (probeLogin() === true) {
|
|
199
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS, key: null };
|
|
200
|
+
}
|
|
201
|
+
return { source: CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN, key };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ../../scripts/virtual-office/code-runner/anthropic-key-store.mjs
|
|
205
|
+
var require2 = createRequire(import.meta.url);
|
|
206
|
+
var KEY_SERVICE = "algosuite-vo";
|
|
207
|
+
var KEY_ACCOUNT = "anthropic-api-key";
|
|
208
|
+
var _entryCtor;
|
|
209
|
+
var _loadTried = false;
|
|
210
|
+
function defaultEntryCtor() {
|
|
211
|
+
if (_loadTried) return _entryCtor;
|
|
212
|
+
_loadTried = true;
|
|
213
|
+
try {
|
|
214
|
+
_entryCtor = require2("@napi-rs/keyring").Entry;
|
|
215
|
+
} catch {
|
|
216
|
+
_entryCtor = null;
|
|
217
|
+
}
|
|
218
|
+
return _entryCtor;
|
|
219
|
+
}
|
|
220
|
+
function getAnthropicKey({ EntryCtor = defaultEntryCtor() } = {}) {
|
|
221
|
+
if (!EntryCtor) return null;
|
|
222
|
+
try {
|
|
223
|
+
return new EntryCtor(KEY_SERVICE, KEY_ACCOUNT).getPassword() || null;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function withAnthropicKey(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
|
|
229
|
+
const { source, key } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
|
|
230
|
+
const next = { ...baseEnv };
|
|
231
|
+
if (source === CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN) {
|
|
232
|
+
delete next.ANTHROPIC_API_KEY;
|
|
233
|
+
return next;
|
|
234
|
+
}
|
|
235
|
+
if (key !== null) next.ANTHROPIC_API_KEY = key;
|
|
236
|
+
return next;
|
|
237
|
+
}
|
|
238
|
+
function claudeCostBasis(env = process.env) {
|
|
239
|
+
return String(env.ANTHROPIC_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
|
|
240
|
+
}
|
|
241
|
+
var AUTH_SOURCE_DESCRIPTION = Object.freeze({
|
|
242
|
+
[CLAUDE_CREDENTIAL_SOURCE.PREFER_LOGIN]: "claude auth login (VO_RUNNER_PREFER_LOGIN set \u2014 any API key ignored)",
|
|
243
|
+
[CLAUDE_CREDENTIAL_SOURCE.ENV_KEY]: "ANTHROPIC_API_KEY from environment",
|
|
244
|
+
[CLAUDE_CREDENTIAL_SOURCE.NO_KEY]: "claude auth login session (no API key set)",
|
|
245
|
+
[CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN_PREFER_KEY]: "ANTHROPIC_API_KEY from OS keychain (VO_RUNNER_PREFER_KEY set \u2014 subscription ignored)",
|
|
246
|
+
[CLAUDE_CREDENTIAL_SOURCE.SUBSCRIPTION_WINS]: "claude auth login session (subscription beats the stored keychain key)",
|
|
247
|
+
[CLAUDE_CREDENTIAL_SOURCE.KEYCHAIN]: "ANTHROPIC_API_KEY from OS keychain"
|
|
248
|
+
});
|
|
249
|
+
function describeAnthropicAuthSource(baseEnv = {}, { getKey = getAnthropicKey, probeLogin = probeClaudeLoginState } = {}) {
|
|
250
|
+
const { source } = classifyClaudeCredential(baseEnv, { getKey, probeLogin });
|
|
251
|
+
return AUTH_SOURCE_DESCRIPTION[source];
|
|
252
|
+
}
|
|
253
|
+
function probeClaudeLoginState({
|
|
254
|
+
spawn: spawn2 = spawnSync2,
|
|
255
|
+
buildWindowsLaunch = buildWindowsClaudeLaunch,
|
|
256
|
+
platform = process.platform
|
|
257
|
+
} = {}) {
|
|
258
|
+
try {
|
|
259
|
+
const launch = platform === "win32" ? buildWindowsLaunch({ bin: "claude", args: ["auth", "status"] }) : { bin: "claude", args: ["auth", "status"], spawnOptions: { windowsHide: true } };
|
|
260
|
+
const st = spawn2(launch.bin, launch.args, { ...launch.spawnOptions, timeout: 5e3, encoding: "utf8" });
|
|
261
|
+
const parsed = JSON.parse(String(st.stdout || "").trim() || "{}");
|
|
262
|
+
return typeof parsed.loggedIn === "boolean" ? parsed.loggedIn : null;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ../../scripts/virtual-office/code-runner/sandbox/sandbox-docker.mjs
|
|
269
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
270
|
+
|
|
271
|
+
// ../../scripts/virtual-office/code-runner/context7-mcp.mjs
|
|
272
|
+
var CONTEXT7_URL = "https://mcp.context7.com/mcp";
|
|
273
|
+
function context7McpConfig(env = process.env) {
|
|
274
|
+
if (env.VO_ENABLE_CONTEXT7 !== "1") return null;
|
|
275
|
+
const url = env.VO_CONTEXT7_URL && env.VO_CONTEXT7_URL.trim() || CONTEXT7_URL;
|
|
276
|
+
const server = { type: "http", url };
|
|
277
|
+
if (env.CONTEXT7_API_KEY && env.CONTEXT7_API_KEY.trim()) {
|
|
278
|
+
server.headers = { CONTEXT7_API_KEY: env.CONTEXT7_API_KEY.trim() };
|
|
279
|
+
}
|
|
280
|
+
return { mcpServers: { context7: server } };
|
|
281
|
+
}
|
|
282
|
+
function context7McpArgs(env = process.env) {
|
|
283
|
+
const cfg = context7McpConfig(env);
|
|
284
|
+
return cfg ? ["--mcp-config", JSON.stringify(cfg)] : [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ../../scripts/virtual-office/code-runner/claude-args.mjs
|
|
288
|
+
var DEFAULT_PERMISSION_MODE = "acceptEdits";
|
|
289
|
+
var VO_SESSION_STATE_TOOL = "mcp__vo-mcp__vo_report_session_state";
|
|
290
|
+
var VO_HEADLESS_PNPM_TOOL = "Bash(pnpm *)";
|
|
291
|
+
var VO_HEADLESS_PNPM_FROM_DIR_TOOL = "Bash(pnpm --dir *)";
|
|
292
|
+
var VO_RESEARCH_TOOLS = ["WebFetch", "WebSearch"];
|
|
293
|
+
var VO_WORKFLOW_TOOLS = ["Workflow"];
|
|
294
|
+
var VO_CONSENSUS_TOOLS = ["mcp__vo-mcp__vo_consensus_judgment", "mcp__vo-mcp__vo_verify_answer"];
|
|
295
|
+
var SAFE_PERMISSION_MODES = /* @__PURE__ */ new Set(["acceptEdits", "plan", "default", "dontAsk", "delegate"]);
|
|
296
|
+
function normalizeClaudePermissionMode(value) {
|
|
297
|
+
const normalized = String(value ?? "").trim() || DEFAULT_PERMISSION_MODE;
|
|
298
|
+
if (!SAFE_PERMISSION_MODES.has(normalized)) {
|
|
299
|
+
throw new Error(`unsafe Claude permission mode "${normalized}"`);
|
|
300
|
+
}
|
|
301
|
+
return normalized;
|
|
302
|
+
}
|
|
303
|
+
function buildClaudeArgs({ permissionMode = DEFAULT_PERMISSION_MODE, maxTurns, model, effort, maxBudgetUsd, researchHarness = false, toolPolicy = "default", env = process.env } = {}) {
|
|
304
|
+
const effectivePermissionMode = normalizeClaudePermissionMode(permissionMode);
|
|
305
|
+
if (!["default", "skill_readonly", "frozen_inputs_only"].includes(toolPolicy)) {
|
|
306
|
+
throw new Error(`unsupported Claude tool policy "${toolPolicy}"`);
|
|
307
|
+
}
|
|
308
|
+
const frozenInputsOnly = toolPolicy === "frozen_inputs_only";
|
|
309
|
+
const skillReadonly = toolPolicy === "skill_readonly";
|
|
310
|
+
const restrictedSkill = frozenInputsOnly || skillReadonly;
|
|
311
|
+
const noWeb = frozenInputsOnly || String(env?.VO_CODE_RUNNER_NO_WEB ?? "").trim() === "1";
|
|
312
|
+
const research = noWeb ? [] : VO_RESEARCH_TOOLS;
|
|
313
|
+
const noWorkflow = noWeb || String(env?.VO_CODE_RUNNER_NO_WORKFLOW ?? "").trim() === "1";
|
|
314
|
+
const workflow = !restrictedSkill && researchHarness === true && !noWorkflow ? VO_WORKFLOW_TOOLS : [];
|
|
315
|
+
const noConsensus = frozenInputsOnly || String(env?.VO_CODE_RUNNER_NO_CONSENSUS ?? "").trim() === "1";
|
|
316
|
+
const consensus = noConsensus ? [] : VO_CONSENSUS_TOOLS;
|
|
317
|
+
const baseTools = restrictedSkill ? [] : effectivePermissionMode === DEFAULT_PERMISSION_MODE ? [VO_SESSION_STATE_TOOL, VO_HEADLESS_PNPM_TOOL, VO_HEADLESS_PNPM_FROM_DIR_TOOL] : [VO_SESSION_STATE_TOOL];
|
|
318
|
+
const allowedTools = [...baseTools, ...consensus, ...research, ...workflow].join(",");
|
|
319
|
+
const args = [
|
|
320
|
+
"-p",
|
|
321
|
+
"--output-format",
|
|
322
|
+
"stream-json",
|
|
323
|
+
"--verbose",
|
|
324
|
+
"--permission-mode",
|
|
325
|
+
effectivePermissionMode,
|
|
326
|
+
"--allowedTools",
|
|
327
|
+
allowedTools
|
|
328
|
+
];
|
|
329
|
+
if (restrictedSkill) {
|
|
330
|
+
const builtInTools = skillReadonly ? research.join(",") : "";
|
|
331
|
+
args.push(
|
|
332
|
+
"--tools",
|
|
333
|
+
builtInTools,
|
|
334
|
+
"--disable-slash-commands",
|
|
335
|
+
"--no-chrome",
|
|
336
|
+
"--no-session-persistence",
|
|
337
|
+
"--permission-prompts",
|
|
338
|
+
"none"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (frozenInputsOnly) {
|
|
342
|
+
args.push("--strict-mcp-config", "--safe-mode");
|
|
343
|
+
}
|
|
344
|
+
if (Number.isInteger(maxTurns) && maxTurns > 0) {
|
|
345
|
+
args.push("--max-turns", String(maxTurns));
|
|
346
|
+
}
|
|
347
|
+
if (model) {
|
|
348
|
+
args.push("--model", String(model));
|
|
349
|
+
}
|
|
350
|
+
if (effort) {
|
|
351
|
+
args.push("--effort", String(effort));
|
|
352
|
+
}
|
|
353
|
+
if (typeof maxBudgetUsd === "number" && maxBudgetUsd > 0) {
|
|
354
|
+
args.push("--max-budget-usd", String(maxBudgetUsd));
|
|
355
|
+
}
|
|
356
|
+
if (!restrictedSkill) {
|
|
357
|
+
args.push(...context7McpArgs(env));
|
|
358
|
+
}
|
|
359
|
+
return args;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ../../scripts/virtual-office/code-runner/terminal-process-cleanup.mjs
|
|
363
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
364
|
+
|
|
365
|
+
// ../../scripts/virtual-office/code-runner/orphan-agent-reaper.mjs
|
|
366
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
367
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
368
|
+
import os from "node:os";
|
|
369
|
+
import path2 from "node:path";
|
|
370
|
+
|
|
371
|
+
// ../../scripts/virtual-office/code-runner/agent-token-usage.mjs
|
|
372
|
+
var MAX_TOKEN_COUNT = 1e9;
|
|
373
|
+
var MAX_COST_USD = 1e4;
|
|
374
|
+
var NO_AGENT_SPAWNED_ECONOMICS = Object.freeze({ cost_usd: 0, cost_basis: "no_agent_spawned" });
|
|
375
|
+
function count(value) {
|
|
376
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
377
|
+
return Math.min(MAX_TOKEN_COUNT, Math.round(value));
|
|
378
|
+
}
|
|
379
|
+
function money(value) {
|
|
380
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
381
|
+
return Math.min(MAX_COST_USD, value);
|
|
382
|
+
}
|
|
383
|
+
function extractTokenUsage(evt) {
|
|
384
|
+
const models = extractModelUsage(evt);
|
|
385
|
+
if (models) {
|
|
386
|
+
const out2 = models.reduce((sum, row) => ({
|
|
387
|
+
input_tokens: Math.min(MAX_TOKEN_COUNT, sum.input_tokens + row.input_tokens),
|
|
388
|
+
output_tokens: Math.min(MAX_TOKEN_COUNT, sum.output_tokens + row.output_tokens),
|
|
389
|
+
cache_creation_tokens: Math.min(MAX_TOKEN_COUNT, sum.cache_creation_tokens + row.cache_creation_tokens),
|
|
390
|
+
cache_read_tokens: Math.min(MAX_TOKEN_COUNT, sum.cache_read_tokens + row.cache_read_tokens)
|
|
391
|
+
}), { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0 });
|
|
392
|
+
return Object.values(out2).some((value) => value > 0) ? out2 : null;
|
|
393
|
+
}
|
|
394
|
+
const u = evt?.usage;
|
|
395
|
+
if (!u || typeof u !== "object") return null;
|
|
396
|
+
const out = {
|
|
397
|
+
input_tokens: count(u.input_tokens) ?? 0,
|
|
398
|
+
output_tokens: count(u.output_tokens) ?? 0,
|
|
399
|
+
cache_creation_tokens: count(u.cache_creation_input_tokens) ?? 0,
|
|
400
|
+
cache_read_tokens: count(u.cache_read_input_tokens) ?? 0
|
|
401
|
+
};
|
|
402
|
+
const total = out.input_tokens + out.output_tokens + out.cache_creation_tokens + out.cache_read_tokens;
|
|
403
|
+
return total > 0 ? out : null;
|
|
404
|
+
}
|
|
405
|
+
var MAX_MODELS = 20;
|
|
406
|
+
function extractModelUsage(evt) {
|
|
407
|
+
const m = evt?.modelUsage;
|
|
408
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) return null;
|
|
409
|
+
const rows = [];
|
|
410
|
+
for (const [model, raw] of Object.entries(m)) {
|
|
411
|
+
if (rows.length >= MAX_MODELS) break;
|
|
412
|
+
if (!model || typeof raw !== "object" || raw === null) continue;
|
|
413
|
+
rows.push({
|
|
414
|
+
model: String(model).slice(0, 120),
|
|
415
|
+
input_tokens: count(raw.inputTokens) ?? 0,
|
|
416
|
+
output_tokens: count(raw.outputTokens) ?? 0,
|
|
417
|
+
cache_read_tokens: count(raw.cacheReadInputTokens) ?? 0,
|
|
418
|
+
cache_creation_tokens: count(raw.cacheCreationInputTokens) ?? 0,
|
|
419
|
+
cost_usd: money(raw.costUSD) ?? 0
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return rows.length > 0 ? rows : null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ../../scripts/virtual-office/code-runner/claude-result-event.mjs
|
|
426
|
+
var CAPPED_RESULT_SUBTYPES = Object.freeze(["error_max_budget_usd", "error_max_turns"]);
|
|
427
|
+
function buildResultEvent(evt) {
|
|
428
|
+
const isError = Boolean(evt.is_error) || evt.subtype === "error_max_turns" || evt.subtype === "error_during_execution";
|
|
429
|
+
return {
|
|
430
|
+
kind: "result",
|
|
431
|
+
isError,
|
|
432
|
+
costUsd: typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : null,
|
|
433
|
+
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
434
|
+
numTurns: typeof evt.num_turns === "number" ? evt.num_turns : null,
|
|
435
|
+
tokenUsage: extractTokenUsage(evt),
|
|
436
|
+
modelUsage: extractModelUsage(evt)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ../../scripts/virtual-office/code-runner/claude-stream-event.mjs
|
|
441
|
+
function extractText(content) {
|
|
442
|
+
if (typeof content === "string") return content.trim();
|
|
443
|
+
if (!Array.isArray(content)) return "";
|
|
444
|
+
return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
|
|
445
|
+
}
|
|
446
|
+
function parseClaudeStreamEvent(line) {
|
|
447
|
+
const trimmed = String(line || "").trim();
|
|
448
|
+
if (!trimmed) return null;
|
|
449
|
+
let event;
|
|
450
|
+
try {
|
|
451
|
+
event = JSON.parse(trimmed);
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
if (!event || typeof event !== "object") return null;
|
|
456
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
457
|
+
const text = extractText(event.message.content);
|
|
458
|
+
const tokenUsage = extractTokenUsage({ usage: event.message.usage });
|
|
459
|
+
return text || tokenUsage ? { kind: "progress", text, ...tokenUsage ? { tokenUsage } : {} } : null;
|
|
460
|
+
}
|
|
461
|
+
return event.type === "result" ? buildResultEvent(event) : null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ../../scripts/virtual-office/code-runner/agent-auth-tier.mjs
|
|
465
|
+
var AUTH_TIER_SUBSCRIPTION = "subscription";
|
|
466
|
+
var AUTH_TIER_API_KEY = "api_key";
|
|
467
|
+
var AUTH_TIER_LOCAL = "local";
|
|
468
|
+
var AUTH_TIER_UNKNOWN = "unknown";
|
|
469
|
+
var AUTH_TIERS = Object.freeze([
|
|
470
|
+
AUTH_TIER_SUBSCRIPTION,
|
|
471
|
+
AUTH_TIER_API_KEY,
|
|
472
|
+
AUTH_TIER_LOCAL,
|
|
473
|
+
AUTH_TIER_UNKNOWN
|
|
474
|
+
]);
|
|
475
|
+
var COST_BASIS_TO_TIER = Object.freeze({
|
|
476
|
+
subscription_api_equivalent: AUTH_TIER_SUBSCRIPTION,
|
|
477
|
+
vendor_billed: AUTH_TIER_API_KEY,
|
|
478
|
+
local_zero: AUTH_TIER_LOCAL
|
|
479
|
+
});
|
|
480
|
+
function authTierFromCostBasis(costBasis) {
|
|
481
|
+
return COST_BASIS_TO_TIER[String(costBasis ?? "")] ?? AUTH_TIER_UNKNOWN;
|
|
482
|
+
}
|
|
483
|
+
function safeAuthTier(compute) {
|
|
484
|
+
try {
|
|
485
|
+
return normalizeAuthTier(compute());
|
|
486
|
+
} catch {
|
|
487
|
+
return AUTH_TIER_UNKNOWN;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function normalizeAuthTier(value) {
|
|
491
|
+
return AUTH_TIERS.includes(value) ? value : AUTH_TIER_UNKNOWN;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ../../scripts/virtual-office/code-runner/cli-version-floor.mjs
|
|
495
|
+
var MIN_CLAUDE_CLI_VERSION = "2.1.218";
|
|
496
|
+
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).";
|
|
497
|
+
function parseCliVersion(output) {
|
|
498
|
+
const match = /\b(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?\b/.exec(String(output ?? ""));
|
|
499
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
|
|
500
|
+
}
|
|
501
|
+
function compareSemver(a, b) {
|
|
502
|
+
const pa = a.split(".").map(Number);
|
|
503
|
+
const pb = b.split(".").map(Number);
|
|
504
|
+
for (let i = 0; i < 3; i += 1) {
|
|
505
|
+
if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
506
|
+
}
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
function checkCliVersionFloor(versionOutput, { floor = MIN_CLAUDE_CLI_VERSION } = {}) {
|
|
510
|
+
const version = parseCliVersion(versionOutput);
|
|
511
|
+
if (!version) {
|
|
512
|
+
const seen = String(versionOutput ?? "").trim().slice(0, 120) || "<empty>";
|
|
513
|
+
return {
|
|
514
|
+
ok: false,
|
|
515
|
+
version: null,
|
|
516
|
+
floor,
|
|
517
|
+
message: `could not parse a semver from \`claude --version\` output ("${seen}") \u2014 cannot prove the CLI meets the ${floor} security floor. ${SECURITY_RATIONALE}`
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (compareSemver(version, floor) < 0) {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
version,
|
|
524
|
+
floor,
|
|
525
|
+
message: `claude CLI ${version} is BELOW the minimum security floor ${floor}. ` + SECURITY_RATIONALE
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
version,
|
|
531
|
+
floor,
|
|
532
|
+
message: `claude CLI ${version} meets the minimum security floor ${floor}`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function applyCliVersionFloor({ versionOutput, env = process.env, log = console.error } = {}) {
|
|
536
|
+
const check = checkCliVersionFloor(versionOutput);
|
|
537
|
+
if (check.ok) return { refused: false, check, message: check.message };
|
|
538
|
+
const allowUnsafe = String(env?.VO_CLI_FLOOR_ALLOW_UNSAFE ?? "") === "1";
|
|
539
|
+
const message = `[cli-version-floor] ${allowUnsafe ? "WARNING (unsafe emergency override)" : "REFUSING"}: ` + check.message;
|
|
540
|
+
try {
|
|
541
|
+
log(message);
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
return { refused: !allowUnsafe, check, message };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ../../scripts/virtual-office/code-runner/claude-auth-check.mjs
|
|
548
|
+
var FIRST_VERSION_TIMEOUT_MS = 4500;
|
|
549
|
+
var RETRY_VERSION_TIMEOUT_MS = 2e3;
|
|
550
|
+
function errorCode(error) {
|
|
551
|
+
return String(error?.code || "").toUpperCase();
|
|
552
|
+
}
|
|
553
|
+
function isTimeout(probe) {
|
|
554
|
+
return errorCode(probe?.error) === "ETIMEDOUT" || String(probe?.signal || "").toUpperCase() === "SIGTERM";
|
|
555
|
+
}
|
|
556
|
+
function notFound(probe) {
|
|
557
|
+
return errorCode(probe?.error) === "ENOENT";
|
|
558
|
+
}
|
|
559
|
+
function resolveClaudeAuthTier({
|
|
560
|
+
env = process.env,
|
|
561
|
+
loggedIn = null,
|
|
562
|
+
getStoredKey = getAnthropicKey
|
|
563
|
+
} = {}) {
|
|
564
|
+
return safeAuthTier(() => {
|
|
565
|
+
const spawnEnv = withAnthropicKey(env, { getKey: getStoredKey, probeLogin: () => loggedIn });
|
|
566
|
+
const tier = authTierFromCostBasis(claudeCostBasis(spawnEnv));
|
|
567
|
+
if (tier === AUTH_TIER_SUBSCRIPTION && loggedIn !== true) return AUTH_TIER_UNKNOWN;
|
|
568
|
+
return tier;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async function checkClaudeAuth({
|
|
572
|
+
spawnVersion = spawnClaudeSync,
|
|
573
|
+
probeLogin = probeClaudeLoginState,
|
|
574
|
+
getStoredKey = getAnthropicKey,
|
|
575
|
+
env = process.env
|
|
576
|
+
} = {}) {
|
|
577
|
+
try {
|
|
578
|
+
let probe = spawnVersion(["--version"], {
|
|
579
|
+
timeout: FIRST_VERSION_TIMEOUT_MS,
|
|
580
|
+
encoding: "utf8",
|
|
581
|
+
env
|
|
582
|
+
});
|
|
583
|
+
let retriedAfterTimeout = false;
|
|
584
|
+
if (isTimeout(probe)) {
|
|
585
|
+
retriedAfterTimeout = true;
|
|
586
|
+
probe = spawnVersion(["--version"], {
|
|
587
|
+
timeout: RETRY_VERSION_TIMEOUT_MS,
|
|
588
|
+
encoding: "utf8",
|
|
589
|
+
env
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (probe.error) {
|
|
593
|
+
if (notFound(probe)) {
|
|
594
|
+
return {
|
|
595
|
+
installed: false,
|
|
596
|
+
authenticated: false,
|
|
597
|
+
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."
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
installed: true,
|
|
602
|
+
authenticated: false,
|
|
603
|
+
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}`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (probe.status !== 0) {
|
|
607
|
+
return { installed: true, authenticated: false, message: "claude binary exists but --version failed (auth unclear)" };
|
|
608
|
+
}
|
|
609
|
+
const floorGate = applyCliVersionFloor({ versionOutput: probe.stdout, env });
|
|
610
|
+
if (floorGate.refused) {
|
|
611
|
+
return { installed: true, authenticated: false, message: floorGate.message };
|
|
612
|
+
}
|
|
613
|
+
const loggedIn = retriedAfterTimeout ? null : probeLogin();
|
|
614
|
+
if (loggedIn === false) {
|
|
615
|
+
return {
|
|
616
|
+
installed: true,
|
|
617
|
+
authenticated: false,
|
|
618
|
+
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."
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
installed: true,
|
|
623
|
+
authenticated: true,
|
|
624
|
+
// Dispatch-time billing signal, carried on the same probe that already
|
|
625
|
+
// paid for the login read. Never sent for a non-authenticated result:
|
|
626
|
+
// there is no tier without a working credential.
|
|
627
|
+
authTier: resolveClaudeAuthTier({ env, loggedIn, getStoredKey }),
|
|
628
|
+
message: loggedIn === true ? "claude CLI installed and logged in (claude auth status)" : "claude binary found (login state unknown \u2014 auth check is best-effort)"
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
return {
|
|
632
|
+
installed: false,
|
|
633
|
+
authenticated: false,
|
|
634
|
+
message: `checkAuth probe failed: ${error.message}`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ../../scripts/virtual-office/code-runner/claude-runner.mjs
|
|
640
|
+
function parseStreamEvent(line) {
|
|
641
|
+
return parseClaudeStreamEvent(line);
|
|
642
|
+
}
|
|
643
|
+
var ClaudeRunner = class {
|
|
644
|
+
get enforcesBudgetCap() {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
get binary() {
|
|
648
|
+
return "claude";
|
|
649
|
+
}
|
|
650
|
+
buildArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy } = {}) {
|
|
651
|
+
return buildClaudeArgs({ permissionMode, maxTurns, model, effort, maxBudgetUsd, researchHarness, toolPolicy });
|
|
652
|
+
}
|
|
653
|
+
parseEvent(line) {
|
|
654
|
+
return parseStreamEvent(line);
|
|
655
|
+
}
|
|
656
|
+
getSpawnOptions() {
|
|
657
|
+
return { shell: false, windowsHide: true };
|
|
658
|
+
}
|
|
659
|
+
prepareSpawn({ bin, args, spawnOptions, env = process.env } = {}) {
|
|
660
|
+
if (process.platform !== "win32") return { bin, args, spawnOptions: spawnOptions ?? this.getSpawnOptions() };
|
|
661
|
+
return buildWindowsClaudeLaunch({ bin, args, env });
|
|
662
|
+
}
|
|
663
|
+
applyAuthEnv(env = process.env) {
|
|
664
|
+
return withAnthropicKey(env);
|
|
665
|
+
}
|
|
666
|
+
costBasis(env = process.env) {
|
|
667
|
+
return claudeCostBasis(env);
|
|
668
|
+
}
|
|
669
|
+
describeAuth(env = process.env) {
|
|
670
|
+
return describeAnthropicAuthSource(env);
|
|
671
|
+
}
|
|
672
|
+
async checkAuth() {
|
|
673
|
+
return checkClaudeAuth();
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
var claudeRunner = new ClaudeRunner();
|
|
677
|
+
|
|
678
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
679
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
680
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
681
|
+
import { win32 } from "node:path";
|
|
682
|
+
|
|
683
|
+
// ../../scripts/virtual-office/code-runner/agent-key-store.mjs
|
|
684
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
685
|
+
var require3 = createRequire2(import.meta.url);
|
|
686
|
+
var KEY_SERVICE2 = "algosuite-vo";
|
|
687
|
+
var PROVIDER_ENV = {
|
|
688
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
689
|
+
openai: ["OPENAI_API_KEY", "CODEX_API_KEY"],
|
|
690
|
+
cursor: ["CURSOR_API_KEY"],
|
|
691
|
+
meta: ["MODEL_API_KEY"],
|
|
692
|
+
// Generic OpenAI-compatible runner (bring-your-own model + endpoint): its key
|
|
693
|
+
// is a dedicated var so it never collides with a real OpenAI/Codex key.
|
|
694
|
+
"oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"],
|
|
695
|
+
// Sovereign local inference (Ollama / LM Studio). The key is OPTIONAL — most
|
|
696
|
+
// local servers need none — and exists for locally secured endpoints only.
|
|
697
|
+
local: ["VO_CODE_RUNNER_LOCAL_API_KEY"],
|
|
698
|
+
// AlgoHQ cloud consensus (ADR-002 moat plane) entitlement token. Not a model
|
|
699
|
+
// key: it authorises the runner's `vo_consensus_judgment` / `vo_verify_answer`
|
|
700
|
+
// tools against the moat. On an npm-installed runner the local consensus
|
|
701
|
+
// engine is never present (it is a workspace-only package), so WITHOUT this
|
|
702
|
+
// token every consensus call answers `unimplemented /
|
|
703
|
+
// consensus-engine-package-not-installed` (2026-08-16 finding, task fbd8659b).
|
|
704
|
+
moat: ["VO_ENTITLEMENT_TOKEN"]
|
|
705
|
+
};
|
|
706
|
+
var PROVIDER_ALIAS = {
|
|
707
|
+
claude: "anthropic",
|
|
708
|
+
anthropic: "anthropic",
|
|
709
|
+
codex: "openai",
|
|
710
|
+
openai: "openai",
|
|
711
|
+
cursor: "cursor",
|
|
712
|
+
meta: "meta",
|
|
713
|
+
muse: "meta",
|
|
714
|
+
spark: "meta",
|
|
715
|
+
"muse-spark": "meta",
|
|
716
|
+
oai: "oai-compat",
|
|
717
|
+
"oai-compat": "oai-compat",
|
|
718
|
+
local: "local",
|
|
719
|
+
ollama: "local",
|
|
720
|
+
lmstudio: "local",
|
|
721
|
+
moat: "moat",
|
|
722
|
+
entitlement: "moat",
|
|
723
|
+
consensus: "moat"
|
|
724
|
+
};
|
|
725
|
+
function resolveProvider(name) {
|
|
726
|
+
const key = String(name || "").trim().toLowerCase();
|
|
727
|
+
return PROVIDER_ALIAS[key] || null;
|
|
728
|
+
}
|
|
729
|
+
function accountFor(provider) {
|
|
730
|
+
return `${provider}-api-key`;
|
|
731
|
+
}
|
|
732
|
+
var _entryCtor2;
|
|
733
|
+
var _loadTried2 = false;
|
|
734
|
+
function defaultEntryCtor2() {
|
|
735
|
+
if (_loadTried2) return _entryCtor2;
|
|
736
|
+
_loadTried2 = true;
|
|
737
|
+
try {
|
|
738
|
+
_entryCtor2 = require3("@napi-rs/keyring").Entry;
|
|
739
|
+
} catch {
|
|
740
|
+
_entryCtor2 = null;
|
|
741
|
+
}
|
|
742
|
+
return _entryCtor2;
|
|
743
|
+
}
|
|
744
|
+
function getAgentKey(provider, { EntryCtor = defaultEntryCtor2() } = {}) {
|
|
745
|
+
const p = resolveProvider(provider);
|
|
746
|
+
if (!p || !EntryCtor) return null;
|
|
747
|
+
try {
|
|
748
|
+
return new EntryCtor(KEY_SERVICE2, accountFor(p)).getPassword() || null;
|
|
749
|
+
} catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function withAgentKey(provider, baseEnv = {}, { getKey = getAgentKey } = {}) {
|
|
754
|
+
const p = resolveProvider(provider);
|
|
755
|
+
const vars = p && PROVIDER_ENV[p] || [];
|
|
756
|
+
const out = { ...baseEnv };
|
|
757
|
+
if (!p || vars.length === 0) return out;
|
|
758
|
+
if (vars.some((v) => out[v])) return out;
|
|
759
|
+
const key = getKey(p);
|
|
760
|
+
if (!key) return out;
|
|
761
|
+
for (const v of vars) out[v] = key;
|
|
762
|
+
return out;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ../../scripts/virtual-office/code-runner/flat-token-usage.mjs
|
|
766
|
+
var MAX_TOKEN_COUNT2 = 1e9;
|
|
767
|
+
function count2(value) {
|
|
768
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0;
|
|
769
|
+
return Math.min(MAX_TOKEN_COUNT2, Math.round(value));
|
|
770
|
+
}
|
|
771
|
+
function extractFlatTokenUsage(usage) {
|
|
772
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
|
|
773
|
+
const rawInput = count2(usage.input_tokens ?? usage.prompt_tokens);
|
|
774
|
+
const cached = count2(
|
|
775
|
+
usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? usage.cache_read_tokens
|
|
776
|
+
);
|
|
777
|
+
const hasInclusiveCache = usage.cached_input_tokens !== void 0;
|
|
778
|
+
const out = {
|
|
779
|
+
input_tokens: hasInclusiveCache ? Math.max(0, rawInput - cached) : rawInput,
|
|
780
|
+
output_tokens: count2(usage.output_tokens ?? usage.completion_tokens),
|
|
781
|
+
cache_creation_tokens: count2(
|
|
782
|
+
usage.cache_creation_input_tokens ?? usage.cache_creation_tokens
|
|
783
|
+
),
|
|
784
|
+
cache_read_tokens: cached
|
|
785
|
+
};
|
|
786
|
+
return Object.values(out).some((value) => value > 0) ? out : null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ../../scripts/virtual-office/code-runner/error-message.mjs
|
|
790
|
+
var FAILURE_SCHEMA = "vo.runner_failure.v1";
|
|
791
|
+
var FAILURE_DEFINITIONS = {
|
|
792
|
+
model_unsupported_for_account: {
|
|
793
|
+
source: "terminal_event",
|
|
794
|
+
operator_next_action: "Create a new owner-reviewed task with an account-supported Codex model; keep the preserved draft PR for review rather than resuming this task."
|
|
795
|
+
},
|
|
796
|
+
codex_runtime_launch_logon_session: {
|
|
797
|
+
source: "stderr",
|
|
798
|
+
win32_error: 1312,
|
|
799
|
+
operator_next_action: "Repair the Codex Windows logon/session configuration on the serving host, then rerun the runner."
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
function codexFailure(code, fields = {}) {
|
|
803
|
+
const definition = FAILURE_DEFINITIONS[code];
|
|
804
|
+
return {
|
|
805
|
+
schema: FAILURE_SCHEMA,
|
|
806
|
+
agent: "codex",
|
|
807
|
+
source: definition.source,
|
|
808
|
+
code,
|
|
809
|
+
...fields,
|
|
810
|
+
operator_next_action: definition.operator_next_action
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function classifyCodexTerminalFailure(evt = {}) {
|
|
814
|
+
const error = evt && typeof evt.error === "object" && evt.error !== null ? evt.error : null;
|
|
815
|
+
if (!/^The ['"][^'"]+['"] model is not supported when using Codex with a ChatGPT account\.$/u.test(
|
|
816
|
+
typeof error?.message === "string" ? error.message : ""
|
|
817
|
+
)) return null;
|
|
818
|
+
const status = Number(evt.status ?? error.status);
|
|
819
|
+
return codexFailure("model_unsupported_for_account", Number.isInteger(status) ? { provider_status: status } : {});
|
|
820
|
+
}
|
|
821
|
+
function classifyCodexStderr(stderr = "") {
|
|
822
|
+
if (!/(?:^|\n)\s*windows sandbox(?: failed)?: runner error:.*\bCreateProcessAsUserW failed:\s*1312\b/imu.test(String(stderr))) return null;
|
|
823
|
+
return codexFailure("codex_runtime_launch_logon_session", { win32_error: 1312 });
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ../../scripts/virtual-office/code-runner/codex-runner.mjs
|
|
827
|
+
var CODEX_PREFER_LOGIN_ENV = "VO_RUNNER_CODEX_PREFER_LOGIN";
|
|
828
|
+
var LEGACY_PREFER_LOGIN_ENV = "VO_RUNNER_PREFER_LOGIN";
|
|
829
|
+
function isTruthyFlag2(value) {
|
|
830
|
+
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
831
|
+
}
|
|
832
|
+
function resolveCodexBinary({
|
|
833
|
+
env = process.env,
|
|
834
|
+
platform = process.platform,
|
|
835
|
+
exists = existsSync3
|
|
836
|
+
} = {}) {
|
|
837
|
+
if (platform !== "win32") return "codex";
|
|
838
|
+
const appData = String(env.APPDATA || "").trim();
|
|
839
|
+
const userProfile = String(env.USERPROFILE || "").trim();
|
|
840
|
+
const localAppData = String(env.LOCALAPPDATA || "").trim();
|
|
841
|
+
const candidates = [];
|
|
842
|
+
if (appData) {
|
|
843
|
+
candidates.push(win32.join(
|
|
844
|
+
appData,
|
|
845
|
+
"npm",
|
|
846
|
+
"node_modules",
|
|
847
|
+
"@openai",
|
|
848
|
+
"codex",
|
|
849
|
+
"node_modules",
|
|
850
|
+
"@openai",
|
|
851
|
+
"codex-win32-x64",
|
|
852
|
+
"vendor",
|
|
853
|
+
"x86_64-pc-windows-msvc",
|
|
854
|
+
"bin",
|
|
855
|
+
"codex.exe"
|
|
856
|
+
));
|
|
857
|
+
}
|
|
858
|
+
if (userProfile) {
|
|
859
|
+
candidates.push(win32.join(userProfile, ".local", "bin", "codex.exe"));
|
|
860
|
+
candidates.push(win32.join(userProfile, ".codex", "bin", "codex.exe"));
|
|
861
|
+
}
|
|
862
|
+
if (localAppData) {
|
|
863
|
+
candidates.push(win32.join(localAppData, "Microsoft", "WindowsApps", "codex.exe"));
|
|
864
|
+
}
|
|
865
|
+
const absolute = candidates.find((candidate) => exists(candidate));
|
|
866
|
+
if (absolute) return absolute;
|
|
867
|
+
return "codex";
|
|
868
|
+
}
|
|
869
|
+
function buildCodexArgs({ model, effort } = {}) {
|
|
870
|
+
const args = ["exec", "--json", "-c", 'approval_policy="never"', "--sandbox", "workspace-write", "--skip-git-repo-check"];
|
|
871
|
+
if (model) {
|
|
872
|
+
args.push("--model", String(model));
|
|
873
|
+
}
|
|
874
|
+
if (effort) {
|
|
875
|
+
args.push("-c", `model_reasoning_effort="${String(effort)}"`);
|
|
876
|
+
}
|
|
877
|
+
args.push("-");
|
|
878
|
+
return args;
|
|
879
|
+
}
|
|
880
|
+
function itemText(item) {
|
|
881
|
+
if (!item) return "";
|
|
882
|
+
if (typeof item.text === "string") return item.text;
|
|
883
|
+
if (typeof item.message === "string") return item.message;
|
|
884
|
+
if (Array.isArray(item.content)) {
|
|
885
|
+
return item.content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
886
|
+
}
|
|
887
|
+
return "";
|
|
888
|
+
}
|
|
889
|
+
function parseCodexVersion(stdout) {
|
|
890
|
+
if (typeof stdout !== "string") return null;
|
|
891
|
+
const match = stdout.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u);
|
|
892
|
+
return match ? match[1] : null;
|
|
893
|
+
}
|
|
894
|
+
function parseCodexEvent(line) {
|
|
895
|
+
const trimmed = String(line || "").trim();
|
|
896
|
+
if (!trimmed) return null;
|
|
897
|
+
let evt;
|
|
898
|
+
try {
|
|
899
|
+
evt = JSON.parse(trimmed);
|
|
900
|
+
} catch {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
if (!evt || typeof evt !== "object") return null;
|
|
904
|
+
const type = evt.type;
|
|
905
|
+
if (type === "item.completed" && evt.item) {
|
|
906
|
+
const it = evt.item.type;
|
|
907
|
+
if (it === "agent_message" || it === "assistant_message") {
|
|
908
|
+
const text = itemText(evt.item).trim();
|
|
909
|
+
return text ? { kind: "progress", text } : null;
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
if (type === "turn.completed") {
|
|
914
|
+
return {
|
|
915
|
+
kind: "result",
|
|
916
|
+
isError: false,
|
|
917
|
+
costUsd: null,
|
|
918
|
+
summary: "completed",
|
|
919
|
+
numTurns: null,
|
|
920
|
+
tokenUsage: extractFlatTokenUsage(evt.usage)
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (type === "turn.failed" || type === "error") {
|
|
924
|
+
const msg = evt.error && (evt.error.message || evt.error) || evt.message || "codex run failed";
|
|
925
|
+
const failure = classifyCodexTerminalFailure(evt);
|
|
926
|
+
return { kind: "result", isError: true, costUsd: null, summary: String(msg), numTurns: null, ...failure ? { failure } : {} };
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
var CodexRunner = class {
|
|
931
|
+
constructor({ spawn: spawn2 = spawnSync6, resolveBinary = resolveCodexBinary, env = process.env } = {}) {
|
|
932
|
+
this.spawn = spawn2;
|
|
933
|
+
this.resolveBinary = resolveBinary;
|
|
934
|
+
this.env = env;
|
|
935
|
+
}
|
|
936
|
+
get binary() {
|
|
937
|
+
return this.resolveBinary();
|
|
938
|
+
}
|
|
939
|
+
buildArgs(opts = {}) {
|
|
940
|
+
return buildCodexArgs(opts);
|
|
941
|
+
}
|
|
942
|
+
parseEvent(line) {
|
|
943
|
+
return parseCodexEvent(line);
|
|
944
|
+
}
|
|
945
|
+
classifyStderr(stderr) {
|
|
946
|
+
return classifyCodexStderr(stderr);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* SECURITY: never `shell: true` — same RCE class as cursor-runner. The old
|
|
950
|
+
* `shell: win32 && !/\.exe$/` fell back to shell mode whenever
|
|
951
|
+
* resolveCodexBinary() could not find one of its hardcoded absolute paths and
|
|
952
|
+
* returned the bare string 'codex'. Node's shell mode joins argv into
|
|
953
|
+
* `cmd /d /s /c` with windowsVerbatimArguments, and buildCodexArgs() puts the
|
|
954
|
+
* control-plane-controlled `model` into argv, so a payload of
|
|
955
|
+
* `{ agent: 'codex', model: 'gpt-5 & <cmd>' }` executed arbitrary code —
|
|
956
|
+
* including on hosts where codex is NOT installed, because cmd runs the first
|
|
957
|
+
* command, it fails, and `&` runs the rest anyway.
|
|
958
|
+
*
|
|
959
|
+
* With shell:false a `.cmd`/`.ps1` shim no longer resolves and the spawn fails
|
|
960
|
+
* closed with ENOENT, matching resolveWindowsClaudeExecutable()'s policy.
|
|
961
|
+
*/
|
|
962
|
+
getSpawnOptions() {
|
|
963
|
+
return {
|
|
964
|
+
shell: false,
|
|
965
|
+
windowsHide: true,
|
|
966
|
+
windowsVerbatimArguments: false
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
applyAuthEnv(env = process.env) {
|
|
970
|
+
if (isTruthyFlag2(env[CODEX_PREFER_LOGIN_ENV]) || isTruthyFlag2(env[LEGACY_PREFER_LOGIN_ENV])) {
|
|
971
|
+
const out = { ...env };
|
|
972
|
+
delete out.OPENAI_API_KEY;
|
|
973
|
+
delete out.CODEX_API_KEY;
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
976
|
+
return withAgentKey("openai", env);
|
|
977
|
+
}
|
|
978
|
+
costBasis(env = process.env) {
|
|
979
|
+
return String(env.OPENAI_API_KEY || env.CODEX_API_KEY || "").trim() ? "vendor_billed" : "subscription_api_equivalent";
|
|
980
|
+
}
|
|
981
|
+
authTier(env = this.env) {
|
|
982
|
+
return safeAuthTier(() => authTierFromCostBasis(this.costBasis(this.applyAuthEnv(env))));
|
|
983
|
+
}
|
|
984
|
+
async checkAuth() {
|
|
985
|
+
try {
|
|
986
|
+
const bin = this.binary;
|
|
987
|
+
const version = this.spawn(bin, ["--version"], {
|
|
988
|
+
...this.getSpawnOptions({ bin }),
|
|
989
|
+
windowsHide: true,
|
|
990
|
+
timeout: 3e3,
|
|
991
|
+
encoding: "utf8"
|
|
992
|
+
});
|
|
993
|
+
if (version.error) {
|
|
994
|
+
return { installed: false, authenticated: false, message: `codex not found on PATH: ${version.error.message}` };
|
|
995
|
+
}
|
|
996
|
+
if (version.status !== 0) {
|
|
997
|
+
return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
|
|
998
|
+
}
|
|
999
|
+
const cliVersion = parseCodexVersion(version.stdout);
|
|
1000
|
+
const versionField = cliVersion ? { version: cliVersion } : {};
|
|
1001
|
+
const login = this.spawn(bin, ["login", "status"], {
|
|
1002
|
+
...this.getSpawnOptions({ bin }),
|
|
1003
|
+
windowsHide: true,
|
|
1004
|
+
timeout: 5e3,
|
|
1005
|
+
encoding: "utf8"
|
|
1006
|
+
});
|
|
1007
|
+
const output = `${login.stdout || ""}
|
|
1008
|
+
${login.stderr || ""}`.trim();
|
|
1009
|
+
if (login.error || login.status !== 0) {
|
|
1010
|
+
const authEnv = this.applyAuthEnv(this.env);
|
|
1011
|
+
if (authEnv.OPENAI_API_KEY || authEnv.CODEX_API_KEY) {
|
|
1012
|
+
return {
|
|
1013
|
+
installed: true,
|
|
1014
|
+
authenticated: true,
|
|
1015
|
+
...versionField,
|
|
1016
|
+
authTier: this.authTier(),
|
|
1017
|
+
message: "codex API key available (no persisted ChatGPT login)"
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
installed: true,
|
|
1022
|
+
authenticated: false,
|
|
1023
|
+
...versionField,
|
|
1024
|
+
message: output || login.error?.message || "codex is installed but not logged in"
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
installed: true,
|
|
1029
|
+
authenticated: true,
|
|
1030
|
+
...versionField,
|
|
1031
|
+
authTier: this.authTier(),
|
|
1032
|
+
message: output || "codex login status succeeded"
|
|
1033
|
+
};
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
var codexRunner = new CodexRunner();
|
|
1040
|
+
|
|
1041
|
+
// ../../scripts/virtual-office/code-runner/cursor-runner.mjs
|
|
1042
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
1043
|
+
function buildCursorArgs({ model, prompt } = {}) {
|
|
1044
|
+
const args = ["-p", "--output-format", "stream-json", "--force"];
|
|
1045
|
+
if (model) {
|
|
1046
|
+
args.push("--model", String(model));
|
|
1047
|
+
}
|
|
1048
|
+
const p = String(prompt ?? "");
|
|
1049
|
+
if (p.length > 0) {
|
|
1050
|
+
args.push(p);
|
|
1051
|
+
}
|
|
1052
|
+
return args;
|
|
1053
|
+
}
|
|
1054
|
+
function messageText(message) {
|
|
1055
|
+
if (!message) return "";
|
|
1056
|
+
const content = message.content;
|
|
1057
|
+
if (typeof content === "string") return content;
|
|
1058
|
+
if (Array.isArray(content)) {
|
|
1059
|
+
return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
1060
|
+
}
|
|
1061
|
+
return "";
|
|
1062
|
+
}
|
|
1063
|
+
function parseCursorEvent(line) {
|
|
1064
|
+
const trimmed = String(line || "").trim();
|
|
1065
|
+
if (!trimmed) return null;
|
|
1066
|
+
let evt;
|
|
1067
|
+
try {
|
|
1068
|
+
evt = JSON.parse(trimmed);
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
if (!evt || typeof evt !== "object") return null;
|
|
1073
|
+
if (evt.type === "assistant") {
|
|
1074
|
+
const text = messageText(evt.message).trim();
|
|
1075
|
+
return text ? { kind: "progress", text } : null;
|
|
1076
|
+
}
|
|
1077
|
+
if (evt.type === "result") {
|
|
1078
|
+
const isError = Boolean(evt.is_error) || evt.subtype === "error";
|
|
1079
|
+
const tokenUsage = extractFlatTokenUsage(evt.usage);
|
|
1080
|
+
return {
|
|
1081
|
+
kind: "result",
|
|
1082
|
+
isError,
|
|
1083
|
+
costUsd: null,
|
|
1084
|
+
...tokenUsage ? { tokenUsage } : {},
|
|
1085
|
+
summary: typeof evt.result === "string" && evt.result.length > 0 ? evt.result : evt.subtype || (isError ? "error" : "completed"),
|
|
1086
|
+
numTurns: null
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
var CursorRunner = class {
|
|
1092
|
+
get binary() {
|
|
1093
|
+
return "cursor-agent";
|
|
1094
|
+
}
|
|
1095
|
+
buildArgs(opts = {}) {
|
|
1096
|
+
return buildCursorArgs(opts);
|
|
1097
|
+
}
|
|
1098
|
+
parseEvent(line) {
|
|
1099
|
+
return parseCursorEvent(line);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* SECURITY: never `shell: true`. Node's shell mode on Windows joins argv and
|
|
1103
|
+
* hands it to `cmd /d /s /c` with windowsVerbatimArguments, so every cmd
|
|
1104
|
+
* metacharacter (& | > ^) in an argument is interpreted by the shell. This
|
|
1105
|
+
* runner puts two control-plane-controlled strings into argv — `task.model`
|
|
1106
|
+
* and the composed prompt (buildCursorArgs) — so shell mode turned a task
|
|
1107
|
+
* payload into arbitrary host code execution. It fired even without
|
|
1108
|
+
* cursor-agent installed: cmd runs the first command, it fails, and `&` runs
|
|
1109
|
+
* the rest anyway. With shell:false argv goes straight to CreateProcess and
|
|
1110
|
+
* metacharacters are inert.
|
|
1111
|
+
*
|
|
1112
|
+
* Consequence on Windows: a `.cmd`/`.ps1` shim no longer resolves, so the
|
|
1113
|
+
* runner fails closed with ENOENT rather than executing through a shell —
|
|
1114
|
+
* the same policy resolveWindowsClaudeExecutable() enforces for Claude.
|
|
1115
|
+
*/
|
|
1116
|
+
getSpawnOptions() {
|
|
1117
|
+
return {
|
|
1118
|
+
shell: false,
|
|
1119
|
+
windowsHide: true,
|
|
1120
|
+
windowsVerbatimArguments: false
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Fill CURSOR_API_KEY from the OS keychain when not already set, so a BYO
|
|
1125
|
+
* friend who ran `vo-mcp set-key --provider cursor` authenticates without an
|
|
1126
|
+
* env var. Explicit env wins; no key stored → a prior `cursor-agent login`.
|
|
1127
|
+
*/
|
|
1128
|
+
applyAuthEnv(env = process.env) {
|
|
1129
|
+
return withAgentKey("cursor", env);
|
|
1130
|
+
}
|
|
1131
|
+
costBasis(env = process.env) {
|
|
1132
|
+
return env.CURSOR_API_KEY ? "vendor_billed" : "unknown";
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Dispatch-time billing tier. Inherits costBasis()'s deliberate refusal to
|
|
1136
|
+
* guess: a prior interactive `cursor-agent login` has undocumented
|
|
1137
|
+
* subscription semantics, so it reports 'unknown' rather than claiming a
|
|
1138
|
+
* flat-cost seat the runner cannot actually prove. Keychain read only — the
|
|
1139
|
+
* heartbeat never pays for a subprocess to answer this.
|
|
1140
|
+
*/
|
|
1141
|
+
authTier(env = process.env) {
|
|
1142
|
+
return safeAuthTier(() => authTierFromCostBasis(this.costBasis(this.applyAuthEnv(env))));
|
|
1143
|
+
}
|
|
1144
|
+
/** Best-effort: is `cursor-agent` on PATH? Never throws. */
|
|
1145
|
+
async checkAuth() {
|
|
1146
|
+
try {
|
|
1147
|
+
const { status, error } = spawnSync7("cursor-agent", ["--version"], {
|
|
1148
|
+
shell: false,
|
|
1149
|
+
windowsHide: true,
|
|
1150
|
+
timeout: 3e3,
|
|
1151
|
+
stdio: "ignore"
|
|
1152
|
+
});
|
|
1153
|
+
if (error) {
|
|
1154
|
+
return { installed: false, authenticated: false, message: `cursor-agent not found on PATH: ${error.message}` };
|
|
1155
|
+
}
|
|
1156
|
+
if (status !== 0) {
|
|
1157
|
+
return { installed: true, authenticated: false, message: "cursor-agent exists but --version failed (auth unclear)" };
|
|
1158
|
+
}
|
|
1159
|
+
if (process.env.VO_CODE_RUNNER_ALLOW_UNMETERED_CURSOR !== "1") {
|
|
1160
|
+
return {
|
|
1161
|
+
installed: true,
|
|
1162
|
+
authenticated: false,
|
|
1163
|
+
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"
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
installed: true,
|
|
1168
|
+
authenticated: true,
|
|
1169
|
+
authTier: this.authTier(),
|
|
1170
|
+
message: "cursor-agent found (EXPERIMENTAL: headless mode may need a TTY; auth check is best-effort)"
|
|
1171
|
+
};
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
return { installed: false, authenticated: false, message: `checkAuth probe failed: ${err.message}` };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
var cursorRunner = new CursorRunner();
|
|
1178
|
+
|
|
1179
|
+
// ../../scripts/virtual-office/code-runner/ollama-agent-tools.mjs
|
|
1180
|
+
var MAX_READ_BYTES = 64 * 1024;
|
|
1181
|
+
var MAX_WRITE_BYTES = 512 * 1024;
|
|
1182
|
+
var TOOL_DEFS = [
|
|
1183
|
+
{
|
|
1184
|
+
type: "function",
|
|
1185
|
+
function: {
|
|
1186
|
+
name: "read_file",
|
|
1187
|
+
description: "Read a UTF-8 text file inside the working directory. Returns up to 64 KiB.",
|
|
1188
|
+
parameters: {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {
|
|
1191
|
+
path: { type: "string", description: "File path relative to the working directory." }
|
|
1192
|
+
},
|
|
1193
|
+
required: ["path"]
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
{
|
|
1198
|
+
type: "function",
|
|
1199
|
+
function: {
|
|
1200
|
+
name: "list_files",
|
|
1201
|
+
description: "List entries in a directory inside the working directory (files and subdirs).",
|
|
1202
|
+
parameters: {
|
|
1203
|
+
type: "object",
|
|
1204
|
+
properties: {
|
|
1205
|
+
path: { type: "string", description: 'Directory path relative to the working directory. Default ".".' }
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
type: "function",
|
|
1212
|
+
function: {
|
|
1213
|
+
name: "write_file",
|
|
1214
|
+
description: "Create or overwrite a UTF-8 text file inside the working directory. Parent dirs are created.",
|
|
1215
|
+
parameters: {
|
|
1216
|
+
type: "object",
|
|
1217
|
+
properties: {
|
|
1218
|
+
path: { type: "string", description: "File path relative to the working directory." },
|
|
1219
|
+
content: { type: "string", description: "Full new file contents." }
|
|
1220
|
+
},
|
|
1221
|
+
required: ["path", "content"]
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
];
|
|
1226
|
+
var TOOL_NAMES = TOOL_DEFS.map((t) => t.function.name);
|
|
1227
|
+
var READ_ONLY_TOOL_NAMES = Object.freeze(["read_file", "list_files"]);
|
|
1228
|
+
var READ_ONLY_TOOL_DEFS = Object.freeze(
|
|
1229
|
+
TOOL_DEFS.filter((tool) => READ_ONLY_TOOL_NAMES.includes(tool.function.name))
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
// ../../scripts/virtual-office/code-runner/ollama-agent-core.mjs
|
|
1233
|
+
var MAX_TURNS_DEFAULT = 20;
|
|
1234
|
+
var NUM_CTX_DEFAULT = 16384;
|
|
1235
|
+
function parseOllamaAgentEvent(line) {
|
|
1236
|
+
const trimmed = String(line || "").trim();
|
|
1237
|
+
if (!trimmed) return null;
|
|
1238
|
+
let evt;
|
|
1239
|
+
try {
|
|
1240
|
+
evt = JSON.parse(trimmed);
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
if (!evt || typeof evt !== "object") return null;
|
|
1245
|
+
if (evt.type === "progress") {
|
|
1246
|
+
const text = String(evt.text || "").trim();
|
|
1247
|
+
return text ? { kind: "progress", text } : null;
|
|
1248
|
+
}
|
|
1249
|
+
if (evt.type === "tool") {
|
|
1250
|
+
const via = evt.recovered ? " (recovered from text)" : "";
|
|
1251
|
+
const label = `${evt.ok === false ? "tool failed" : "tool"}: ${evt.name}${evt.path ? ` ${evt.path}` : ""}${via}`;
|
|
1252
|
+
return { kind: "progress", text: label };
|
|
1253
|
+
}
|
|
1254
|
+
if (evt.type === "result") {
|
|
1255
|
+
const usage = evt.usage || null;
|
|
1256
|
+
const tokenUsage = usage ? { inputTokens: usage.inputTokens ?? null, outputTokens: usage.outputTokens ?? null, totalTokens: usage.totalTokens ?? null } : void 0;
|
|
1257
|
+
return {
|
|
1258
|
+
kind: "result",
|
|
1259
|
+
isError: Boolean(evt.isError),
|
|
1260
|
+
costUsd: 0,
|
|
1261
|
+
// sovereign local inference is free — no meter.
|
|
1262
|
+
summary: String(evt.summary || (evt.isError ? "local run failed" : "completed")),
|
|
1263
|
+
numTurns: Number.isInteger(evt.numTurns) ? evt.numTurns : null,
|
|
1264
|
+
...tokenUsage ? { tokenUsage } : {},
|
|
1265
|
+
// Pass the sovereign receipt through to the daemon. Omitted entirely when
|
|
1266
|
+
// absent so the event shape is unchanged for every other transport.
|
|
1267
|
+
...evt.receipt ? { receipt: evt.receipt } : {}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ../../scripts/virtual-office/code-runner/ollama-native-transport.mjs
|
|
1274
|
+
import { fileURLToPath } from "node:url";
|
|
1275
|
+
import { dirname, join } from "node:path";
|
|
1276
|
+
var DEFAULT_LOCAL_TRANSPORT = "codex";
|
|
1277
|
+
var LOCAL_NATIVE_PROFILES = Object.freeze(["coding", "verification"]);
|
|
1278
|
+
var DEFAULT_LOCAL_NATIVE_PROFILE = "coding";
|
|
1279
|
+
function resolveLocalNativeProfile(env = process.env) {
|
|
1280
|
+
const profile = String(env.VO_CODE_RUNNER_LOCAL_PROFILE || "").trim().toLowerCase() || DEFAULT_LOCAL_NATIVE_PROFILE;
|
|
1281
|
+
if (!LOCAL_NATIVE_PROFILES.includes(profile)) {
|
|
1282
|
+
throw new Error(`local-model runner (native): unknown profile "${profile}" (coding|verification).`);
|
|
1283
|
+
}
|
|
1284
|
+
return profile;
|
|
1285
|
+
}
|
|
1286
|
+
function resolveLocalTransport(env = process.env) {
|
|
1287
|
+
return String(env.VO_CODE_RUNNER_LOCAL_TRANSPORT || "").trim().toLowerCase() === "native" ? "native" : DEFAULT_LOCAL_TRANSPORT;
|
|
1288
|
+
}
|
|
1289
|
+
function ollamaAgentScriptPath() {
|
|
1290
|
+
return join(dirname(fileURLToPath(import.meta.url)), "ollama-agent.mjs");
|
|
1291
|
+
}
|
|
1292
|
+
function posIntOr(raw, fallback) {
|
|
1293
|
+
const n = Number(String(raw ?? "").trim());
|
|
1294
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
1295
|
+
}
|
|
1296
|
+
function buildOllamaAgentArgs({ model, numCtx, maxTurns, profile = DEFAULT_LOCAL_NATIVE_PROFILE } = {}) {
|
|
1297
|
+
return [
|
|
1298
|
+
ollamaAgentScriptPath(),
|
|
1299
|
+
"--model",
|
|
1300
|
+
String(model),
|
|
1301
|
+
"--profile",
|
|
1302
|
+
String(profile),
|
|
1303
|
+
"--num-ctx",
|
|
1304
|
+
String(posIntOr(numCtx, NUM_CTX_DEFAULT)),
|
|
1305
|
+
"--max-turns",
|
|
1306
|
+
String(posIntOr(maxTurns, MAX_TURNS_DEFAULT))
|
|
1307
|
+
];
|
|
1308
|
+
}
|
|
1309
|
+
function buildLocalNativeArgs(opts = {}, env = process.env) {
|
|
1310
|
+
const provider = resolveLocalProvider(env);
|
|
1311
|
+
if (provider !== "ollama") {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`local-model runner (native): the native tool-loop executor speaks Ollama's /api/chat; provider "${provider}" is not supported on native transport. Set VO_CODE_RUNNER_LOCAL_PROVIDER=ollama, or use VO_CODE_RUNNER_LOCAL_TRANSPORT=codex for LM Studio.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
const model = resolveLocalModel(env);
|
|
1317
|
+
if (!model) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
"local-model runner (native): set VO_CODE_RUNNER_LOCAL_MODEL to a coding model your Ollama server already has (e.g. qwen2.5-coder:7b). Refusing to run with no explicit model (fail-closed)."
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (!isValidLocalModel(model)) {
|
|
1323
|
+
throw new Error(`local-model runner (native): "${model}" is not a valid local model id.`);
|
|
1324
|
+
}
|
|
1325
|
+
const baseUrl = resolveLocalBaseUrl(env);
|
|
1326
|
+
if (baseUrl && !isLoopbackBaseUrl(baseUrl)) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
"local-model runner (native): 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."
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return buildOllamaAgentArgs({
|
|
1332
|
+
model,
|
|
1333
|
+
profile: resolveLocalNativeProfile(env),
|
|
1334
|
+
numCtx: env.VO_CODE_RUNNER_LOCAL_NUM_CTX,
|
|
1335
|
+
maxTurns: env.VO_CODE_RUNNER_LOCAL_MAX_TURNS
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// ../../scripts/virtual-office/code-runner/local-model-runner.mjs
|
|
1340
|
+
var LOCAL_API_KEY_ENV = "VO_CODE_RUNNER_LOCAL_API_KEY";
|
|
1341
|
+
var FORBIDDEN_LOCAL_CHILD_CREDENTIALS = /* @__PURE__ */ new Set([
|
|
1342
|
+
"ANTHROPIC_API_KEY",
|
|
1343
|
+
"AWS_ACCESS_KEY_ID",
|
|
1344
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1345
|
+
"AWS_SESSION_TOKEN",
|
|
1346
|
+
"FIREBASE_TOKEN",
|
|
1347
|
+
"GH_TOKEN",
|
|
1348
|
+
"GITHUB_TOKEN",
|
|
1349
|
+
"GOOGLE_API_KEY",
|
|
1350
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1351
|
+
"OPENAI_API_KEY"
|
|
1352
|
+
]);
|
|
1353
|
+
var LOCAL_PROVIDERS = ["ollama", "lmstudio"];
|
|
1354
|
+
var DEFAULT_LOCAL_PROVIDER = "ollama";
|
|
1355
|
+
var LOCAL_PROBE_URLS = {
|
|
1356
|
+
ollama: "http://127.0.0.1:11434/api/version",
|
|
1357
|
+
lmstudio: "http://127.0.0.1:1234/v1/models"
|
|
1358
|
+
};
|
|
1359
|
+
function resolveLocalProvider(env = process.env) {
|
|
1360
|
+
return String(env.VO_CODE_RUNNER_LOCAL_PROVIDER || "").trim().toLowerCase() || DEFAULT_LOCAL_PROVIDER;
|
|
1361
|
+
}
|
|
1362
|
+
var remoteDesiredLocalModel = "";
|
|
1363
|
+
function resolveLocalModel(env = process.env) {
|
|
1364
|
+
return String(env.VO_CODE_RUNNER_LOCAL_MODEL || "").trim() || remoteDesiredLocalModel;
|
|
1365
|
+
}
|
|
1366
|
+
function resolveLocalBaseUrl(env = process.env) {
|
|
1367
|
+
return String(env.VO_CODE_RUNNER_LOCAL_BASE_URL || "").trim();
|
|
1368
|
+
}
|
|
1369
|
+
var LOCAL_MODEL_RE = new RegExp("^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$");
|
|
1370
|
+
function isValidLocalModel(model) {
|
|
1371
|
+
return LOCAL_MODEL_RE.test(String(model || ""));
|
|
1372
|
+
}
|
|
1373
|
+
function isLoopbackBaseUrl(url) {
|
|
1374
|
+
const raw = String(url || "").trim();
|
|
1375
|
+
if (!/^https?:\/\/[^\s"'`\\]+$/.test(raw)) return false;
|
|
1376
|
+
let parsed;
|
|
1377
|
+
try {
|
|
1378
|
+
parsed = new URL(raw);
|
|
1379
|
+
} catch {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
const host = parsed.hostname.toLowerCase();
|
|
1383
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1384
|
+
}
|
|
1385
|
+
function buildLocalArgs(opts = {}, env = process.env) {
|
|
1386
|
+
const provider = resolveLocalProvider(env);
|
|
1387
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`local-model runner: unknown VO_CODE_RUNNER_LOCAL_PROVIDER "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")}).`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
const model = resolveLocalModel(env);
|
|
1393
|
+
if (!model) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
"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)."
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (!isValidLocalModel(model)) {
|
|
1399
|
+
throw new Error(`local-model runner: "${model}" is not a valid local model id.`);
|
|
1400
|
+
}
|
|
1401
|
+
const baseUrl = resolveLocalBaseUrl(env);
|
|
1402
|
+
if (baseUrl && !isLoopbackBaseUrl(baseUrl)) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
"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."
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
const args = [
|
|
1408
|
+
"exec",
|
|
1409
|
+
"--json",
|
|
1410
|
+
"-c",
|
|
1411
|
+
'approval_policy="never"',
|
|
1412
|
+
"--sandbox",
|
|
1413
|
+
"workspace-write",
|
|
1414
|
+
"--skip-git-repo-check",
|
|
1415
|
+
"--oss",
|
|
1416
|
+
"--local-provider",
|
|
1417
|
+
provider,
|
|
1418
|
+
"--model",
|
|
1419
|
+
model
|
|
1420
|
+
];
|
|
1421
|
+
if (opts.effort) {
|
|
1422
|
+
args.push("-c", `model_reasoning_effort="${String(opts.effort)}"`);
|
|
1423
|
+
}
|
|
1424
|
+
args.push("-");
|
|
1425
|
+
return args;
|
|
1426
|
+
}
|
|
1427
|
+
function applyLocalAuthEnv(baseEnv = process.env, configEnv = process.env) {
|
|
1428
|
+
const out = withAgentKey("local", baseEnv);
|
|
1429
|
+
for (const key of Object.keys(out)) {
|
|
1430
|
+
if (FORBIDDEN_LOCAL_CHILD_CREDENTIALS.has(key.toUpperCase())) delete out[key];
|
|
1431
|
+
}
|
|
1432
|
+
const baseUrl = resolveLocalBaseUrl(configEnv);
|
|
1433
|
+
if (baseUrl && isLoopbackBaseUrl(baseUrl) && resolveLocalProvider(configEnv) === "ollama" && !String(out.OLLAMA_HOST || "").trim()) {
|
|
1434
|
+
out.OLLAMA_HOST = baseUrl.replace(/\/+$/, "");
|
|
1435
|
+
}
|
|
1436
|
+
return out;
|
|
1437
|
+
}
|
|
1438
|
+
var LocalModelRunner = class {
|
|
1439
|
+
constructor({
|
|
1440
|
+
spawn: spawn2 = null,
|
|
1441
|
+
resolveBinary = resolveCodexBinary,
|
|
1442
|
+
env = process.env,
|
|
1443
|
+
fetchImpl = globalThis.fetch
|
|
1444
|
+
} = {}) {
|
|
1445
|
+
this.spawn = spawn2;
|
|
1446
|
+
this.resolveBinary = resolveBinary;
|
|
1447
|
+
this.env = env;
|
|
1448
|
+
this.fetchImpl = fetchImpl;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Transport binary. codex transport → the codex CLI. native transport →
|
|
1452
|
+
* this daemon's own node (process.execPath), which runs ollama-agent.mjs; no
|
|
1453
|
+
* external CLI is involved on the native path.
|
|
1454
|
+
*/
|
|
1455
|
+
get binary() {
|
|
1456
|
+
return resolveLocalTransport(this.env) === "native" ? process.execPath : this.resolveBinary();
|
|
1457
|
+
}
|
|
1458
|
+
buildArgs(opts = {}) {
|
|
1459
|
+
return resolveLocalTransport(this.env) === "native" ? buildLocalNativeArgs(opts, this.env) : buildLocalArgs(opts, this.env);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* native transport → parse the executor's own JSONL contract. codex transport
|
|
1463
|
+
* → codex JSONL maps identically, except a sovereign local model has no vendor
|
|
1464
|
+
* bill: codex omits total_cost_usd for OSS runs, so turn that known fact into a
|
|
1465
|
+
* measured zero at the producer. (The native parser already stamps costUsd:0.)
|
|
1466
|
+
*/
|
|
1467
|
+
parseEvent(line) {
|
|
1468
|
+
if (resolveLocalTransport(this.env) === "native") return parseOllamaAgentEvent(line);
|
|
1469
|
+
const event = parseCodexEvent(line);
|
|
1470
|
+
return event?.kind === "result" ? { ...event, costUsd: 0 } : event;
|
|
1471
|
+
}
|
|
1472
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. The model id is
|
|
1473
|
+
// control-plane-influenced and validated, but shell:false is the hard floor.
|
|
1474
|
+
getSpawnOptions() {
|
|
1475
|
+
return {
|
|
1476
|
+
shell: false,
|
|
1477
|
+
windowsHide: true,
|
|
1478
|
+
windowsVerbatimArguments: false
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
applyAuthEnv(env = process.env) {
|
|
1482
|
+
return applyLocalAuthEnv(env, this.env);
|
|
1483
|
+
}
|
|
1484
|
+
costBasis() {
|
|
1485
|
+
return "local_zero";
|
|
1486
|
+
}
|
|
1487
|
+
/** Dispatch-time billing tier: local inference is never billed by a vendor. */
|
|
1488
|
+
authTier() {
|
|
1489
|
+
return safeAuthTier(() => authTierFromCostBasis(this.costBasis()));
|
|
1490
|
+
}
|
|
1491
|
+
describeAuth(env = process.env) {
|
|
1492
|
+
const authEnv = this.applyAuthEnv(env);
|
|
1493
|
+
const provider = resolveLocalProvider(this.env);
|
|
1494
|
+
const model = resolveLocalModel(this.env) || "<unset>";
|
|
1495
|
+
const hasKey = Boolean(String(authEnv[LOCAL_API_KEY_ENV] || "").trim());
|
|
1496
|
+
return `local provider=${provider} model=${model} key=${hasKey ? "set" : "none (optional)"}`;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Best-effort: codex transport present AND the local inference endpoint
|
|
1500
|
+
* answers. Never throws, never spends tokens; the endpoint probe is bounded
|
|
1501
|
+
* to 1.5s so a stopped Ollama can't hang availability checks.
|
|
1502
|
+
*/
|
|
1503
|
+
async checkAuth() {
|
|
1504
|
+
const provider = resolveLocalProvider(this.env);
|
|
1505
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
1506
|
+
return {
|
|
1507
|
+
installed: false,
|
|
1508
|
+
authenticated: false,
|
|
1509
|
+
message: `unknown local provider "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")})`
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
const model = resolveLocalModel(this.env);
|
|
1513
|
+
const override = resolveLocalBaseUrl(this.env);
|
|
1514
|
+
if (override && !isLoopbackBaseUrl(override)) {
|
|
1515
|
+
return {
|
|
1516
|
+
installed: true,
|
|
1517
|
+
authenticated: false,
|
|
1518
|
+
message: "VO_CODE_RUNNER_LOCAL_BASE_URL is not loopback \u2014 refused (fail-closed)"
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const probeUrl = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/version` : LOCAL_PROBE_URLS[provider];
|
|
1522
|
+
let endpointUp = false;
|
|
1523
|
+
let probeNote = "";
|
|
1524
|
+
try {
|
|
1525
|
+
const res = await this.fetchImpl(probeUrl, { signal: AbortSignal.timeout(1500) });
|
|
1526
|
+
endpointUp = Boolean(res?.ok);
|
|
1527
|
+
if (!endpointUp) probeNote = `endpoint ${probeUrl} answered HTTP ${res?.status}`;
|
|
1528
|
+
} catch {
|
|
1529
|
+
probeNote = `no local inference server answering at ${probeUrl}`;
|
|
1530
|
+
}
|
|
1531
|
+
if (!endpointUp) {
|
|
1532
|
+
return {
|
|
1533
|
+
installed: true,
|
|
1534
|
+
authenticated: false,
|
|
1535
|
+
message: `${probeNote} \u2014 start ${provider === "ollama" ? "Ollama" : "LM Studio"} first`
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (!model) {
|
|
1539
|
+
return {
|
|
1540
|
+
installed: true,
|
|
1541
|
+
authenticated: false,
|
|
1542
|
+
message: `${provider} is running but VO_CODE_RUNNER_LOCAL_MODEL is not set`
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
return {
|
|
1546
|
+
installed: true,
|
|
1547
|
+
authenticated: true,
|
|
1548
|
+
authTier: this.authTier(),
|
|
1549
|
+
message: `${provider} reachable; model "${model}" configured (local-only, no cloud spend)`
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
var localModelRunner = new LocalModelRunner();
|
|
1554
|
+
|
|
1555
|
+
// ../../scripts/virtual-office/code-runner/meta-runner.mjs
|
|
1556
|
+
var META_API_KEY_ENV = "MODEL_API_KEY";
|
|
1557
|
+
var META_API_KEY_ALIAS = "META_API";
|
|
1558
|
+
function applyMetaAuthEnv(baseEnv = process.env) {
|
|
1559
|
+
const out = withAgentKey("meta", baseEnv);
|
|
1560
|
+
if (!String(out[META_API_KEY_ENV] || "").trim() && String(out[META_API_KEY_ALIAS] || "").trim()) {
|
|
1561
|
+
out[META_API_KEY_ENV] = out[META_API_KEY_ALIAS];
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
function buildMetaArgs(opts = {}) {
|
|
1566
|
+
void opts;
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
"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."
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
var MetaRunner = class {
|
|
1572
|
+
get binary() {
|
|
1573
|
+
return resolveCodexBinary();
|
|
1574
|
+
}
|
|
1575
|
+
buildArgs(opts = {}) {
|
|
1576
|
+
return buildMetaArgs(opts);
|
|
1577
|
+
}
|
|
1578
|
+
parseEvent(line) {
|
|
1579
|
+
return parseCodexEvent(line);
|
|
1580
|
+
}
|
|
1581
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. Inert today (buildArgs
|
|
1582
|
+
// throws) but this goes hot the moment the transport is enabled.
|
|
1583
|
+
getSpawnOptions() {
|
|
1584
|
+
return {
|
|
1585
|
+
shell: false,
|
|
1586
|
+
windowsHide: true,
|
|
1587
|
+
windowsVerbatimArguments: false
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
applyAuthEnv(env = process.env) {
|
|
1591
|
+
return applyMetaAuthEnv(env);
|
|
1592
|
+
}
|
|
1593
|
+
describeAuth(env = process.env) {
|
|
1594
|
+
const authEnv = applyMetaAuthEnv(env);
|
|
1595
|
+
const hasKey = Boolean(String(authEnv[META_API_KEY_ENV] || "").trim());
|
|
1596
|
+
return `meta muse-spark key=${hasKey ? "set" : "MISSING"} transport=codex`;
|
|
1597
|
+
}
|
|
1598
|
+
async checkAuth() {
|
|
1599
|
+
return {
|
|
1600
|
+
installed: false,
|
|
1601
|
+
authenticated: false,
|
|
1602
|
+
message: "Muse Spark coding is disabled; sanitized Model Firewall review only"
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
var metaRunner = new MetaRunner();
|
|
1607
|
+
|
|
1608
|
+
// ../../scripts/virtual-office/code-runner/openai-compatible-runner.mjs
|
|
1609
|
+
var OAI_API_KEY_ENV = "VO_CODE_RUNNER_OAI_API_KEY";
|
|
1610
|
+
function resolveOaiBaseUrl(env = process.env) {
|
|
1611
|
+
return String(env.VO_CODE_RUNNER_OAI_BASE_URL || "").trim();
|
|
1612
|
+
}
|
|
1613
|
+
var OpenAICompatibleRunner = class {
|
|
1614
|
+
/** Codex is the transport binary. */
|
|
1615
|
+
get binary() {
|
|
1616
|
+
return resolveCodexBinary();
|
|
1617
|
+
}
|
|
1618
|
+
buildArgs(opts = {}) {
|
|
1619
|
+
void opts;
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
"OpenAI-compatible full-repository coding is disabled. The `oai` runner lane is RETIRED (PR #8742) and executes nothing. Set VO_CODE_RUNNER_AGENT to claude, codex, cursor, local, or meta. Use an explicit sanitized task capsule through the AlgoSuite Model Firewall."
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
/** Codex JSONL events map identically → reuse the proven parser. */
|
|
1625
|
+
parseEvent(line) {
|
|
1626
|
+
return parseCodexEvent(line);
|
|
1627
|
+
}
|
|
1628
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. Inert today (buildArgs
|
|
1629
|
+
// throws) but this goes hot the moment the transport is enabled.
|
|
1630
|
+
getSpawnOptions() {
|
|
1631
|
+
return {
|
|
1632
|
+
shell: false,
|
|
1633
|
+
windowsHide: true,
|
|
1634
|
+
windowsVerbatimArguments: false
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
/** Fill the BYO key env var from the OS keychain when not already set. */
|
|
1638
|
+
applyAuthEnv(env = process.env) {
|
|
1639
|
+
return withAgentKey("oai-compat", env);
|
|
1640
|
+
}
|
|
1641
|
+
describeAuth(env = process.env) {
|
|
1642
|
+
const hasKey = Boolean(String(env[OAI_API_KEY_ENV] || "").trim());
|
|
1643
|
+
const baseUrl = resolveOaiBaseUrl(env);
|
|
1644
|
+
return `oai-compat RETIRED endpoint=${baseUrl || "<unset>"} key=${hasKey ? "set" : "MISSING"}`;
|
|
1645
|
+
}
|
|
1646
|
+
/** Always unavailable: the lane is retired, so nothing can authenticate it. */
|
|
1647
|
+
async checkAuth() {
|
|
1648
|
+
return {
|
|
1649
|
+
installed: false,
|
|
1650
|
+
authenticated: false,
|
|
1651
|
+
message: "the `oai` lane is RETIRED (PR #8742); OpenAI-compatible coding is disabled \u2014 sanitized Model Firewall task capsules only"
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
var openaiCompatibleRunner = new OpenAICompatibleRunner();
|
|
1656
|
+
|
|
1657
|
+
// ../../scripts/virtual-office/code-runner/agent-runner-interface.mjs
|
|
1658
|
+
function validateAgentRunner(runner) {
|
|
1659
|
+
if (!runner || typeof runner !== "object") {
|
|
1660
|
+
throw new TypeError("AgentRunner must be an object");
|
|
1661
|
+
}
|
|
1662
|
+
if (typeof runner.binary !== "string" || runner.binary.length === 0) {
|
|
1663
|
+
throw new TypeError("AgentRunner.binary must be a non-empty string");
|
|
1664
|
+
}
|
|
1665
|
+
if (typeof runner.buildArgs !== "function") {
|
|
1666
|
+
throw new TypeError("AgentRunner.buildArgs must be a function");
|
|
1667
|
+
}
|
|
1668
|
+
if (typeof runner.parseEvent !== "function") {
|
|
1669
|
+
throw new TypeError("AgentRunner.parseEvent must be a function");
|
|
1670
|
+
}
|
|
1671
|
+
if (typeof runner.getSpawnOptions !== "function") {
|
|
1672
|
+
throw new TypeError("AgentRunner.getSpawnOptions must be a function");
|
|
1673
|
+
}
|
|
1674
|
+
if (typeof runner.checkAuth !== "function") {
|
|
1675
|
+
throw new TypeError("AgentRunner.checkAuth must be a function");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// ../../scripts/virtual-office/code-runner/resolve-runner.mjs
|
|
1680
|
+
var DEFAULT_AGENT = "claude";
|
|
1681
|
+
var RUNNERS = {
|
|
1682
|
+
claude: claudeRunner,
|
|
1683
|
+
codex: codexRunner,
|
|
1684
|
+
cursor: cursorRunner,
|
|
1685
|
+
// `local` = sovereign local inference (Ollama / LM Studio; Mistral/Llama-class
|
|
1686
|
+
// models) — free tier of the pricing pivot; nothing leaves the user's machine.
|
|
1687
|
+
local: localModelRunner,
|
|
1688
|
+
meta: metaRunner,
|
|
1689
|
+
oai: openaiCompatibleRunner
|
|
1690
|
+
};
|
|
1691
|
+
function listAgents() {
|
|
1692
|
+
return Object.keys(RUNNERS);
|
|
1693
|
+
}
|
|
1694
|
+
function inferAgentFromBin(bin) {
|
|
1695
|
+
const raw = String(bin || "").trim().toLowerCase();
|
|
1696
|
+
if (!raw) return null;
|
|
1697
|
+
const base = raw.replace(/\\/g, "/").split("/").pop() || raw;
|
|
1698
|
+
if (base.includes("codex")) return "codex";
|
|
1699
|
+
if (base.includes("cursor-agent") || base === "cursor" || base.startsWith("cursor.")) return "cursor";
|
|
1700
|
+
if (base.includes("claude")) return "claude";
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
function inferAgentFromEnvBin(env) {
|
|
1704
|
+
for (const bin of [
|
|
1705
|
+
env.VO_CODE_RUNNER_BIN,
|
|
1706
|
+
env.VO_CODE_RUNNER_CLAUDE_BIN
|
|
1707
|
+
]) {
|
|
1708
|
+
const agent2 = inferAgentFromBin(bin);
|
|
1709
|
+
if (agent2) return { agent: agent2, bin };
|
|
1710
|
+
}
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
function resolveRunner(env = process.env, { warn = () => {
|
|
1714
|
+
} } = {}) {
|
|
1715
|
+
const explicitAgent = String(env.VO_CODE_RUNNER_AGENT || env.VO_AGENT || "").trim();
|
|
1716
|
+
const inferred = explicitAgent ? null : inferAgentFromEnvBin(env);
|
|
1717
|
+
const raw = String(explicitAgent || inferred?.agent || DEFAULT_AGENT).trim().toLowerCase();
|
|
1718
|
+
let agent2 = raw;
|
|
1719
|
+
let fellBack = false;
|
|
1720
|
+
let runner = RUNNERS[agent2];
|
|
1721
|
+
if (inferred && runner) {
|
|
1722
|
+
try {
|
|
1723
|
+
warn(`VO_CODE_RUNNER_AGENT not set; inferred "${agent2}" from runner binary "${inferred.bin}"`);
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (!runner) {
|
|
1728
|
+
try {
|
|
1729
|
+
warn(`unknown VO_CODE_RUNNER_AGENT "${raw}"; falling back to "${DEFAULT_AGENT}" (known: ${listAgents().join(", ")})`);
|
|
1730
|
+
} catch {
|
|
1731
|
+
}
|
|
1732
|
+
agent2 = DEFAULT_AGENT;
|
|
1733
|
+
runner = RUNNERS[DEFAULT_AGENT];
|
|
1734
|
+
fellBack = true;
|
|
1735
|
+
}
|
|
1736
|
+
validateAgentRunner(runner);
|
|
1737
|
+
const runnerBin = env.VO_CODE_RUNNER_BIN || (inferred && inferred.agent === agent2 ? inferred.bin : "") || (agent2 === "claude" ? env.VO_CODE_RUNNER_CLAUDE_BIN : "") || runner.binary;
|
|
1738
|
+
return { agent: agent2, runner, runnerBin, fellBack };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// ../../scripts/virtual-office/code-runner/agent-auth-probe-cli.mjs
|
|
1742
|
+
var agent = String(process.argv[2] || "");
|
|
1743
|
+
try {
|
|
1744
|
+
const result = await resolveRunner({ ...process.env, VO_CODE_RUNNER_AGENT: agent }).runner.checkAuth();
|
|
1745
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1746
|
+
`);
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
process.stdout.write(`${JSON.stringify({
|
|
1749
|
+
installed: false,
|
|
1750
|
+
authenticated: false,
|
|
1751
|
+
message: String(error?.message || error)
|
|
1752
|
+
})}
|
|
1753
|
+
`);
|
|
1754
|
+
}
|