@algosuite/vo-mcp 0.2.0-beta.20 → 0.2.0-beta.21

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.
@@ -2477,7 +2477,7 @@ function createControlPlaneClient({
2477
2477
  * authenticated operator so the web shows a TRUE "runner online" signal.
2478
2478
  * Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
2479
2479
  */
2480
- async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
2480
+ async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, maxConcurrency, effectiveConcurrency, measuredTaskSlots, measuredCpuSlots, measuredMemorySlots, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage, availableLocalModels }) {
2481
2481
  const body = { runner_id: runnerId };
2482
2482
  if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
2483
2483
  if (operatorId) body.operator_id = operatorId;
@@ -2506,6 +2506,9 @@ function createControlPlaneClient({
2506
2506
  if (Array.isArray(accountUsage) && accountUsage.length > 0) {
2507
2507
  body.account_usage = accountUsage;
2508
2508
  }
2509
+ if (Array.isArray(availableLocalModels) && availableLocalModels.length > 0) {
2510
+ body.available_local_models = availableLocalModels;
2511
+ }
2509
2512
  const res = await req("POST", "/api/v1/runner/heartbeat", body, {
2510
2513
  timeoutMs: heartbeatTimeoutMs
2511
2514
  });
@@ -3752,7 +3755,10 @@ var init_agent_key_store = __esm({
3752
3755
  meta: ["MODEL_API_KEY"],
3753
3756
  // Generic OpenAI-compatible runner (bring-your-own model + endpoint): its key
3754
3757
  // is a dedicated var so it never collides with a real OpenAI/Codex key.
3755
- "oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"]
3758
+ "oai-compat": ["VO_CODE_RUNNER_OAI_API_KEY"],
3759
+ // Sovereign local inference (Ollama / LM Studio). The key is OPTIONAL — most
3760
+ // local servers need none — and exists for locally secured endpoints only.
3761
+ local: ["VO_CODE_RUNNER_LOCAL_API_KEY"]
3756
3762
  };
3757
3763
  PROVIDER_ALIAS = {
3758
3764
  claude: "anthropic",
@@ -3765,7 +3771,10 @@ var init_agent_key_store = __esm({
3765
3771
  spark: "meta",
3766
3772
  "muse-spark": "meta",
3767
3773
  oai: "oai-compat",
3768
- "oai-compat": "oai-compat"
3774
+ "oai-compat": "oai-compat",
3775
+ local: "local",
3776
+ ollama: "local",
3777
+ lmstudio: "local"
3769
3778
  };
3770
3779
  _loadTried2 = false;
3771
3780
  }
@@ -4096,6 +4105,200 @@ var init_cursor_runner = __esm({
4096
4105
  }
4097
4106
  });
4098
4107
 
4108
+ // ../../scripts/virtual-office/code-runner/local-model-runner.mjs
4109
+ function resolveLocalProvider(env2 = process.env) {
4110
+ return String(env2.VO_CODE_RUNNER_LOCAL_PROVIDER || "").trim().toLowerCase() || DEFAULT_LOCAL_PROVIDER;
4111
+ }
4112
+ function setRemoteDesiredLocalModel(model) {
4113
+ const value = typeof model === "string" ? model.trim() : "";
4114
+ remoteDesiredLocalModel = value && isValidLocalModel(value) ? value : "";
4115
+ }
4116
+ function resolveLocalModel(env2 = process.env) {
4117
+ return String(env2.VO_CODE_RUNNER_LOCAL_MODEL || "").trim() || remoteDesiredLocalModel;
4118
+ }
4119
+ function resolveLocalBaseUrl(env2 = process.env) {
4120
+ return String(env2.VO_CODE_RUNNER_LOCAL_BASE_URL || "").trim();
4121
+ }
4122
+ function isValidLocalModel(model) {
4123
+ return LOCAL_MODEL_RE.test(String(model || ""));
4124
+ }
4125
+ function isLoopbackBaseUrl(url) {
4126
+ const raw = String(url || "").trim();
4127
+ if (!/^https?:\/\/[^\s"'`\\]+$/.test(raw)) return false;
4128
+ let parsed;
4129
+ try {
4130
+ parsed = new URL(raw);
4131
+ } catch {
4132
+ return false;
4133
+ }
4134
+ const host = parsed.hostname.toLowerCase();
4135
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
4136
+ }
4137
+ function buildLocalArgs(opts = {}, env2 = process.env) {
4138
+ const provider = resolveLocalProvider(env2);
4139
+ if (!LOCAL_PROVIDERS.includes(provider)) {
4140
+ throw new Error(
4141
+ `local-model runner: unknown VO_CODE_RUNNER_LOCAL_PROVIDER "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")}).`
4142
+ );
4143
+ }
4144
+ const model = resolveLocalModel(env2);
4145
+ if (!model) {
4146
+ throw new Error(
4147
+ "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)."
4148
+ );
4149
+ }
4150
+ if (!isValidLocalModel(model)) {
4151
+ throw new Error(`local-model runner: "${model}" is not a valid local model id.`);
4152
+ }
4153
+ const baseUrl = resolveLocalBaseUrl(env2);
4154
+ if (baseUrl && !isLoopbackBaseUrl(baseUrl)) {
4155
+ throw new Error(
4156
+ "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."
4157
+ );
4158
+ }
4159
+ const args = [
4160
+ "exec",
4161
+ "--json",
4162
+ "-c",
4163
+ 'approval_policy="never"',
4164
+ "--sandbox",
4165
+ "workspace-write",
4166
+ "--skip-git-repo-check",
4167
+ "--oss",
4168
+ "--local-provider",
4169
+ provider,
4170
+ "--model",
4171
+ model
4172
+ ];
4173
+ if (opts.effort) {
4174
+ args.push("-c", `model_reasoning_effort="${String(opts.effort)}"`);
4175
+ }
4176
+ args.push("-");
4177
+ return args;
4178
+ }
4179
+ function applyLocalAuthEnv(baseEnv = process.env, configEnv = process.env) {
4180
+ const out = withAgentKey("local", baseEnv);
4181
+ const baseUrl = resolveLocalBaseUrl(configEnv);
4182
+ if (baseUrl && isLoopbackBaseUrl(baseUrl) && resolveLocalProvider(configEnv) === "ollama" && !String(out.OLLAMA_HOST || "").trim()) {
4183
+ out.OLLAMA_HOST = baseUrl.replace(/\/+$/, "");
4184
+ }
4185
+ return out;
4186
+ }
4187
+ var LOCAL_API_KEY_ENV, LOCAL_PROVIDERS, DEFAULT_LOCAL_PROVIDER, LOCAL_PROBE_URLS, remoteDesiredLocalModel, LOCAL_MODEL_RE, LocalModelRunner, localModelRunner;
4188
+ var init_local_model_runner = __esm({
4189
+ "../../scripts/virtual-office/code-runner/local-model-runner.mjs"() {
4190
+ "use strict";
4191
+ init_codex_runner();
4192
+ init_agent_key_store();
4193
+ LOCAL_API_KEY_ENV = "VO_CODE_RUNNER_LOCAL_API_KEY";
4194
+ LOCAL_PROVIDERS = ["ollama", "lmstudio"];
4195
+ DEFAULT_LOCAL_PROVIDER = "ollama";
4196
+ LOCAL_PROBE_URLS = {
4197
+ ollama: "http://127.0.0.1:11434/api/version",
4198
+ lmstudio: "http://127.0.0.1:1234/v1/models"
4199
+ };
4200
+ remoteDesiredLocalModel = "";
4201
+ LOCAL_MODEL_RE = new RegExp("^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$");
4202
+ LocalModelRunner = class {
4203
+ constructor({
4204
+ spawn: spawn5 = null,
4205
+ resolveBinary = resolveCodexBinary,
4206
+ env: env2 = process.env,
4207
+ fetchImpl = globalThis.fetch
4208
+ } = {}) {
4209
+ this.spawn = spawn5;
4210
+ this.resolveBinary = resolveBinary;
4211
+ this.env = env2;
4212
+ this.fetchImpl = fetchImpl;
4213
+ }
4214
+ /** Codex is the transport binary; the model/endpoint are the user's. */
4215
+ get binary() {
4216
+ return this.resolveBinary();
4217
+ }
4218
+ buildArgs(opts = {}) {
4219
+ return buildLocalArgs(opts, this.env);
4220
+ }
4221
+ /** Codex JSONL events map identically → reuse the proven parser. */
4222
+ parseEvent(line) {
4223
+ return parseCodexEvent(line);
4224
+ }
4225
+ // SECURITY: never shell — see no-shell-spawn.test.mjs. The model id is
4226
+ // control-plane-influenced and validated, but shell:false is the hard floor.
4227
+ getSpawnOptions() {
4228
+ return {
4229
+ shell: false,
4230
+ windowsHide: true,
4231
+ windowsVerbatimArguments: false
4232
+ };
4233
+ }
4234
+ applyAuthEnv(env2 = process.env) {
4235
+ return applyLocalAuthEnv(env2, this.env);
4236
+ }
4237
+ describeAuth(env2 = process.env) {
4238
+ const authEnv = this.applyAuthEnv(env2);
4239
+ const provider = resolveLocalProvider(this.env);
4240
+ const model = resolveLocalModel(this.env) || "<unset>";
4241
+ const hasKey = Boolean(String(authEnv[LOCAL_API_KEY_ENV] || "").trim());
4242
+ return `local provider=${provider} model=${model} key=${hasKey ? "set" : "none (optional)"}`;
4243
+ }
4244
+ /**
4245
+ * Best-effort: codex transport present AND the local inference endpoint
4246
+ * answers. Never throws, never spends tokens; the endpoint probe is bounded
4247
+ * to 1.5s so a stopped Ollama can't hang availability checks.
4248
+ */
4249
+ async checkAuth() {
4250
+ const provider = resolveLocalProvider(this.env);
4251
+ if (!LOCAL_PROVIDERS.includes(provider)) {
4252
+ return {
4253
+ installed: false,
4254
+ authenticated: false,
4255
+ message: `unknown local provider "${provider}" (supported: ${LOCAL_PROVIDERS.join(", ")})`
4256
+ };
4257
+ }
4258
+ const model = resolveLocalModel(this.env);
4259
+ const override = resolveLocalBaseUrl(this.env);
4260
+ if (override && !isLoopbackBaseUrl(override)) {
4261
+ return {
4262
+ installed: true,
4263
+ authenticated: false,
4264
+ message: "VO_CODE_RUNNER_LOCAL_BASE_URL is not loopback \u2014 refused (fail-closed)"
4265
+ };
4266
+ }
4267
+ const probeUrl = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/version` : LOCAL_PROBE_URLS[provider];
4268
+ let endpointUp = false;
4269
+ let probeNote = "";
4270
+ try {
4271
+ const res = await this.fetchImpl(probeUrl, { signal: AbortSignal.timeout(1500) });
4272
+ endpointUp = Boolean(res?.ok);
4273
+ if (!endpointUp) probeNote = `endpoint ${probeUrl} answered HTTP ${res?.status}`;
4274
+ } catch {
4275
+ probeNote = `no local inference server answering at ${probeUrl}`;
4276
+ }
4277
+ if (!endpointUp) {
4278
+ return {
4279
+ installed: true,
4280
+ authenticated: false,
4281
+ message: `${probeNote} \u2014 start ${provider === "ollama" ? "Ollama" : "LM Studio"} first`
4282
+ };
4283
+ }
4284
+ if (!model) {
4285
+ return {
4286
+ installed: true,
4287
+ authenticated: false,
4288
+ message: `${provider} is running but VO_CODE_RUNNER_LOCAL_MODEL is not set`
4289
+ };
4290
+ }
4291
+ return {
4292
+ installed: true,
4293
+ authenticated: true,
4294
+ message: `${provider} reachable; model "${model}" configured (local-only, no cloud spend)`
4295
+ };
4296
+ }
4297
+ };
4298
+ localModelRunner = new LocalModelRunner();
4299
+ }
4300
+ });
4301
+
4099
4302
  // ../../scripts/virtual-office/code-runner/meta-runner.mjs
4100
4303
  function applyMetaAuthEnv(baseEnv = process.env) {
4101
4304
  const out = withAgentKey("meta", baseEnv);
@@ -4311,6 +4514,7 @@ var init_resolve_runner = __esm({
4311
4514
  init_claude_runner();
4312
4515
  init_codex_runner();
4313
4516
  init_cursor_runner();
4517
+ init_local_model_runner();
4314
4518
  init_meta_runner();
4315
4519
  init_openai_compatible_runner();
4316
4520
  init_agent_runner_interface();
@@ -4319,6 +4523,9 @@ var init_resolve_runner = __esm({
4319
4523
  claude: claudeRunner,
4320
4524
  codex: codexRunner,
4321
4525
  cursor: cursorRunner,
4526
+ // `local` = sovereign local inference (Ollama / LM Studio; Mistral/Llama-class
4527
+ // models) — free tier of the pricing pivot; nothing leaves the user's machine.
4528
+ local: localModelRunner,
4322
4529
  meta: metaRunner,
4323
4530
  oai: openaiCompatibleRunner
4324
4531
  };
@@ -5983,6 +6190,12 @@ function makeLoopTicks({
5983
6190
  applyCapacity: () => false,
5984
6191
  heartbeatFields: () => ({})
5985
6192
  },
6193
+ // Track 1: served-local-model reporting + desired-model echo application
6194
+ // (local-model-remote-config.mjs). No-op defaults keep old callers working.
6195
+ localModelController = {
6196
+ applyRemoteConfig: () => false,
6197
+ heartbeatFields: () => ({})
6198
+ },
5986
6199
  // Cached agent-availability provider (agent-availability.mjs); returns null
5987
6200
  // until the first probe completes — the heartbeat simply omits the field.
5988
6201
  getAgentAvailability = () => null,
@@ -6020,7 +6233,10 @@ function makeLoopTicks({
6020
6233
  } catch (error) {
6021
6234
  request = Promise.reject(error);
6022
6235
  }
6023
- request.then((response) => capacityController.applyCapacity(response?.capacity, nextPayload.operatorId)).catch((e) => log3(`heartbeat failed: ${e.message}`)).finally(() => {
6236
+ request.then((response) => {
6237
+ capacityController.applyCapacity(response?.capacity, nextPayload.operatorId);
6238
+ localModelController.applyRemoteConfig(response?.local_model, nextPayload.operatorId);
6239
+ }).catch((e) => log3(`heartbeat failed: ${e.message}`)).finally(() => {
6024
6240
  for (const done of waiters) done();
6025
6241
  if (state.pending) {
6026
6242
  const pending = state.pending;
@@ -6062,6 +6278,7 @@ function makeLoopTicks({
6062
6278
  const supervisorVersion = String(env2.VO_RUNNER_SUPERVISOR_VERSION || "").trim().slice(0, 40);
6063
6279
  const supervisorCapabilities = String(env2.VO_RUNNER_SUPERVISOR_CAPABILITIES || "").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
6064
6280
  const capacityFields = capacityController.heartbeatFields();
6281
+ const localModelFields = localModelController.heartbeatFields();
6065
6282
  const baseHeartbeat = {
6066
6283
  runnerId: cfg.runnerId,
6067
6284
  ...runnerInstanceId ? { runnerInstanceId } : {},
@@ -6078,7 +6295,8 @@ function makeLoopTicks({
6078
6295
  uptimeSec: Math.floor(process.uptime()),
6079
6296
  activeTasks: getActive(),
6080
6297
  maxConcurrency: cfg.maxConcurrency,
6081
- ...capacityFields
6298
+ ...capacityFields,
6299
+ ...localModelFields
6082
6300
  };
6083
6301
  const operatorIds = servedOperators.length > 0 ? servedOperators : [void 0];
6084
6302
  for (const operatorId of operatorIds) {
@@ -6245,6 +6463,127 @@ var init_agent_availability = __esm({
6245
6463
  }
6246
6464
  });
6247
6465
 
6466
+ // ../../scripts/virtual-office/code-runner/local-model-remote-config.mjs
6467
+ async function listServedLocalModels({
6468
+ env: env2 = process.env,
6469
+ fetchImpl = globalThis.fetch,
6470
+ timeoutMs = 1500
6471
+ } = {}) {
6472
+ const provider = resolveLocalProvider(env2);
6473
+ if (!LOCAL_PROVIDERS.includes(provider)) return null;
6474
+ const override = resolveLocalBaseUrl(env2);
6475
+ if (override && !isLoopbackBaseUrl(override)) return null;
6476
+ const url = provider === "ollama" && override ? `${override.replace(/\/+$/, "")}/api/tags` : SERVED_MODELS_PROBE_URLS[provider];
6477
+ try {
6478
+ const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
6479
+ if (!res?.ok) return null;
6480
+ const json = await res.json();
6481
+ 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;
6482
+ if (!names) return null;
6483
+ return names.filter((name) => typeof name === "string" && isValidLocalModel(name)).slice(0, MAX_REPORTED_MODELS);
6484
+ } catch {
6485
+ return null;
6486
+ }
6487
+ }
6488
+ function createLocalModelRemoteController({
6489
+ env: env2 = process.env,
6490
+ fetchImpl = globalThis.fetch,
6491
+ log: log3 = () => {
6492
+ },
6493
+ probeIntervalMs = 6e4,
6494
+ now = () => Date.now(),
6495
+ apply = setRemoteDesiredLocalModel,
6496
+ listModels = listServedLocalModels
6497
+ } = {}) {
6498
+ let served = null;
6499
+ const byOperator = /* @__PURE__ */ new Map();
6500
+ let probing = false;
6501
+ let lastProbeAt = 0;
6502
+ let warnedUnserved = "";
6503
+ let warnedConflict = "";
6504
+ function desiredModel() {
6505
+ const models = [...new Set(
6506
+ [...byOperator.values()].map((s) => s.desired).filter((m) => typeof m === "string" && m)
6507
+ )];
6508
+ if (models.length > 1) {
6509
+ const key = models.slice().sort().join(",");
6510
+ if (warnedConflict !== key) {
6511
+ warnedConflict = key;
6512
+ log3(`local-model remote config: conflicting desired models across served operators (${key}) \u2014 applying none`);
6513
+ }
6514
+ return null;
6515
+ }
6516
+ return models[0] ?? null;
6517
+ }
6518
+ function syncEffective() {
6519
+ const desired = desiredModel();
6520
+ const effective = desired && Array.isArray(served) && served.includes(desired) ? desired : "";
6521
+ apply(effective);
6522
+ if (desired && !effective && warnedUnserved !== desired) {
6523
+ warnedUnserved = desired;
6524
+ log3(
6525
+ `local-model remote config: "${desired}" is not served by the local inference server \u2014 ignored (never auto-pull; pull it locally first)`
6526
+ );
6527
+ }
6528
+ return effective;
6529
+ }
6530
+ function refreshServedModels() {
6531
+ if (probing || now() - lastProbeAt < probeIntervalMs) return;
6532
+ probing = true;
6533
+ lastProbeAt = now();
6534
+ Promise.resolve(listModels({ env: env2, fetchImpl })).then((models) => {
6535
+ if (Array.isArray(models)) served = models;
6536
+ syncEffective();
6537
+ }).catch(() => {
6538
+ }).finally(() => {
6539
+ probing = false;
6540
+ });
6541
+ }
6542
+ return {
6543
+ /** Heartbeat payload extras; also kicks the throttled background probe. */
6544
+ heartbeatFields() {
6545
+ refreshServedModels();
6546
+ return Array.isArray(served) && served.length > 0 ? { availableLocalModels: served } : {};
6547
+ },
6548
+ /**
6549
+ * Apply one operator's heartbeat-response echo
6550
+ * { schema_version, desired_local_model, revision }. Revision ordering is
6551
+ * PER OPERATOR — counters are independent across operator scopes.
6552
+ */
6553
+ applyRemoteConfig(echo, operatorId = "") {
6554
+ const scope = String(operatorId || "");
6555
+ const previous = byOperator.get(scope);
6556
+ if (!echo || echo.schema_version !== 1 || !Number.isInteger(echo.revision) || previous && echo.revision < previous.revision) {
6557
+ return false;
6558
+ }
6559
+ const model = echo.desired_local_model;
6560
+ if (model !== null && (typeof model !== "string" || !isValidLocalModel(model))) {
6561
+ return false;
6562
+ }
6563
+ byOperator.set(scope, { revision: echo.revision, desired: model });
6564
+ syncEffective();
6565
+ return true;
6566
+ },
6567
+ snapshot: () => ({
6568
+ served,
6569
+ desired: desiredModel(),
6570
+ operators: Object.fromEntries(byOperator)
6571
+ })
6572
+ };
6573
+ }
6574
+ var SERVED_MODELS_PROBE_URLS, MAX_REPORTED_MODELS;
6575
+ var init_local_model_remote_config = __esm({
6576
+ "../../scripts/virtual-office/code-runner/local-model-remote-config.mjs"() {
6577
+ "use strict";
6578
+ init_local_model_runner();
6579
+ SERVED_MODELS_PROBE_URLS = {
6580
+ ollama: "http://127.0.0.1:11434/api/tags",
6581
+ lmstudio: "http://127.0.0.1:1234/v1/models"
6582
+ };
6583
+ MAX_REPORTED_MODELS = 50;
6584
+ }
6585
+ });
6586
+
6248
6587
  // ../../scripts/virtual-office/code-runner/account-usage.mjs
6249
6588
  import { spawn as spawn4 } from "node:child_process";
6250
6589
  import fs6 from "node:fs";
@@ -6854,7 +7193,8 @@ function makeWatchRunner({
6854
7193
  maxFixAttempts,
6855
7194
  repairEvidence = readCiRepairEvidence,
6856
7195
  stateFile = DEFAULT_STATE_FILE,
6857
- autoMergeEnabled = process.env.VO_CODE_RUNNER_ARM_AUTOMERGE === "1"
7196
+ // Default ON (operator directive 2026-07-24); VO_CODE_RUNNER_ARM_AUTOMERGE=0 opts out.
7197
+ autoMergeEnabled = process.env.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0"
6858
7198
  }) {
6859
7199
  return () => runWatchCycle({
6860
7200
  viewPr,
@@ -7511,7 +7851,7 @@ var init_model_router = __esm({
7511
7851
  "use strict";
7512
7852
  init_model_registry();
7513
7853
  init_meta_model_catalog();
7514
- TASK_MODEL_AGENTS = ["claude", "codex", "cursor", "meta"];
7854
+ TASK_MODEL_AGENTS = ["claude", "codex", "cursor", "local", "meta"];
7515
7855
  DEFAULT_AGENT2 = "claude";
7516
7856
  AGENT_TIER_FAMILIES = {
7517
7857
  claude: {
@@ -7533,6 +7873,13 @@ var init_model_router = __esm({
7533
7873
  mid: null,
7534
7874
  best: null
7535
7875
  },
7876
+ // Local models are machine-specific pulls (Ollama / LM Studio); the runner's
7877
+ // VO_CODE_RUNNER_LOCAL_MODEL is the source of truth, not the registry.
7878
+ local: {
7879
+ cheap: null,
7880
+ mid: null,
7881
+ best: null
7882
+ },
7536
7883
  meta: {
7537
7884
  cheap: null,
7538
7885
  mid: null,
@@ -7555,6 +7902,11 @@ var init_model_router = __esm({
7555
7902
  mid: null,
7556
7903
  best: null
7557
7904
  },
7905
+ local: {
7906
+ cheap: null,
7907
+ mid: null,
7908
+ best: null
7909
+ },
7558
7910
  meta: {
7559
7911
  cheap: resolveMetaModelForTier("cheap"),
7560
7912
  mid: resolveMetaModelForTier("mid"),
@@ -7573,6 +7925,16 @@ var init_model_router = __esm({
7573
7925
  // cursor-runner. Restrict to the shape a model id actually has so a payload
7574
7926
  // like `x & powershell -enc ...` is rejected before it reaches a spawn.
7575
7927
  cursor: (model) => /^[A-Za-z0-9][A-Za-z0-9._:@\[\]-]{0,79}$/.test(String(model || "")),
7928
+ // SECURITY: the local lane accepts NO remote model pins — return false for
7929
+ // EVERY value so control-plane `task.model` can never influence what runs on
7930
+ // the user's machine. codex `--oss` AUTO-PULLS missing models (live-verified
7931
+ // 2026-07-24: a single --model argument downloaded 397MB unprompted), so an
7932
+ // honored pin like "llama3.1:405b" (231GB, valid id shape) would let any
7933
+ // tenant member remotely fill a BYO runner owner's disk. The machine-local
7934
+ // env (VO_CODE_RUNNER_LOCAL_MODEL — the owner's own choice in the app) is
7935
+ // the ONLY model authority, mirroring the runner-release-target principle
7936
+ // that browser input is never local-execution authority.
7937
+ local: () => false,
7576
7938
  meta: (model) => /^muse-spark-[A-Za-z0-9._:@\[\]-]{0,79}$/i.test(String(model || ""))
7577
7939
  };
7578
7940
  }
@@ -8569,6 +8931,198 @@ var init_agent_process_env = __esm({
8569
8931
  }
8570
8932
  });
8571
8933
 
8934
+ // ../../scripts/virtual-office/code-runner/inference-executor.mjs
8935
+ function buildChatBody({ messages, prompt, model, maxTokens, temperature } = {}) {
8936
+ const msgs = Array.isArray(messages) && messages.length > 0 ? messages : [{ role: "user", content: String(prompt ?? "") }];
8937
+ if (msgs.length > MAX_INFERENCE_MESSAGES) {
8938
+ throw new Error(`inference: too many messages (${msgs.length} > ${MAX_INFERENCE_MESSAGES})`);
8939
+ }
8940
+ const totalChars = msgs.reduce((n, m) => n + String(m?.content ?? "").length, 0);
8941
+ if (totalChars > MAX_INFERENCE_PROMPT_CHARS) {
8942
+ throw new Error(`inference: prompt too large (${totalChars} > ${MAX_INFERENCE_PROMPT_CHARS} chars)`);
8943
+ }
8944
+ if (!model || typeof model !== "string") {
8945
+ throw new Error("inference: a model id is required");
8946
+ }
8947
+ const body = {
8948
+ model,
8949
+ messages: msgs.map((m) => ({ role: String(m.role || "user"), content: String(m.content ?? "") })),
8950
+ stream: false,
8951
+ max_tokens: Number.isInteger(maxTokens) && maxTokens > 0 ? maxTokens : DEFAULT_MAX_OUTPUT_TOKENS
8952
+ };
8953
+ if (Number.isFinite(temperature)) body.temperature = temperature;
8954
+ return body;
8955
+ }
8956
+ function resolveInferenceEndpoint({ providerMode, baseUrl } = {}) {
8957
+ const mode = String(providerMode || "local").toLowerCase();
8958
+ if (mode !== "local") {
8959
+ throw new Error(`inference: unsupported provider mode "${mode}" (this slice is local-only)`);
8960
+ }
8961
+ const url = String(baseUrl || DEFAULT_LOCAL_INFERENCE_BASE_URL).replace(/\/+$/, "");
8962
+ if (!isLoopbackBaseUrl(url)) {
8963
+ throw new Error(`inference: local base_url must be loopback (localhost/127.0.0.1/[::1]); got "${url}"`);
8964
+ }
8965
+ return url;
8966
+ }
8967
+ function extractCompletion(json) {
8968
+ const choice = json?.choices?.[0];
8969
+ const text = choice?.message?.content ?? choice?.text ?? "";
8970
+ if (typeof text !== "string" || text.length === 0) {
8971
+ throw new Error("inference: provider returned no completion text");
8972
+ }
8973
+ const usage = json?.usage ?? null;
8974
+ return {
8975
+ text,
8976
+ usage: usage ? {
8977
+ input_tokens: Number(usage.prompt_tokens) || 0,
8978
+ output_tokens: Number(usage.completion_tokens) || 0,
8979
+ total_tokens: Number(usage.total_tokens) || 0
8980
+ } : null,
8981
+ finish_reason: choice?.finish_reason ?? null
8982
+ };
8983
+ }
8984
+ async function runInference({ providerMode = "local", baseUrl, model, prompt, messages, maxTokens, temperature, timeoutMs = 12e4 } = {}, { fetchImpl = globalThis.fetch } = {}) {
8985
+ const endpoint = resolveInferenceEndpoint({ providerMode, baseUrl });
8986
+ const body = buildChatBody({ messages, prompt, model, maxTokens, temperature });
8987
+ const res = await fetchImpl(`${endpoint}/chat/completions`, {
8988
+ method: "POST",
8989
+ headers: { "Content-Type": "application/json" },
8990
+ body: JSON.stringify(body),
8991
+ signal: AbortSignal.timeout(timeoutMs)
8992
+ });
8993
+ if (!res?.ok) {
8994
+ const status = res?.status ?? "ERR";
8995
+ let detail = "";
8996
+ try {
8997
+ detail = (await res.text()).slice(0, 300);
8998
+ } catch {
8999
+ }
9000
+ throw new Error(`inference: endpoint returned HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`);
9001
+ }
9002
+ const json = await res.json();
9003
+ const { text, usage, finish_reason } = extractCompletion(json);
9004
+ return { text, usage, finish_reason, model, endpoint };
9005
+ }
9006
+ var DEFAULT_LOCAL_INFERENCE_BASE_URL, MAX_INFERENCE_MESSAGES, MAX_INFERENCE_PROMPT_CHARS, DEFAULT_MAX_OUTPUT_TOKENS;
9007
+ var init_inference_executor = __esm({
9008
+ "../../scripts/virtual-office/code-runner/inference-executor.mjs"() {
9009
+ "use strict";
9010
+ init_local_model_runner();
9011
+ DEFAULT_LOCAL_INFERENCE_BASE_URL = "http://127.0.0.1:11434/v1";
9012
+ MAX_INFERENCE_MESSAGES = 200;
9013
+ MAX_INFERENCE_PROMPT_CHARS = 1e5;
9014
+ DEFAULT_MAX_OUTPUT_TOKENS = 1024;
9015
+ }
9016
+ });
9017
+
9018
+ // ../../scripts/virtual-office/code-runner/inference-task-handler.mjs
9019
+ async function handleInferenceTask(task, { runInferenceImpl = runInference, now = () => Date.now() } = {}) {
9020
+ const started = now();
9021
+ try {
9022
+ const result = await runInferenceImpl({
9023
+ providerMode: "local",
9024
+ baseUrl: task.base_url,
9025
+ model: task.model,
9026
+ prompt: task.prompt,
9027
+ messages: task.messages,
9028
+ maxTokens: task.max_output_tokens,
9029
+ temperature: task.temperature
9030
+ });
9031
+ const text = String(result.text || "").slice(0, MAX_RESULT_CHARS);
9032
+ return {
9033
+ ok: true,
9034
+ text,
9035
+ usage: result.usage ?? null,
9036
+ finish_reason: result.finish_reason ?? null,
9037
+ provider_mode: "local",
9038
+ // Structural $0: no AlgoSuite provider call happened, so nothing to charge.
9039
+ billable: false,
9040
+ duration_ms: now() - started
9041
+ };
9042
+ } catch (err) {
9043
+ return {
9044
+ ok: false,
9045
+ provider_mode: "local",
9046
+ error: err instanceof Error ? err.message : String(err),
9047
+ duration_ms: now() - started
9048
+ };
9049
+ }
9050
+ }
9051
+ var MAX_RESULT_CHARS;
9052
+ var init_inference_task_handler = __esm({
9053
+ "../../scripts/virtual-office/code-runner/inference-task-handler.mjs"() {
9054
+ "use strict";
9055
+ init_inference_executor();
9056
+ MAX_RESULT_CHARS = 1e5;
9057
+ }
9058
+ });
9059
+
9060
+ // ../../scripts/virtual-office/code-runner/inference-task-runner.mjs
9061
+ async function processInferenceTask(client, task, cfg, {
9062
+ env: env2 = process.env,
9063
+ safeProgress: safeProgress2,
9064
+ runnerStagePatch: runnerStagePatch2,
9065
+ handle = handleInferenceTask,
9066
+ log: log3 = () => {
9067
+ }
9068
+ } = {}) {
9069
+ const id = task.code_task_id;
9070
+ await safeProgress2(
9071
+ client,
9072
+ id,
9073
+ runnerStagePatch2("starting_agent", `${cfg.runnerId} running local inference for this task`, {
9074
+ status: "running"
9075
+ })
9076
+ );
9077
+ const model = resolveLocalModel(env2);
9078
+ if (!model) {
9079
+ await safeProgress2(client, id, {
9080
+ status: "failed",
9081
+ message: "inference: this runner has no local model configured",
9082
+ 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."
9083
+ });
9084
+ return;
9085
+ }
9086
+ const out = await handle(
9087
+ {
9088
+ provider_mode: "local",
9089
+ model,
9090
+ prompt: task.prompt,
9091
+ base_url: resolveLocalBaseUrl(env2) || void 0
9092
+ },
9093
+ { now: () => Date.now() }
9094
+ );
9095
+ if (out.ok) {
9096
+ log3(`inference task ${id} completed on ${model} (${out.usage?.output_tokens ?? "?"} output tokens)`);
9097
+ await safeProgress2(client, id, {
9098
+ status: INFERENCE_SUCCESS_STATUS,
9099
+ message: "inference complete",
9100
+ result: out.text,
9101
+ // $0 by construction — the completion was produced on the user's machine,
9102
+ // AlgoSuite made no provider call, so there is nothing to bill.
9103
+ cost_usd: 0,
9104
+ num_turns: 1
9105
+ });
9106
+ return;
9107
+ }
9108
+ log3(`inference task ${id} failed: ${out.error}`);
9109
+ await safeProgress2(client, id, {
9110
+ status: "failed",
9111
+ message: "inference failed",
9112
+ result: out.error,
9113
+ cost_usd: 0
9114
+ });
9115
+ }
9116
+ var INFERENCE_SUCCESS_STATUS;
9117
+ var init_inference_task_runner = __esm({
9118
+ "../../scripts/virtual-office/code-runner/inference-task-runner.mjs"() {
9119
+ "use strict";
9120
+ init_local_model_runner();
9121
+ init_inference_task_handler();
9122
+ INFERENCE_SUCCESS_STATUS = "no_changes_needed";
9123
+ }
9124
+ });
9125
+
8572
9126
  // ../../scripts/virtual-office/code-runner/isolation-audit.mjs
8573
9127
  import fs8 from "node:fs";
8574
9128
  import fsp9 from "node:fs/promises";
@@ -9001,7 +9555,8 @@ function loadConfig(env2 = process.env) {
9001
9555
  watchEnabled: env2.VO_CODE_RUNNER_WATCH !== "0",
9002
9556
  watchMaxFix: Math.max(0, Number(env2.VO_CODE_RUNNER_WATCH_MAX_FIX ?? 1) || 0),
9003
9557
  watchIntervalSec: Math.max(30, Number(env2.VO_CODE_RUNNER_WATCH_SEC ?? 60) || 60),
9004
- armAutoMerge: env2.VO_CODE_RUNNER_ARM_AUTOMERGE === "1",
9558
+ armAutoMerge: env2.VO_CODE_RUNNER_ARM_AUTOMERGE !== "0",
9559
+ // default ON (operator directive 2026-07-24); =0 opts out; receipt-gated consensus merge stays the mechanism
9005
9560
  // In-product runner control. Off: VO_CODE_RUNNER_CONTROL=0.
9006
9561
  controlEnabled: env2.VO_CODE_RUNNER_CONTROL !== "0",
9007
9562
  controlPort: Math.max(1, Number(env2.VO_CODE_RUNNER_CONTROL_PORT ?? 7787) || 7787),
@@ -9231,7 +9786,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
9231
9786
  );
9232
9787
  for (const line of describeClaimScoping(cfg, env2)) log2(line);
9233
9788
  log2(
9234
- cfg.armAutoMerge ? "PR auto-merge arming ON for complete PRs (VO_CODE_RUNNER_ARM_AUTOMERGE=1)" : "PR auto-merge arming OFF (set VO_CODE_RUNNER_ARM_AUTOMERGE=1 for internal dogfood autonomy)"
9789
+ 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
9790
  );
9236
9791
  log2(
9237
9792
  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 +9795,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
9240
9795
  const watchCoordinator = makeWatchCycleCoordinator({ runWatch, log: log2, intervalMs: cfg.watchIntervalSec * 1e3 });
9241
9796
  const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log2(`agent probe failed: ${e.message}`) });
9242
9797
  const accountUsage = makeAccountUsageProvider();
9243
- const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, capacityController, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
9798
+ 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
9799
  const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log: log2 });
9245
9800
  while (!stopping) {
9246
9801
  const heartbeatCompletion = loopTick();
@@ -9283,7 +9838,8 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
9283
9838
  }
9284
9839
  log2(`claimed task ${task.code_task_id} (${task.repo})`);
9285
9840
  active += 1;
9286
- const done = processOneTask(client, task, cfg).finally(() => {
9841
+ const runTask = task.kind === "inference" ? processInferenceTask(client, task, cfg, { safeProgress, runnerStagePatch, log: log2 }) : processOneTask(client, task, cfg);
9842
+ const done = runTask.finally(() => {
9287
9843
  active -= 1;
9288
9844
  });
9289
9845
  if (once2) {
@@ -9316,6 +9872,7 @@ var init_code_runner_daemon = __esm({
9316
9872
  init_loop_ticks();
9317
9873
  init_runner_capacity();
9318
9874
  init_agent_availability();
9875
+ init_local_model_remote_config();
9319
9876
  init_account_usage();
9320
9877
  init_pr_watcher();
9321
9878
  init_existing_pr_target();
@@ -9326,6 +9883,7 @@ var init_code_runner_daemon = __esm({
9326
9883
  init_reconnect_backoff();
9327
9884
  init_task_helpers();
9328
9885
  init_agent_process_env();
9886
+ init_inference_task_runner();
9329
9887
  init_isolation_audit();
9330
9888
  init_recovery_ledger();
9331
9889
  init_no_changes_terminal_status();