@algosuite/vo-mcp 0.2.0-beta.20 → 0.2.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +180 -15
- package/dist/cli.js.map +4 -4
- package/dist/index.js +186 -34
- package/dist/index.js.map +4 -4
- package/dist/runner-cli.js +675 -50
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +32 -32
- package/dist/runner-supervisor.js.map +3 -3
- package/dist/set-key-cli.js +8 -2
- package/dist/set-key-cli.js.map +2 -2
- package/package.json +1 -1
package/dist/runner-cli.js
CHANGED
|
@@ -2241,6 +2241,37 @@ var init_spend_cap_shim = __esm({
|
|
|
2241
2241
|
}
|
|
2242
2242
|
});
|
|
2243
2243
|
|
|
2244
|
+
// ../../scripts/virtual-office/code-runner/installation-token.mjs
|
|
2245
|
+
async function fetchInstallationToken({ req, required = false, readOnly = false, repo = null }) {
|
|
2246
|
+
const fail = (reason) => {
|
|
2247
|
+
if (required) throw new Error(`installation-token required: ${reason}`);
|
|
2248
|
+
return null;
|
|
2249
|
+
};
|
|
2250
|
+
try {
|
|
2251
|
+
const res = await req(
|
|
2252
|
+
"POST",
|
|
2253
|
+
"/api/v1/github/installation-token",
|
|
2254
|
+
readOnly ? { scope: "read", ...repo ? { repo } : {} } : {},
|
|
2255
|
+
readOnly ? { timeoutMs: READ_TOKEN_TIMEOUT_MS } : {}
|
|
2256
|
+
);
|
|
2257
|
+
if (!res.ok) return fail(`HTTP ${res.status}`);
|
|
2258
|
+
const json = await res.json();
|
|
2259
|
+
if (!json || !json.token) return fail("missing token");
|
|
2260
|
+
if (readOnly && json.scope !== "read") return fail("control plane did not confirm a read-only grant");
|
|
2261
|
+
return { token: json.token, expiresAt: json.expires_at || null };
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
if (required) throw err;
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
var READ_TOKEN_TIMEOUT_MS;
|
|
2268
|
+
var init_installation_token = __esm({
|
|
2269
|
+
"../../scripts/virtual-office/code-runner/installation-token.mjs"() {
|
|
2270
|
+
"use strict";
|
|
2271
|
+
READ_TOKEN_TIMEOUT_MS = 15e3;
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2244
2275
|
// src/runner/control-plane-auth-stub.mjs
|
|
2245
2276
|
var control_plane_auth_stub_exports = {};
|
|
2246
2277
|
__export(control_plane_auth_stub_exports, {
|
|
@@ -2477,7 +2508,7 @@ function createControlPlaneClient({
|
|
|
2477
2508
|
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
2478
2509
|
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
2479
2510
|
*/
|
|
2480
|
-
async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
2511
|
+
async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels }) {
|
|
2481
2512
|
const body = { runner_id: runnerId };
|
|
2482
2513
|
if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
|
|
2483
2514
|
if (operatorId) body.operator_id = operatorId;
|
|
@@ -2506,6 +2537,9 @@ function createControlPlaneClient({
|
|
|
2506
2537
|
if (Array.isArray(accountUsage) && accountUsage.length > 0) {
|
|
2507
2538
|
body.account_usage = accountUsage;
|
|
2508
2539
|
}
|
|
2540
|
+
if (Array.isArray(availableLocalModels) && availableLocalModels.length > 0) {
|
|
2541
|
+
body.available_local_models = availableLocalModels;
|
|
2542
|
+
}
|
|
2509
2543
|
const res = await req("POST", "/api/v1/runner/heartbeat", body, {
|
|
2510
2544
|
timeoutMs: heartbeatTimeoutMs
|
|
2511
2545
|
});
|
|
@@ -2561,37 +2595,9 @@ function createControlPlaneClient({
|
|
|
2561
2595
|
const json = await res.json();
|
|
2562
2596
|
return json?.action || null;
|
|
2563
2597
|
},
|
|
2564
|
-
/**
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
* authenticated operator (ctx.operator_id), so the token covers only that
|
|
2568
|
-
* operator's installation.
|
|
2569
|
-
*
|
|
2570
|
-
* Returns { token, expiresAt } on success. In legacy/admin mode it returns
|
|
2571
|
-
* null on a miss so the caller may use ambient `gh`. In scoped-operator mode
|
|
2572
|
-
* callers pass `{ required: true }`, which fails closed instead of letting a
|
|
2573
|
-
* missing/mis-scoped installation fall through to the runner machine's `gh`.
|
|
2574
|
-
*
|
|
2575
|
-
* The minted token is only usable for push + PR if the GitHub App grants
|
|
2576
|
-
* BOTH `Contents: write` (git push) AND `Pull requests: write` (gh pr
|
|
2577
|
-
* create) — see docs/vo/github-app-setup-2026-06-18.md. A token missing
|
|
2578
|
-
* either scope fails at push (→ ambient fallback) or at `gh pr create`.
|
|
2579
|
-
*/
|
|
2580
|
-
async getInstallationToken({ required = false } = {}) {
|
|
2581
|
-
const fail = (reason) => {
|
|
2582
|
-
if (required) throw new Error(`installation-token required: ${reason}`);
|
|
2583
|
-
return null;
|
|
2584
|
-
};
|
|
2585
|
-
try {
|
|
2586
|
-
const res = await req("POST", "/api/v1/github/installation-token", {});
|
|
2587
|
-
if (!res.ok) return fail(`HTTP ${res.status}`);
|
|
2588
|
-
const json = await res.json();
|
|
2589
|
-
if (!json || !json.token) return fail("missing token");
|
|
2590
|
-
return { token: json.token, expiresAt: json.expires_at || null };
|
|
2591
|
-
} catch (err) {
|
|
2592
|
-
if (required) throw err;
|
|
2593
|
-
return null;
|
|
2594
|
-
}
|
|
2598
|
+
/** Mint a GitHub App installation token — see installation-token.mjs. */
|
|
2599
|
+
async getInstallationToken({ required = false, readOnly = false, repo = null } = {}) {
|
|
2600
|
+
return fetchInstallationToken({ req, required, readOnly, repo });
|
|
2595
2601
|
},
|
|
2596
2602
|
/**
|
|
2597
2603
|
* Read the operator's dispatch-mode config (Fast→Ultracode effort setting).
|
|
@@ -2614,6 +2620,7 @@ var cachedFirebaseToken;
|
|
|
2614
2620
|
var init_control_plane_client = __esm({
|
|
2615
2621
|
"../../scripts/virtual-office/code-runner/control-plane-client.mjs"() {
|
|
2616
2622
|
"use strict";
|
|
2623
|
+
init_installation_token();
|
|
2617
2624
|
cachedFirebaseToken = null;
|
|
2618
2625
|
}
|
|
2619
2626
|
});
|
|
@@ -3752,7 +3759,10 @@ var init_agent_key_store = __esm({
|
|
|
3752
3759
|
meta: ["MODEL_API_KEY"],
|
|
3753
3760
|
// Generic OpenAI-compatible runner (bring-your-own model + endpoint): its key
|
|
3754
3761
|
// is a dedicated var so it never collides with a real OpenAI/Codex key.
|
|
3755
|
-
"oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"]
|
|
3762
|
+
"oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"],
|
|
3763
|
+
// Sovereign local inference (Ollama / LM Studio). The key is OPTIONAL — most
|
|
3764
|
+
// local servers need none — and exists for locally secured endpoints only.
|
|
3765
|
+
local: ["VO_CODE_RUNNER_LOCAL_API_KEY"]
|
|
3756
3766
|
};
|
|
3757
3767
|
PROVIDER_ALIAS = {
|
|
3758
3768
|
claude: "anthropic",
|
|
@@ -3765,7 +3775,10 @@ var init_agent_key_store = __esm({
|
|
|
3765
3775
|
spark: "meta",
|
|
3766
3776
|
"muse-spark": "meta",
|
|
3767
3777
|
oai: "oai-compat",
|
|
3768
|
-
"oai-compat": "oai-compat"
|
|
3778
|
+
"oai-compat": "oai-compat",
|
|
3779
|
+
local: "local",
|
|
3780
|
+
ollama: "local",
|
|
3781
|
+
lmstudio: "local"
|
|
3769
3782
|
};
|
|
3770
3783
|
_loadTried2 = false;
|
|
3771
3784
|
}
|
|
@@ -3835,6 +3848,11 @@ function itemText(item) {
|
|
|
3835
3848
|
}
|
|
3836
3849
|
return "";
|
|
3837
3850
|
}
|
|
3851
|
+
function parseCodexVersion(stdout) {
|
|
3852
|
+
if (typeof stdout !== "string") return null;
|
|
3853
|
+
const match = stdout.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u);
|
|
3854
|
+
return match ? match[1] : null;
|
|
3855
|
+
}
|
|
3838
3856
|
function parseCodexEvent(line) {
|
|
3839
3857
|
const trimmed = String(line || "").trim();
|
|
3840
3858
|
if (!trimmed) return null;
|
|
@@ -3929,7 +3947,7 @@ var init_codex_runner = __esm({
|
|
|
3929
3947
|
...this.getSpawnOptions({ bin }),
|
|
3930
3948
|
windowsHide: true,
|
|
3931
3949
|
timeout: 3e3,
|
|
3932
|
-
|
|
3950
|
+
encoding: "utf8"
|
|
3933
3951
|
});
|
|
3934
3952
|
if (version.error) {
|
|
3935
3953
|
return { installed: false, authenticated: false, message: `codex not found on PATH: ${version.error.message}` };
|
|
@@ -3937,6 +3955,8 @@ var init_codex_runner = __esm({
|
|
|
3937
3955
|
if (version.status !== 0) {
|
|
3938
3956
|
return { installed: true, authenticated: false, message: "codex exists but --version failed (auth unclear)" };
|
|
3939
3957
|
}
|
|
3958
|
+
const cliVersion = parseCodexVersion(version.stdout);
|
|
3959
|
+
const versionField = cliVersion ? { version: cliVersion } : {};
|
|
3940
3960
|
const login = this.spawn(bin, ["login", "status"], {
|
|
3941
3961
|
...this.getSpawnOptions({ bin }),
|
|
3942
3962
|
windowsHide: true,
|
|
@@ -3951,18 +3971,21 @@ ${login.stderr || ""}`.trim();
|
|
|
3951
3971
|
return {
|
|
3952
3972
|
installed: true,
|
|
3953
3973
|
authenticated: true,
|
|
3974
|
+
...versionField,
|
|
3954
3975
|
message: "codex API key available (no persisted ChatGPT login)"
|
|
3955
3976
|
};
|
|
3956
3977
|
}
|
|
3957
3978
|
return {
|
|
3958
3979
|
installed: true,
|
|
3959
3980
|
authenticated: false,
|
|
3981
|
+
...versionField,
|
|
3960
3982
|
message: output || login.error?.message || "codex is installed but not logged in"
|
|
3961
3983
|
};
|
|
3962
3984
|
}
|
|
3963
3985
|
return {
|
|
3964
3986
|
installed: true,
|
|
3965
3987
|
authenticated: true,
|
|
3988
|
+
...versionField,
|
|
3966
3989
|
message: output || "codex login status succeeded"
|
|
3967
3990
|
};
|
|
3968
3991
|
} catch (err) {
|
|
@@ -4096,6 +4119,200 @@ var init_cursor_runner = __esm({
|
|
|
4096
4119
|
}
|
|
4097
4120
|
});
|
|
4098
4121
|
|
|
4122
|
+
// ../../scripts/virtual-office/code-runner/local-model-runner.mjs
|
|
4123
|
+
function resolveLocalProvider(env2 = process.env) {
|
|
4124
|
+
return String(env2.VO_CODE_RUNNER_LOCAL_PROVIDER || "").trim().toLowerCase() || DEFAULT_LOCAL_PROVIDER;
|
|
4125
|
+
}
|
|
4126
|
+
function setRemoteDesiredLocalModel(model) {
|
|
4127
|
+
const value = typeof model === "string" ? model.trim() : "";
|
|
4128
|
+
remoteDesiredLocalModel = value && isValidLocalModel(value) ? value : "";
|
|
4129
|
+
}
|
|
4130
|
+
function resolveLocalModel(env2 = process.env) {
|
|
4131
|
+
return String(env2.VO_CODE_RUNNER_LOCAL_MODEL || "").trim() || remoteDesiredLocalModel;
|
|
4132
|
+
}
|
|
4133
|
+
function resolveLocalBaseUrl(env2 = process.env) {
|
|
4134
|
+
return String(env2.VO_CODE_RUNNER_LOCAL_BASE_URL || "").trim();
|
|
4135
|
+
}
|
|
4136
|
+
function isValidLocalModel(model) {
|
|
4137
|
+
return LOCAL_MODEL_RE.test(String(model || ""));
|
|
4138
|
+
}
|
|
4139
|
+
function isLoopbackBaseUrl(url) {
|
|
4140
|
+
const raw = String(url || "").trim();
|
|
4141
|
+
if (!/^https?:\/\/[^\s"'`\\]+$/.test(raw)) return false;
|
|
4142
|
+
let parsed;
|
|
4143
|
+
try {
|
|
4144
|
+
parsed = new URL(raw);
|
|
4145
|
+
} catch {
|
|
4146
|
+
return false;
|
|
4147
|
+
}
|
|
4148
|
+
const host = parsed.hostname.toLowerCase();
|
|
4149
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
4150
|
+
}
|
|
4151
|
+
function buildLocalArgs(opts = {}, env2 = process.env) {
|
|
4152
|
+
const provider = resolveLocalProvider(env2);
|
|
4153
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
4154
|
+
throw new Error(
|
|
4155
|
+
`local-model runner: unknown VO_CODE_RUNNER_LOCAL_PROVIDER "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")}).`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
const model = resolveLocalModel(env2);
|
|
4159
|
+
if (!model) {
|
|
4160
|
+
throw new Error(
|
|
4161
|
+
"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)."
|
|
4162
|
+
);
|
|
4163
|
+
}
|
|
4164
|
+
if (!isValidLocalModel(model)) {
|
|
4165
|
+
throw new Error(`local-model runner: "${model}" is not a valid local model id.`);
|
|
4166
|
+
}
|
|
4167
|
+
const baseUrl = resolveLocalBaseUrl(env2);
|
|
4168
|
+
if (baseUrl && !isLoopbackBaseUrl(baseUrl)) {
|
|
4169
|
+
throw new Error(
|
|
4170
|
+
"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."
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
const args = [
|
|
4174
|
+
"exec",
|
|
4175
|
+
"--json",
|
|
4176
|
+
"-c",
|
|
4177
|
+
'approval_policy="never"',
|
|
4178
|
+
"--sandbox",
|
|
4179
|
+
"workspace-write",
|
|
4180
|
+
"--skip-git-repo-check",
|
|
4181
|
+
"--oss",
|
|
4182
|
+
"--local-provider",
|
|
4183
|
+
provider,
|
|
4184
|
+
"--model",
|
|
4185
|
+
model
|
|
4186
|
+
];
|
|
4187
|
+
if (opts.effort) {
|
|
4188
|
+
args.push("-c", `model_reasoning_effort="${String(opts.effort)}"`);
|
|
4189
|
+
}
|
|
4190
|
+
args.push("-");
|
|
4191
|
+
return args;
|
|
4192
|
+
}
|
|
4193
|
+
function applyLocalAuthEnv(baseEnv = process.env, configEnv = process.env) {
|
|
4194
|
+
const out = withAgentKey("local", baseEnv);
|
|
4195
|
+
const baseUrl = resolveLocalBaseUrl(configEnv);
|
|
4196
|
+
if (baseUrl && isLoopbackBaseUrl(baseUrl) && resolveLocalProvider(configEnv) === "ollama" && !String(out.OLLAMA_HOST || "").trim()) {
|
|
4197
|
+
out.OLLAMA_HOST = baseUrl.replace(/\/+$/, "");
|
|
4198
|
+
}
|
|
4199
|
+
return out;
|
|
4200
|
+
}
|
|
4201
|
+
var LOCAL_API_KEY_ENV, LOCAL_PROVIDERS, DEFAULT_LOCAL_PROVIDER, LOCAL_PROBE_URLS, remoteDesiredLocalModel, LOCAL_MODEL_RE, LocalModelRunner, localModelRunner;
|
|
4202
|
+
var init_local_model_runner = __esm({
|
|
4203
|
+
"../../scripts/virtual-office/code-runner/local-model-runner.mjs"() {
|
|
4204
|
+
"use strict";
|
|
4205
|
+
init_codex_runner();
|
|
4206
|
+
init_agent_key_store();
|
|
4207
|
+
LOCAL_API_KEY_ENV = "VO_CODE_RUNNER_LOCAL_API_KEY";
|
|
4208
|
+
LOCAL_PROVIDERS = ["ollama", "lmstudio"];
|
|
4209
|
+
DEFAULT_LOCAL_PROVIDER = "ollama";
|
|
4210
|
+
LOCAL_PROBE_URLS = {
|
|
4211
|
+
ollama: "http://127.0.0.1:11434/api/version",
|
|
4212
|
+
lmstudio: "http://127.0.0.1:1234/v1/models"
|
|
4213
|
+
};
|
|
4214
|
+
remoteDesiredLocalModel = "";
|
|
4215
|
+
LOCAL_MODEL_RE = new RegExp("^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$");
|
|
4216
|
+
LocalModelRunner = class {
|
|
4217
|
+
constructor({
|
|
4218
|
+
spawn: spawn5 = null,
|
|
4219
|
+
resolveBinary = resolveCodexBinary,
|
|
4220
|
+
env: env2 = process.env,
|
|
4221
|
+
fetchImpl = globalThis.fetch
|
|
4222
|
+
} = {}) {
|
|
4223
|
+
this.spawn = spawn5;
|
|
4224
|
+
this.resolveBinary = resolveBinary;
|
|
4225
|
+
this.env = env2;
|
|
4226
|
+
this.fetchImpl = fetchImpl;
|
|
4227
|
+
}
|
|
4228
|
+
/** Codex is the transport binary; the model/endpoint are the user's. */
|
|
4229
|
+
get binary() {
|
|
4230
|
+
return this.resolveBinary();
|
|
4231
|
+
}
|
|
4232
|
+
buildArgs(opts = {}) {
|
|
4233
|
+
return buildLocalArgs(opts, this.env);
|
|
4234
|
+
}
|
|
4235
|
+
/** Codex JSONL events map identically → reuse the proven parser. */
|
|
4236
|
+
parseEvent(line) {
|
|
4237
|
+
return parseCodexEvent(line);
|
|
4238
|
+
}
|
|
4239
|
+
// SECURITY: never shell — see no-shell-spawn.test.mjs. The model id is
|
|
4240
|
+
// control-plane-influenced and validated, but shell:false is the hard floor.
|
|
4241
|
+
getSpawnOptions() {
|
|
4242
|
+
return {
|
|
4243
|
+
shell: false,
|
|
4244
|
+
windowsHide: true,
|
|
4245
|
+
windowsVerbatimArguments: false
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
applyAuthEnv(env2 = process.env) {
|
|
4249
|
+
return applyLocalAuthEnv(env2, this.env);
|
|
4250
|
+
}
|
|
4251
|
+
describeAuth(env2 = process.env) {
|
|
4252
|
+
const authEnv = this.applyAuthEnv(env2);
|
|
4253
|
+
const provider = resolveLocalProvider(this.env);
|
|
4254
|
+
const model = resolveLocalModel(this.env) || "<unset>";
|
|
4255
|
+
const hasKey = Boolean(String(authEnv[LOCAL_API_KEY_ENV] || "").trim());
|
|
4256
|
+
return `local provider=${provider} model=${model} key=${hasKey ? "set" : "none (optional)"}`;
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* Best-effort: codex transport present AND the local inference endpoint
|
|
4260
|
+
* answers. Never throws, never spends tokens; the endpoint probe is bounded
|
|
4261
|
+
* to 1.5s so a stopped Ollama can't hang availability checks.
|
|
4262
|
+
*/
|
|
4263
|
+
async checkAuth() {
|
|
4264
|
+
const provider = resolveLocalProvider(this.env);
|
|
4265
|
+
if (!LOCAL_PROVIDERS.includes(provider)) {
|
|
4266
|
+
return {
|
|
4267
|
+
installed: false,
|
|
4268
|
+
authenticated: false,
|
|
4269
|
+
message: `unknown local provider "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")})`
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
const model = resolveLocalModel(this.env);
|
|
4273
|
+
const override = resolveLocalBaseUrl(this.env);
|
|
4274
|
+
if (override && !isLoopbackBaseUrl(override)) {
|
|
4275
|
+
return {
|
|
4276
|
+
installed: true,
|
|
4277
|
+
authenticated: false,
|
|
4278
|
+
message: "VO_CODE_RUNNER_LOCAL_BASE_URL is not loopback \u2014 refused (fail-closed)"
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
const probeUrl = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/version` : LOCAL_PROBE_URLS[provider];
|
|
4282
|
+
let endpointUp = false;
|
|
4283
|
+
let probeNote = "";
|
|
4284
|
+
try {
|
|
4285
|
+
const res = await this.fetchImpl(probeUrl, { signal: AbortSignal.timeout(1500) });
|
|
4286
|
+
endpointUp = Boolean(res?.ok);
|
|
4287
|
+
if (!endpointUp) probeNote = `endpoint ${probeUrl} answered HTTP ${res?.status}`;
|
|
4288
|
+
} catch {
|
|
4289
|
+
probeNote = `no local inference server answering at ${probeUrl}`;
|
|
4290
|
+
}
|
|
4291
|
+
if (!endpointUp) {
|
|
4292
|
+
return {
|
|
4293
|
+
installed: true,
|
|
4294
|
+
authenticated: false,
|
|
4295
|
+
message: `${probeNote} \u2014 start ${provider === "ollama" ? "Ollama" : "LM Studio"} first`
|
|
4296
|
+
};
|
|
4297
|
+
}
|
|
4298
|
+
if (!model) {
|
|
4299
|
+
return {
|
|
4300
|
+
installed: true,
|
|
4301
|
+
authenticated: false,
|
|
4302
|
+
message: `${provider} is running but VO_CODE_RUNNER_LOCAL_MODEL is not set`
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
return {
|
|
4306
|
+
installed: true,
|
|
4307
|
+
authenticated: true,
|
|
4308
|
+
message: `${provider} reachable; model "${model}" configured (local-only, no cloud spend)`
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
localModelRunner = new LocalModelRunner();
|
|
4313
|
+
}
|
|
4314
|
+
});
|
|
4315
|
+
|
|
4099
4316
|
// ../../scripts/virtual-office/code-runner/meta-runner.mjs
|
|
4100
4317
|
function applyMetaAuthEnv(baseEnv = process.env) {
|
|
4101
4318
|
const out = withAgentKey("meta", baseEnv);
|
|
@@ -4311,6 +4528,7 @@ var init_resolve_runner = __esm({
|
|
|
4311
4528
|
init_claude_runner();
|
|
4312
4529
|
init_codex_runner();
|
|
4313
4530
|
init_cursor_runner();
|
|
4531
|
+
init_local_model_runner();
|
|
4314
4532
|
init_meta_runner();
|
|
4315
4533
|
init_openai_compatible_runner();
|
|
4316
4534
|
init_agent_runner_interface();
|
|
@@ -4319,6 +4537,9 @@ var init_resolve_runner = __esm({
|
|
|
4319
4537
|
claude: claudeRunner,
|
|
4320
4538
|
codex: codexRunner,
|
|
4321
4539
|
cursor: cursorRunner,
|
|
4540
|
+
// `local` = sovereign local inference (Ollama / LM Studio; Mistral/Llama-class
|
|
4541
|
+
// models) — free tier of the pricing pivot; nothing leaves the user's machine.
|
|
4542
|
+
local: localModelRunner,
|
|
4322
4543
|
meta: metaRunner,
|
|
4323
4544
|
oai: openaiCompatibleRunner
|
|
4324
4545
|
};
|
|
@@ -5983,6 +6204,12 @@ function makeLoopTicks({
|
|
|
5983
6204
|
applyCapacity: () => false,
|
|
5984
6205
|
heartbeatFields: () => ({})
|
|
5985
6206
|
},
|
|
6207
|
+
// Track 1: served-local-model reporting + desired-model echo application
|
|
6208
|
+
// (local-model-remote-config.mjs). No-op defaults keep old callers working.
|
|
6209
|
+
localModelController = {
|
|
6210
|
+
applyRemoteConfig: () => false,
|
|
6211
|
+
heartbeatFields: () => ({})
|
|
6212
|
+
},
|
|
5986
6213
|
// Cached agent-availability provider (agent-availability.mjs); returns null
|
|
5987
6214
|
// until the first probe completes — the heartbeat simply omits the field.
|
|
5988
6215
|
getAgentAvailability = () => null,
|
|
@@ -6020,7 +6247,10 @@ function makeLoopTicks({
|
|
|
6020
6247
|
} catch (error) {
|
|
6021
6248
|
request = Promise.reject(error);
|
|
6022
6249
|
}
|
|
6023
|
-
request.then((response) =>
|
|
6250
|
+
request.then((response) => {
|
|
6251
|
+
capacityController.applyCapacity(response?.capacity, nextPayload.operatorId);
|
|
6252
|
+
localModelController.applyRemoteConfig(response?.local_model, nextPayload.operatorId);
|
|
6253
|
+
}).catch((e) => log3(`heartbeat failed: ${e.message}`)).finally(() => {
|
|
6024
6254
|
for (const done of waiters) done();
|
|
6025
6255
|
if (state.pending) {
|
|
6026
6256
|
const pending = state.pending;
|
|
@@ -6062,6 +6292,7 @@ function makeLoopTicks({
|
|
|
6062
6292
|
const supervisorVersion = String(env2.VO_RUNNER_SUPERVISOR_VERSION || "").trim().slice(0, 40);
|
|
6063
6293
|
const supervisorCapabilities = String(env2.VO_RUNNER_SUPERVISOR_CAPABILITIES || "").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
|
|
6064
6294
|
const capacityFields = capacityController.heartbeatFields();
|
|
6295
|
+
const localModelFields = localModelController.heartbeatFields();
|
|
6065
6296
|
const baseHeartbeat = {
|
|
6066
6297
|
runnerId: cfg.runnerId,
|
|
6067
6298
|
...runnerInstanceId ? { runnerInstanceId } : {},
|
|
@@ -6078,7 +6309,8 @@ function makeLoopTicks({
|
|
|
6078
6309
|
uptimeSec: Math.floor(process.uptime()),
|
|
6079
6310
|
activeTasks: getActive(),
|
|
6080
6311
|
maxConcurrency: cfg.maxConcurrency,
|
|
6081
|
-
...capacityFields
|
|
6312
|
+
...capacityFields,
|
|
6313
|
+
...localModelFields
|
|
6082
6314
|
};
|
|
6083
6315
|
const operatorIds = servedOperators.length > 0 ? servedOperators : [void 0];
|
|
6084
6316
|
for (const operatorId of operatorIds) {
|
|
@@ -6204,7 +6436,12 @@ async function collectAgentAvailability({
|
|
|
6204
6436
|
const probes = agents.map(async (agent) => {
|
|
6205
6437
|
try {
|
|
6206
6438
|
const r = await runnerFor(agent).checkAuth();
|
|
6207
|
-
return {
|
|
6439
|
+
return {
|
|
6440
|
+
agent,
|
|
6441
|
+
installed: Boolean(r?.installed),
|
|
6442
|
+
authenticated: Boolean(r?.authenticated),
|
|
6443
|
+
...typeof r?.version === "string" && r.version ? { version: r.version } : {}
|
|
6444
|
+
};
|
|
6208
6445
|
} catch {
|
|
6209
6446
|
return { agent, installed: false, authenticated: false };
|
|
6210
6447
|
}
|
|
@@ -6245,6 +6482,127 @@ var init_agent_availability = __esm({
|
|
|
6245
6482
|
}
|
|
6246
6483
|
});
|
|
6247
6484
|
|
|
6485
|
+
// ../../scripts/virtual-office/code-runner/local-model-remote-config.mjs
|
|
6486
|
+
async function listServedLocalModels({
|
|
6487
|
+
env: env2 = process.env,
|
|
6488
|
+
fetchImpl = globalThis.fetch,
|
|
6489
|
+
timeoutMs = 1500
|
|
6490
|
+
} = {}) {
|
|
6491
|
+
const provider = resolveLocalProvider(env2);
|
|
6492
|
+
if (!LOCAL_PROVIDERS.includes(provider)) return null;
|
|
6493
|
+
const override = resolveLocalBaseUrl(env2);
|
|
6494
|
+
if (override && !isLoopbackBaseUrl(override)) return null;
|
|
6495
|
+
const url = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/tags` : SERVED_MODELS_PROBE_URLS[provider];
|
|
6496
|
+
try {
|
|
6497
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
6498
|
+
if (!res?.ok) return null;
|
|
6499
|
+
const json = await res.json();
|
|
6500
|
+
const names = provider === "ollama" ? Array.isArray(json?.models) ? json.models.map((m) => m?.name) : null : Array.isArray(json?.data) ? json.data.map((m) => m?.id) : null;
|
|
6501
|
+
if (!names) return null;
|
|
6502
|
+
return names.filter((name) => typeof name === "string" && isValidLocalModel(name)).slice(0, MAX_REPORTED_MODELS);
|
|
6503
|
+
} catch {
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
function createLocalModelRemoteController({
|
|
6508
|
+
env: env2 = process.env,
|
|
6509
|
+
fetchImpl = globalThis.fetch,
|
|
6510
|
+
log: log3 = () => {
|
|
6511
|
+
},
|
|
6512
|
+
probeIntervalMs = 6e4,
|
|
6513
|
+
now = () => Date.now(),
|
|
6514
|
+
apply = setRemoteDesiredLocalModel,
|
|
6515
|
+
listModels = listServedLocalModels
|
|
6516
|
+
} = {}) {
|
|
6517
|
+
let served = null;
|
|
6518
|
+
const byOperator = /* @__PURE__ */ new Map();
|
|
6519
|
+
let probing = false;
|
|
6520
|
+
let lastProbeAt = 0;
|
|
6521
|
+
let warnedUnserved = "";
|
|
6522
|
+
let warnedConflict = "";
|
|
6523
|
+
function desiredModel() {
|
|
6524
|
+
const models = [...new Set(
|
|
6525
|
+
[...byOperator.values()].map((s) => s.desired).filter((m) => typeof m === "string" && m)
|
|
6526
|
+
)];
|
|
6527
|
+
if (models.length > 1) {
|
|
6528
|
+
const key = models.slice().sort().join(",");
|
|
6529
|
+
if (warnedConflict !== key) {
|
|
6530
|
+
warnedConflict = key;
|
|
6531
|
+
log3(`local-model remote config: conflicting desired models across served operators (${key}) \u2014 applying none`);
|
|
6532
|
+
}
|
|
6533
|
+
return null;
|
|
6534
|
+
}
|
|
6535
|
+
return models[0] ?? null;
|
|
6536
|
+
}
|
|
6537
|
+
function syncEffective() {
|
|
6538
|
+
const desired = desiredModel();
|
|
6539
|
+
const effective = desired && Array.isArray(served) && served.includes(desired) ? desired : "";
|
|
6540
|
+
apply(effective);
|
|
6541
|
+
if (desired && !effective && warnedUnserved !== desired) {
|
|
6542
|
+
warnedUnserved = desired;
|
|
6543
|
+
log3(
|
|
6544
|
+
`local-model remote config: "${desired}" is not served by the local inference server \u2014 ignored (never auto-pull; pull it locally first)`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
return effective;
|
|
6548
|
+
}
|
|
6549
|
+
function refreshServedModels() {
|
|
6550
|
+
if (probing || now() - lastProbeAt < probeIntervalMs) return;
|
|
6551
|
+
probing = true;
|
|
6552
|
+
lastProbeAt = now();
|
|
6553
|
+
Promise.resolve(listModels({ env: env2, fetchImpl })).then((models) => {
|
|
6554
|
+
if (Array.isArray(models)) served = models;
|
|
6555
|
+
syncEffective();
|
|
6556
|
+
}).catch(() => {
|
|
6557
|
+
}).finally(() => {
|
|
6558
|
+
probing = false;
|
|
6559
|
+
});
|
|
6560
|
+
}
|
|
6561
|
+
return {
|
|
6562
|
+
/** Heartbeat payload extras; also kicks the throttled background probe. */
|
|
6563
|
+
heartbeatFields() {
|
|
6564
|
+
refreshServedModels();
|
|
6565
|
+
return Array.isArray(served) && served.length > 0 ? { availableLocalModels: served } : {};
|
|
6566
|
+
},
|
|
6567
|
+
/**
|
|
6568
|
+
* Apply one operator's heartbeat-response echo
|
|
6569
|
+
* { schema_version, desired_local_model, revision }. Revision ordering is
|
|
6570
|
+
* PER OPERATOR — counters are independent across operator scopes.
|
|
6571
|
+
*/
|
|
6572
|
+
applyRemoteConfig(echo, operatorId = "") {
|
|
6573
|
+
const scope = String(operatorId || "");
|
|
6574
|
+
const previous = byOperator.get(scope);
|
|
6575
|
+
if (!echo || echo.schema_version !== 1 || !Number.isInteger(echo.revision) || previous && echo.revision < previous.revision) {
|
|
6576
|
+
return false;
|
|
6577
|
+
}
|
|
6578
|
+
const model = echo.desired_local_model;
|
|
6579
|
+
if (model !== null && (typeof model !== "string" || !isValidLocalModel(model))) {
|
|
6580
|
+
return false;
|
|
6581
|
+
}
|
|
6582
|
+
byOperator.set(scope, { revision: echo.revision, desired: model });
|
|
6583
|
+
syncEffective();
|
|
6584
|
+
return true;
|
|
6585
|
+
},
|
|
6586
|
+
snapshot: () => ({
|
|
6587
|
+
served,
|
|
6588
|
+
desired: desiredModel(),
|
|
6589
|
+
operators: Object.fromEntries(byOperator)
|
|
6590
|
+
})
|
|
6591
|
+
};
|
|
6592
|
+
}
|
|
6593
|
+
var SERVED_MODELS_PROBE_URLS, MAX_REPORTED_MODELS;
|
|
6594
|
+
var init_local_model_remote_config = __esm({
|
|
6595
|
+
"../../scripts/virtual-office/code-runner/local-model-remote-config.mjs"() {
|
|
6596
|
+
"use strict";
|
|
6597
|
+
init_local_model_runner();
|
|
6598
|
+
SERVED_MODELS_PROBE_URLS = {
|
|
6599
|
+
ollama: "http://127.0.0.1:11434/api/tags",
|
|
6600
|
+
lmstudio: "http://127.0.0.1:1234/v1/models"
|
|
6601
|
+
};
|
|
6602
|
+
MAX_REPORTED_MODELS = 50;
|
|
6603
|
+
}
|
|
6604
|
+
});
|
|
6605
|
+
|
|
6248
6606
|
// ../../scripts/virtual-office/code-runner/account-usage.mjs
|
|
6249
6607
|
import { spawn as spawn4 } from "node:child_process";
|
|
6250
6608
|
import fs6 from "node:fs";
|
|
@@ -6854,7 +7212,8 @@ function makeWatchRunner({
|
|
|
6854
7212
|
maxFixAttempts,
|
|
6855
7213
|
repairEvidence = readCiRepairEvidence,
|
|
6856
7214
|
stateFile = DEFAULT_STATE_FILE,
|
|
6857
|
-
|
|
7215
|
+
// Default ON (operator directive 2026-07-24); VO_CODE_RUNNER_ARM_AUTOMERGE=0 opts out.
|
|
7216
|
+
autoMergeEnabled = process.env.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0"
|
|
6858
7217
|
}) {
|
|
6859
7218
|
return () => runWatchCycle({
|
|
6860
7219
|
viewPr,
|
|
@@ -7511,7 +7870,7 @@ var init_model_router = __esm({
|
|
|
7511
7870
|
"use strict";
|
|
7512
7871
|
init_model_registry();
|
|
7513
7872
|
init_meta_model_catalog();
|
|
7514
|
-
TASK_MODEL_AGENTS = ["claude", "codex", "cursor", "meta"];
|
|
7873
|
+
TASK_MODEL_AGENTS = ["claude", "codex", "cursor", "local", "meta"];
|
|
7515
7874
|
DEFAULT_AGENT2 = "claude";
|
|
7516
7875
|
AGENT_TIER_FAMILIES = {
|
|
7517
7876
|
claude: {
|
|
@@ -7533,6 +7892,13 @@ var init_model_router = __esm({
|
|
|
7533
7892
|
mid: null,
|
|
7534
7893
|
best: null
|
|
7535
7894
|
},
|
|
7895
|
+
// Local models are machine-specific pulls (Ollama / LM Studio); the runner's
|
|
7896
|
+
// VO_CODE_RUNNER_LOCAL_MODEL is the source of truth, not the registry.
|
|
7897
|
+
local: {
|
|
7898
|
+
cheap: null,
|
|
7899
|
+
mid: null,
|
|
7900
|
+
best: null
|
|
7901
|
+
},
|
|
7536
7902
|
meta: {
|
|
7537
7903
|
cheap: null,
|
|
7538
7904
|
mid: null,
|
|
@@ -7555,6 +7921,11 @@ var init_model_router = __esm({
|
|
|
7555
7921
|
mid: null,
|
|
7556
7922
|
best: null
|
|
7557
7923
|
},
|
|
7924
|
+
local: {
|
|
7925
|
+
cheap: null,
|
|
7926
|
+
mid: null,
|
|
7927
|
+
best: null
|
|
7928
|
+
},
|
|
7558
7929
|
meta: {
|
|
7559
7930
|
cheap: resolveMetaModelForTier("cheap"),
|
|
7560
7931
|
mid: resolveMetaModelForTier("mid"),
|
|
@@ -7573,6 +7944,16 @@ var init_model_router = __esm({
|
|
|
7573
7944
|
// cursor-runner. Restrict to the shape a model id actually has so a payload
|
|
7574
7945
|
// like `x & powershell -enc ...` is rejected before it reaches a spawn.
|
|
7575
7946
|
cursor: (model) => /^[A-Za-z0-9][A-Za-z0-9._:@\[\]-]{0,79}$/.test(String(model || "")),
|
|
7947
|
+
// SECURITY: the local lane accepts NO remote model pins — return false for
|
|
7948
|
+
// EVERY value so control-plane `task.model` can never influence what runs on
|
|
7949
|
+
// the user's machine. codex `--oss` AUTO-PULLS missing models (live-verified
|
|
7950
|
+
// 2026-07-24: a single --model argument downloaded 397MB unprompted), so an
|
|
7951
|
+
// honored pin like "llama3.1:405b" (231GB, valid id shape) would let any
|
|
7952
|
+
// tenant member remotely fill a BYO runner owner's disk. The machine-local
|
|
7953
|
+
// env (VO_CODE_RUNNER_LOCAL_MODEL — the owner's own choice in the app) is
|
|
7954
|
+
// the ONLY model authority, mirroring the runner-release-target principle
|
|
7955
|
+
// that browser input is never local-execution authority.
|
|
7956
|
+
local: () => false,
|
|
7576
7957
|
meta: (model) => /^muse-spark-[A-Za-z0-9._:@\[\]-]{0,79}$/i.test(String(model || ""))
|
|
7577
7958
|
};
|
|
7578
7959
|
}
|
|
@@ -8461,11 +8842,41 @@ var init_reconnect_backoff = __esm({
|
|
|
8461
8842
|
}
|
|
8462
8843
|
});
|
|
8463
8844
|
|
|
8845
|
+
// ../../scripts/virtual-office/code-runner/redact-tokens.mjs
|
|
8846
|
+
function redactSecrets(text) {
|
|
8847
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
8848
|
+
let out = text;
|
|
8849
|
+
for (const [re, mask] of TOKEN_PATTERNS) out = out.replace(re, mask);
|
|
8850
|
+
return out;
|
|
8851
|
+
}
|
|
8852
|
+
function redactPatch(patch) {
|
|
8853
|
+
if (!patch || typeof patch !== "object") return patch;
|
|
8854
|
+
const out = Array.isArray(patch) ? [...patch] : { ...patch };
|
|
8855
|
+
for (const [k, v] of Object.entries(out)) {
|
|
8856
|
+
if (typeof v === "string") out[k] = redactSecrets(v);
|
|
8857
|
+
else if (v && typeof v === "object") out[k] = redactPatch(v);
|
|
8858
|
+
}
|
|
8859
|
+
return out;
|
|
8860
|
+
}
|
|
8861
|
+
var TOKEN_PATTERNS;
|
|
8862
|
+
var init_redact_tokens = __esm({
|
|
8863
|
+
"../../scripts/virtual-office/code-runner/redact-tokens.mjs"() {
|
|
8864
|
+
"use strict";
|
|
8865
|
+
TOKEN_PATTERNS = [
|
|
8866
|
+
[/\b(?:gh[oprsu]|vocred|npm)_[A-Za-z0-9._-]{10,}\b/gu, "[REDACTED]"],
|
|
8867
|
+
[/\bgithub_pat_[A-Za-z0-9_]{10,}\b/gu, "[REDACTED]"],
|
|
8868
|
+
// Keep the scheme so the line still reads as an auth header, matching
|
|
8869
|
+
// sanitizeMaintenanceDiagnostic's behaviour.
|
|
8870
|
+
[/\bBearer\s+[A-Za-z0-9._~+/-]{10,}=*/giu, "Bearer [REDACTED]"]
|
|
8871
|
+
];
|
|
8872
|
+
}
|
|
8873
|
+
});
|
|
8874
|
+
|
|
8464
8875
|
// ../../scripts/virtual-office/code-runner/task-helpers.mjs
|
|
8465
8876
|
function makeSafeProgress(log3) {
|
|
8466
8877
|
return async (client, id, patch) => {
|
|
8467
8878
|
try {
|
|
8468
|
-
const r = await client.postProgress(id, patch);
|
|
8879
|
+
const r = await client.postProgress(id, redactPatch(patch));
|
|
8469
8880
|
if (r && r.terminal) log3(`task ${id} is terminal server-side; stopping updates`);
|
|
8470
8881
|
return r;
|
|
8471
8882
|
} catch (err) {
|
|
@@ -8491,20 +8902,34 @@ function buildPrBody(task, run, files, { armAutoMerge = false } = {}) {
|
|
|
8491
8902
|
"### Prompt",
|
|
8492
8903
|
"",
|
|
8493
8904
|
"```",
|
|
8494
|
-
String(task.prompt).slice(0, 2e3),
|
|
8905
|
+
redactSecrets(String(task.prompt)).slice(0, 2e3),
|
|
8495
8906
|
"```",
|
|
8496
8907
|
"",
|
|
8497
8908
|
"### Agent summary",
|
|
8498
8909
|
"",
|
|
8499
|
-
String(run.summary || "").slice(0, 2e3),
|
|
8910
|
+
redactSecrets(String(run.summary || "")).slice(0, 2e3),
|
|
8500
8911
|
"",
|
|
8501
8912
|
"---",
|
|
8502
8913
|
armAutoMerge ? "_Opened by the AlgoHQ code-runner daemon. After CI passes, the watcher must obtain a durable consensus receipt and merge the exact verified SHA._" : "_Opened by the AlgoHQ code-runner daemon. This PR awaits the verify-before-act gate / operator review \u2014 it is NOT auto-merged._"
|
|
8503
8914
|
].filter((l) => l !== "").join("\n");
|
|
8504
8915
|
}
|
|
8916
|
+
async function mintRunnerGithubTokens({ client, taskId, log: log3, repo = null, requirePublish = false }) {
|
|
8917
|
+
const publishToken = (await client.getInstallationToken({ required: requirePublish }))?.token ?? null;
|
|
8918
|
+
let agentReadToken = null;
|
|
8919
|
+
let reason = null;
|
|
8920
|
+
try {
|
|
8921
|
+
agentReadToken = (await client.getInstallationToken({ readOnly: true, repo }))?.token ?? null;
|
|
8922
|
+
if (!agentReadToken) reason = "control plane returned no confirmed read-only grant";
|
|
8923
|
+
} catch (err) {
|
|
8924
|
+
reason = err?.message ?? String(err);
|
|
8925
|
+
}
|
|
8926
|
+
if (!agentReadToken) log3(`task ${taskId}: no GitHub read access for the agent (${reason}); it cannot read a private repo`);
|
|
8927
|
+
return { publishToken, agentReadToken };
|
|
8928
|
+
}
|
|
8505
8929
|
var init_task_helpers = __esm({
|
|
8506
8930
|
"../../scripts/virtual-office/code-runner/task-helpers.mjs"() {
|
|
8507
8931
|
"use strict";
|
|
8932
|
+
init_redact_tokens();
|
|
8508
8933
|
}
|
|
8509
8934
|
});
|
|
8510
8935
|
|
|
@@ -8520,8 +8945,12 @@ function safeBaseEnv(env2 = {}) {
|
|
|
8520
8945
|
}
|
|
8521
8946
|
return result;
|
|
8522
8947
|
}
|
|
8523
|
-
function buildAgentProcessEnv(env2, { agent = "agent", runnerId = "vo-runner", taskId = "task" } = {}) {
|
|
8948
|
+
function buildAgentProcessEnv(env2, { agent = "agent", runnerId = "vo-runner", taskId = "task", githubReadToken = null } = {}) {
|
|
8524
8949
|
const base = safeBaseEnv(env2);
|
|
8950
|
+
if (typeof githubReadToken === "string" && githubReadToken) {
|
|
8951
|
+
base.GH_TOKEN = githubReadToken;
|
|
8952
|
+
base.GITHUB_TOKEN = githubReadToken;
|
|
8953
|
+
}
|
|
8525
8954
|
if (String(env2?.AGENT_ID || "").trim()) return { ...base, AGENT_ID: env2.AGENT_ID };
|
|
8526
8955
|
const generated = [
|
|
8527
8956
|
"vo",
|
|
@@ -8569,6 +8998,198 @@ var init_agent_process_env = __esm({
|
|
|
8569
8998
|
}
|
|
8570
8999
|
});
|
|
8571
9000
|
|
|
9001
|
+
// ../../scripts/virtual-office/code-runner/inference-executor.mjs
|
|
9002
|
+
function buildChatBody({ messages, prompt, model, maxTokens, temperature } = {}) {
|
|
9003
|
+
const msgs = Array.isArray(messages) && messages.length > 0 ? messages : [{ role: "user", content: String(prompt ?? "") }];
|
|
9004
|
+
if (msgs.length > MAX_INFERENCE_MESSAGES) {
|
|
9005
|
+
throw new Error(`inference: too many messages (${msgs.length} > ${MAX_INFERENCE_MESSAGES})`);
|
|
9006
|
+
}
|
|
9007
|
+
const totalChars = msgs.reduce((n, m) => n + String(m?.content ?? "").length, 0);
|
|
9008
|
+
if (totalChars > MAX_INFERENCE_PROMPT_CHARS) {
|
|
9009
|
+
throw new Error(`inference: prompt too large (${totalChars} > ${MAX_INFERENCE_PROMPT_CHARS} chars)`);
|
|
9010
|
+
}
|
|
9011
|
+
if (!model || typeof model !== "string") {
|
|
9012
|
+
throw new Error("inference: a model id is required");
|
|
9013
|
+
}
|
|
9014
|
+
const body = {
|
|
9015
|
+
model,
|
|
9016
|
+
messages: msgs.map((m) => ({ role: String(m.role || "user"), content: String(m.content ?? "") })),
|
|
9017
|
+
stream: false,
|
|
9018
|
+
max_tokens: Number.isInteger(maxTokens) && maxTokens > 0 ? maxTokens : DEFAULT_MAX_OUTPUT_TOKENS
|
|
9019
|
+
};
|
|
9020
|
+
if (Number.isFinite(temperature)) body.temperature = temperature;
|
|
9021
|
+
return body;
|
|
9022
|
+
}
|
|
9023
|
+
function resolveInferenceEndpoint({ providerMode, baseUrl } = {}) {
|
|
9024
|
+
const mode = String(providerMode || "local").toLowerCase();
|
|
9025
|
+
if (mode !== "local") {
|
|
9026
|
+
throw new Error(`inference: unsupported provider mode "${mode}" (this slice is local-only)`);
|
|
9027
|
+
}
|
|
9028
|
+
const url = String(baseUrl || DEFAULT_LOCAL_INFERENCE_BASE_URL).replace(/\/+$/, "");
|
|
9029
|
+
if (!isLoopbackBaseUrl(url)) {
|
|
9030
|
+
throw new Error(`inference: local base_url must be loopback (localhost/127.0.0.1/[::1]); got "${url}"`);
|
|
9031
|
+
}
|
|
9032
|
+
return url;
|
|
9033
|
+
}
|
|
9034
|
+
function extractCompletion(json) {
|
|
9035
|
+
const choice = json?.choices?.[0];
|
|
9036
|
+
const text = choice?.message?.content ?? choice?.text ?? "";
|
|
9037
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
9038
|
+
throw new Error("inference: provider returned no completion text");
|
|
9039
|
+
}
|
|
9040
|
+
const usage = json?.usage ?? null;
|
|
9041
|
+
return {
|
|
9042
|
+
text,
|
|
9043
|
+
usage: usage ? {
|
|
9044
|
+
input_tokens: Number(usage.prompt_tokens) || 0,
|
|
9045
|
+
output_tokens: Number(usage.completion_tokens) || 0,
|
|
9046
|
+
total_tokens: Number(usage.total_tokens) || 0
|
|
9047
|
+
} : null,
|
|
9048
|
+
finish_reason: choice?.finish_reason ?? null
|
|
9049
|
+
};
|
|
9050
|
+
}
|
|
9051
|
+
async function runInference({ providerMode = "local", baseUrl, model, prompt, messages, maxTokens, temperature, timeoutMs = 12e4 } = {}, { fetchImpl = globalThis.fetch } = {}) {
|
|
9052
|
+
const endpoint = resolveInferenceEndpoint({ providerMode, baseUrl });
|
|
9053
|
+
const body = buildChatBody({ messages, prompt, model, maxTokens, temperature });
|
|
9054
|
+
const res = await fetchImpl(`${endpoint}/chat/completions`, {
|
|
9055
|
+
method: "POST",
|
|
9056
|
+
headers: { "Content-Type": "application/json" },
|
|
9057
|
+
body: JSON.stringify(body),
|
|
9058
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
9059
|
+
});
|
|
9060
|
+
if (!res?.ok) {
|
|
9061
|
+
const status = res?.status ?? "ERR";
|
|
9062
|
+
let detail = "";
|
|
9063
|
+
try {
|
|
9064
|
+
detail = (await res.text()).slice(0, 300);
|
|
9065
|
+
} catch {
|
|
9066
|
+
}
|
|
9067
|
+
throw new Error(`inference: endpoint returned HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
9068
|
+
}
|
|
9069
|
+
const json = await res.json();
|
|
9070
|
+
const { text, usage, finish_reason } = extractCompletion(json);
|
|
9071
|
+
return { text, usage, finish_reason, model, endpoint };
|
|
9072
|
+
}
|
|
9073
|
+
var DEFAULT_LOCAL_INFERENCE_BASE_URL, MAX_INFERENCE_MESSAGES, MAX_INFERENCE_PROMPT_CHARS, DEFAULT_MAX_OUTPUT_TOKENS;
|
|
9074
|
+
var init_inference_executor = __esm({
|
|
9075
|
+
"../../scripts/virtual-office/code-runner/inference-executor.mjs"() {
|
|
9076
|
+
"use strict";
|
|
9077
|
+
init_local_model_runner();
|
|
9078
|
+
DEFAULT_LOCAL_INFERENCE_BASE_URL = "http://127.0.0.1:11434/v1";
|
|
9079
|
+
MAX_INFERENCE_MESSAGES = 200;
|
|
9080
|
+
MAX_INFERENCE_PROMPT_CHARS = 1e5;
|
|
9081
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 1024;
|
|
9082
|
+
}
|
|
9083
|
+
});
|
|
9084
|
+
|
|
9085
|
+
// ../../scripts/virtual-office/code-runner/inference-task-handler.mjs
|
|
9086
|
+
async function handleInferenceTask(task, { runInferenceImpl = runInference, now = () => Date.now() } = {}) {
|
|
9087
|
+
const started = now();
|
|
9088
|
+
try {
|
|
9089
|
+
const result = await runInferenceImpl({
|
|
9090
|
+
providerMode: "local",
|
|
9091
|
+
baseUrl: task.base_url,
|
|
9092
|
+
model: task.model,
|
|
9093
|
+
prompt: task.prompt,
|
|
9094
|
+
messages: task.messages,
|
|
9095
|
+
maxTokens: task.max_output_tokens,
|
|
9096
|
+
temperature: task.temperature
|
|
9097
|
+
});
|
|
9098
|
+
const text = String(result.text || "").slice(0, MAX_RESULT_CHARS);
|
|
9099
|
+
return {
|
|
9100
|
+
ok: true,
|
|
9101
|
+
text,
|
|
9102
|
+
usage: result.usage ?? null,
|
|
9103
|
+
finish_reason: result.finish_reason ?? null,
|
|
9104
|
+
provider_mode: "local",
|
|
9105
|
+
// Structural $0: no AlgoSuite provider call happened, so nothing to charge.
|
|
9106
|
+
billable: false,
|
|
9107
|
+
duration_ms: now() - started
|
|
9108
|
+
};
|
|
9109
|
+
} catch (err) {
|
|
9110
|
+
return {
|
|
9111
|
+
ok: false,
|
|
9112
|
+
provider_mode: "local",
|
|
9113
|
+
error: err instanceof Error ? err.message : String(err),
|
|
9114
|
+
duration_ms: now() - started
|
|
9115
|
+
};
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
9118
|
+
var MAX_RESULT_CHARS;
|
|
9119
|
+
var init_inference_task_handler = __esm({
|
|
9120
|
+
"../../scripts/virtual-office/code-runner/inference-task-handler.mjs"() {
|
|
9121
|
+
"use strict";
|
|
9122
|
+
init_inference_executor();
|
|
9123
|
+
MAX_RESULT_CHARS = 1e5;
|
|
9124
|
+
}
|
|
9125
|
+
});
|
|
9126
|
+
|
|
9127
|
+
// ../../scripts/virtual-office/code-runner/inference-task-runner.mjs
|
|
9128
|
+
async function processInferenceTask(client, task, cfg, {
|
|
9129
|
+
env: env2 = process.env,
|
|
9130
|
+
safeProgress: safeProgress2,
|
|
9131
|
+
runnerStagePatch: runnerStagePatch2,
|
|
9132
|
+
handle = handleInferenceTask,
|
|
9133
|
+
log: log3 = () => {
|
|
9134
|
+
}
|
|
9135
|
+
} = {}) {
|
|
9136
|
+
const id = task.code_task_id;
|
|
9137
|
+
await safeProgress2(
|
|
9138
|
+
client,
|
|
9139
|
+
id,
|
|
9140
|
+
runnerStagePatch2("starting_agent", `${cfg.runnerId} running local inference for this task`, {
|
|
9141
|
+
status: "running"
|
|
9142
|
+
})
|
|
9143
|
+
);
|
|
9144
|
+
const model = resolveLocalModel(env2);
|
|
9145
|
+
if (!model) {
|
|
9146
|
+
await safeProgress2(client, id, {
|
|
9147
|
+
status: "failed",
|
|
9148
|
+
message: "inference: this runner has no local model configured",
|
|
9149
|
+
result: "This computer is not configured with a local model. Set VO_CODE_RUNNER_LOCAL_MODEL (a model your Ollama / LM Studio already has), then retry."
|
|
9150
|
+
});
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
const out = await handle(
|
|
9154
|
+
{
|
|
9155
|
+
provider_mode: "local",
|
|
9156
|
+
model,
|
|
9157
|
+
prompt: task.prompt,
|
|
9158
|
+
base_url: resolveLocalBaseUrl(env2) || void 0
|
|
9159
|
+
},
|
|
9160
|
+
{ now: () => Date.now() }
|
|
9161
|
+
);
|
|
9162
|
+
if (out.ok) {
|
|
9163
|
+
log3(`inference task ${id} completed on ${model} (${out.usage?.output_tokens ?? "?"} output tokens)`);
|
|
9164
|
+
await safeProgress2(client, id, {
|
|
9165
|
+
status: INFERENCE_SUCCESS_STATUS,
|
|
9166
|
+
message: "inference complete",
|
|
9167
|
+
result: out.text,
|
|
9168
|
+
// $0 by construction — the completion was produced on the user's machine,
|
|
9169
|
+
// AlgoSuite made no provider call, so there is nothing to bill.
|
|
9170
|
+
cost_usd: 0,
|
|
9171
|
+
num_turns: 1
|
|
9172
|
+
});
|
|
9173
|
+
return;
|
|
9174
|
+
}
|
|
9175
|
+
log3(`inference task ${id} failed: ${out.error}`);
|
|
9176
|
+
await safeProgress2(client, id, {
|
|
9177
|
+
status: "failed",
|
|
9178
|
+
message: "inference failed",
|
|
9179
|
+
result: out.error,
|
|
9180
|
+
cost_usd: 0
|
|
9181
|
+
});
|
|
9182
|
+
}
|
|
9183
|
+
var INFERENCE_SUCCESS_STATUS;
|
|
9184
|
+
var init_inference_task_runner = __esm({
|
|
9185
|
+
"../../scripts/virtual-office/code-runner/inference-task-runner.mjs"() {
|
|
9186
|
+
"use strict";
|
|
9187
|
+
init_local_model_runner();
|
|
9188
|
+
init_inference_task_handler();
|
|
9189
|
+
INFERENCE_SUCCESS_STATUS = "no_changes_needed";
|
|
9190
|
+
}
|
|
9191
|
+
});
|
|
9192
|
+
|
|
8572
9193
|
// ../../scripts/virtual-office/code-runner/isolation-audit.mjs
|
|
8573
9194
|
import fs8 from "node:fs";
|
|
8574
9195
|
import fsp9 from "node:fs/promises";
|
|
@@ -9001,7 +9622,8 @@ function loadConfig(env2 = process.env) {
|
|
|
9001
9622
|
watchEnabled: env2.VO_CODE_RUNNER_WATCH !== "0",
|
|
9002
9623
|
watchMaxFix: Math.max(0, Number(env2.VO_CODE_RUNNER_WATCH_MAX_FIX ?? 1) || 0),
|
|
9003
9624
|
watchIntervalSec: Math.max(30, Number(env2.VO_CODE_RUNNER_WATCH_SEC ?? 60) || 60),
|
|
9004
|
-
armAutoMerge: env2.VO_CODE_RUNNER_ARM_AUTOMERGE
|
|
9625
|
+
armAutoMerge: env2.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0",
|
|
9626
|
+
// default ON (operator directive 2026-07-24); =0 opts out; receipt-gated consensus merge stays the mechanism
|
|
9005
9627
|
// In-product runner control. Off: VO_CODE_RUNNER_CONTROL=0.
|
|
9006
9628
|
controlEnabled: env2.VO_CODE_RUNNER_CONTROL !== "0",
|
|
9007
9629
|
controlPort: Math.max(1, Number(env2.VO_CODE_RUNNER_CONTROL_PORT ?? 7787) || 7787),
|
|
@@ -9019,7 +9641,7 @@ async function processOneTask(client, task, cfg) {
|
|
|
9019
9641
|
const wt = await Promise.resolve(createFixWorktree("code-task", { source: id.slice(0, 8), repo: task.repo }));
|
|
9020
9642
|
worktreeName = wt.worktreeName;
|
|
9021
9643
|
if (!worktreeName || !wt.worktreeDir) throw new Error("worktree isolation failure \u2014 refusing to run in the main tree");
|
|
9022
|
-
const githubToken =
|
|
9644
|
+
const { publishToken: githubToken, agentReadToken: agentGithubReadToken } = await mintRunnerGithubTokens({ client, taskId: id, log: log2, repo: task.repo, requirePublish: cfg.requireGithubAppAuth });
|
|
9023
9645
|
const parentTask = task.resumed_from ? await client.getTask(task.resumed_from).catch(() => null) : null;
|
|
9024
9646
|
const continuationRestore = await prepareContinuationBranch(wt.worktreeDir, {
|
|
9025
9647
|
task,
|
|
@@ -9054,7 +9676,7 @@ async function processOneTask(client, task, cfg) {
|
|
|
9054
9676
|
model,
|
|
9055
9677
|
effort: effectiveEffort,
|
|
9056
9678
|
maxBudgetUsd: effectiveMaxBudgetUsd,
|
|
9057
|
-
env: buildAgentProcessEnv(process.env, { agent: cfg.agent, runnerId: cfg.runnerId, taskId: id }),
|
|
9679
|
+
env: buildAgentProcessEnv(process.env, { agent: cfg.agent, runnerId: cfg.runnerId, taskId: id, githubReadToken: agentGithubReadToken }),
|
|
9058
9680
|
onProgress: (text) => {
|
|
9059
9681
|
void safeProgress(client, id, runnerStagePatch("agent_working", text));
|
|
9060
9682
|
},
|
|
@@ -9231,7 +9853,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
9231
9853
|
);
|
|
9232
9854
|
for (const line of describeClaimScoping(cfg, env2)) log2(line);
|
|
9233
9855
|
log2(
|
|
9234
|
-
cfg.armAutoMerge ? "PR auto-merge arming ON for complete PRs (VO_CODE_RUNNER_ARM_AUTOMERGE=
|
|
9856
|
+
cfg.armAutoMerge ? "PR auto-merge arming ON for complete PRs (default; VO_CODE_RUNNER_ARM_AUTOMERGE=0 to disable)" : "PR auto-merge arming OFF (VO_CODE_RUNNER_ARM_AUTOMERGE=0)"
|
|
9235
9857
|
);
|
|
9236
9858
|
log2(
|
|
9237
9859
|
watchCyclesEnabled ? `PR watcher ON \u2014 auto-fix ${cfg.watchMaxFix}/PR; ${cfg.armAutoMerge ? "receipt-gated merge enabled" : "merge disabled"}; every ${cfg.watchIntervalSec}s (VO_CODE_RUNNER_WATCH=0 to disable)` : once2 ? "PR watcher bypassed for --once; dispatched PRs remain tracked" : "PR watcher OFF (VO_CODE_RUNNER_WATCH=0)"
|
|
@@ -9240,7 +9862,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
9240
9862
|
const watchCoordinator = makeWatchCycleCoordinator({ runWatch, log: log2, intervalMs: cfg.watchIntervalSec * 1e3 });
|
|
9241
9863
|
const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log2(`agent probe failed: ${e.message}`) });
|
|
9242
9864
|
const accountUsage = makeAccountUsageProvider();
|
|
9243
|
-
const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, capacityController, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
|
|
9865
|
+
const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, capacityController, localModelController: createLocalModelRemoteController({ env: env2, log: log2 }), getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
|
|
9244
9866
|
const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log: log2 });
|
|
9245
9867
|
while (!stopping) {
|
|
9246
9868
|
const heartbeatCompletion = loopTick();
|
|
@@ -9283,7 +9905,8 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
9283
9905
|
}
|
|
9284
9906
|
log2(`claimed task ${task.code_task_id} (${task.repo})`);
|
|
9285
9907
|
active += 1;
|
|
9286
|
-
const
|
|
9908
|
+
const runTask = task.kind === "inference" ? processInferenceTask(client, task, cfg, { safeProgress, runnerStagePatch, log: log2 }) : processOneTask(client, task, cfg);
|
|
9909
|
+
const done = runTask.finally(() => {
|
|
9287
9910
|
active -= 1;
|
|
9288
9911
|
});
|
|
9289
9912
|
if (once2) {
|
|
@@ -9316,6 +9939,7 @@ var init_code_runner_daemon = __esm({
|
|
|
9316
9939
|
init_loop_ticks();
|
|
9317
9940
|
init_runner_capacity();
|
|
9318
9941
|
init_agent_availability();
|
|
9942
|
+
init_local_model_remote_config();
|
|
9319
9943
|
init_account_usage();
|
|
9320
9944
|
init_pr_watcher();
|
|
9321
9945
|
init_existing_pr_target();
|
|
@@ -9326,6 +9950,7 @@ var init_code_runner_daemon = __esm({
|
|
|
9326
9950
|
init_reconnect_backoff();
|
|
9327
9951
|
init_task_helpers();
|
|
9328
9952
|
init_agent_process_env();
|
|
9953
|
+
init_inference_task_runner();
|
|
9329
9954
|
init_isolation_audit();
|
|
9330
9955
|
init_recovery_ledger();
|
|
9331
9956
|
init_no_changes_terminal_status();
|