@sagentlab/navarch-runtime 0.1.62 → 0.1.64

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 CHANGED
@@ -30,7 +30,7 @@ Before generating the command, prepare the machine:
30
30
  - install Node.js 20 or later (the standard installation includes `npm` and
31
31
  `npx`) and Git;
32
32
  - install the coding-agent CLI selected in Navarch — `claude`, `codex`,
33
- `gemini`, `opencode`, or `dsh` for the default ACP integration — and complete its normal authentication flow; and
33
+ `gemini`, `opencode`, `dsh` for the default ACP integration, or `qoder` — and complete its normal authentication flow; and
34
34
  - for Docker-backed sessions, install and start Docker and use an image that
35
35
  contains the selected agent CLI and the repository toolchain, with provider
36
36
  credentials supplied through the approved machine or project configuration.
@@ -45,7 +45,7 @@ node --version # 20 or later
45
45
  npm --version
46
46
  npx --version
47
47
  git --version
48
- codex --version # or: claude / gemini / opencode / dsh --version
48
+ codex --version # or: claude / gemini / opencode / qoder / dsh --version
49
49
  ```
50
50
 
51
51
  Every command above must succeed before the long-running worker starts. See
@@ -157,7 +157,7 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
157
157
  | `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
158
158
  | `start [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
159
159
  | `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
160
- | `doctor` | Prints resolved config + Docker/registration status; no side effects. |
160
+ | `doctor [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Prints resolved config + Docker/registration status; no side effects. |
161
161
 
162
162
  ### Running multiple agents on one machine
163
163
 
@@ -370,7 +370,7 @@ unchanged across the deployment.
370
370
  | `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
371
371
  | `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
372
372
  | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, `acp`, or `qoder`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
373
- | `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. Unpinned tasks run on the preferred agent; a task runtime pin selects the matching adapter. |
373
+ | `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. Unpinned tasks run on the preferred agent; a task runtime pin selects the matching adapter. Unrecognized ids are dropped with a warning that names them, so a typo cannot silently shrink the advertised set. |
374
374
  | `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
375
375
  | `NAVARCH_AUTO_UPDATE` | on under `supervise` | Set `off`, `false`, or `0` to report releases without staging or activating them. Automatic activation is always off under plain `start`. |
376
376
  | `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
package/dist/cli.cjs CHANGED
@@ -20,6 +20,8 @@ const supervisor_cjs_1 = require("./supervisor.cjs");
20
20
  const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
21
21
  const log = (0, logger_cjs_1.createLogger)("cli");
22
22
  const PACKAGE_NAME = "@sagentlab/navarch-runtime";
23
+ /** Accepted `--agent` ids as usage metavariables, generated from one source of truth. */
24
+ const AGENT_TYPE_USAGE = config_cjs_1.RUNTIME_AGENT_TYPES.join("|");
23
25
  /**
24
26
  * Exit code for "this machine's identity is gone". Deliberately outside the
25
27
  * supervisor's restart codes (75 update, 76 remote restart): respawning cannot
@@ -78,10 +80,32 @@ function agentFromFlag(flags) {
78
80
  if (value === undefined)
79
81
  return undefined;
80
82
  if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
81
- throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', 'acp', or 'qoder'.");
83
+ throw new Error(`--agent must be one of ${formatChoices(config_cjs_1.RUNTIME_AGENT_TYPES)}.`);
82
84
  }
83
85
  return value;
84
86
  }
87
+ /** `'a', 'b', or 'c'` — one prose list for every place the accepted ids are shown. */
88
+ function formatChoices(values) {
89
+ const quoted = values.map((value) => `'${value}'`);
90
+ const last = quoted.at(-1) ?? "";
91
+ if (quoted.length < 2)
92
+ return last;
93
+ return `${quoted.slice(0, -1).join(", ")}, or ${last}`;
94
+ }
95
+ /**
96
+ * A typo in NAVARCH_RUNTIMES must not silently shrink the advertised adapter
97
+ * set, so name every dropped id on stderr. Runs once per top-level command (not
98
+ * per config load) to avoid duplicate lines when start/supervise/doctor resolve
99
+ * the identity and reload config. A supervised worker inherits NAVARCH_RUNTIMES
100
+ * from the `supervise` parent that already warned, so it stays quiet.
101
+ */
102
+ function warnUnknownRuntimes(config) {
103
+ const unknown = config.unknownRuntimes;
104
+ if (config.supervised || unknown.length === 0)
105
+ return;
106
+ log.warn(`NAVARCH_RUNTIMES ignores unrecognized adapter id(s): ${unknown.join(", ")}. ` +
107
+ `Known adapters: ${config_cjs_1.RUNTIME_AGENT_TYPES.join(", ")}.`);
108
+ }
85
109
  /** Saved pins only apply to the adapter they were selected for. */
86
110
  function modelFromFlags(flags, agentType, identity) {
87
111
  const model = flags.model ?? (identity?.agent_type === agentType ? identity.model : undefined);
@@ -116,6 +140,7 @@ function parseArgs(argv) {
116
140
  */
117
141
  async function registerCommand(flags) {
118
142
  const config = configFromFlags(flags);
143
+ warnUnknownRuntimes(config);
119
144
  const apiBase = flags["api-base"] ?? config.apiBase;
120
145
  const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
121
146
  const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
@@ -166,6 +191,7 @@ async function registerCommand(flags) {
166
191
  */
167
192
  async function connectCommand(flags) {
168
193
  const config = configFromFlags(flags);
194
+ warnUnknownRuntimes(config);
169
195
  const apiBase = flags["api-base"] ?? config.apiBase;
170
196
  const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
171
197
  const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
@@ -209,6 +235,7 @@ async function startCommand(flags) {
209
235
  const baseConfig = configFromFlags(flags);
210
236
  const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
211
237
  const config = configFromFlags(flags, identity);
238
+ warnUnknownRuntimes(config);
212
239
  const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
213
240
  const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
214
241
  const runtimeShutdown = new AbortController();
@@ -287,6 +314,7 @@ async function superviseCommand(flags) {
287
314
  const baseConfig = configFromFlags(flags);
288
315
  const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
289
316
  const config = configFromFlags(flags, identity);
317
+ warnUnknownRuntimes(config);
290
318
  // Resolve and pin the adapter before spawning the worker. The supervisor
291
319
  // passes machine credentials through the environment, so the child no
292
320
  // longer reads machine.json for identity fields (including agent_type).
@@ -325,8 +353,15 @@ async function doctorCommand(flags) {
325
353
  }
326
354
  if (identity)
327
355
  config = configFromFlags(flags, identity);
356
+ warnUnknownRuntimes(config);
328
357
  }
329
358
  catch (err) {
359
+ // The typo may be why the config is unusable (e.g. `opencode,gemni` on a
360
+ // guarded host leaves only OpenCode), so still name it.
361
+ warnUnknownRuntimes({
362
+ supervised: process.env.NAVARCH_SUPERVISED === "1",
363
+ unknownRuntimes: (0, config_cjs_1.unknownRuntimeIds)(process.env),
364
+ });
330
365
  console.log(`config: UNUSABLE — ${err instanceof Error ? err.message : String(err)}`);
331
366
  process.exitCode = 1;
332
367
  return;
@@ -353,12 +388,12 @@ function helpText() {
353
388
 
354
389
  Usage:
355
390
  navarch-runtime register --token <enrollment-token> --name <machine-name> \\
356
- [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
391
+ [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
357
392
  navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
358
- [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
359
- navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>]
360
- navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>]
361
- navarch-runtime doctor [--config-dir <path>]
393
+ [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
394
+ navarch-runtime start [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>]
395
+ navarch-runtime supervise [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}] [--model <model>]
396
+ navarch-runtime doctor [--config-dir <path>] [--agent ${AGENT_TYPE_USAGE}]
362
397
  navarch-runtime code-graph --query <symbol-or-file> [--mode search|callers|callees|impact] [--commit HEAD] [--depth 1..5] [--repo <path>]
363
398
 
364
399
  Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
package/dist/config.cjs CHANGED
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OPENCODE_HOST_GUARD_MESSAGE = exports.DEFAULT_SANDBOX_IMAGE = void 0;
6
+ exports.OPENCODE_HOST_GUARD_MESSAGE = exports.RUNTIME_AGENT_TYPES = exports.DEFAULT_SANDBOX_IMAGE = void 0;
7
7
  exports.isRuntimeAgentType = isRuntimeAgentType;
8
+ exports.unknownRuntimeIds = unknownRuntimeIds;
8
9
  exports.opencodeBlockedOnHost = opencodeBlockedOnHost;
9
10
  exports.loadRuntimeConfig = loadRuntimeConfig;
10
11
  const node_path_1 = __importDefault(require("node:path"));
@@ -12,13 +13,26 @@ const node_os_1 = __importDefault(require("node:os"));
12
13
  const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
13
14
  /** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
14
15
  exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
16
+ /**
17
+ * Adapter ids the runtime accepts for `NAVARCH_AGENT` / `--agent`, in display
18
+ * order. `NAVARCH_RUNTIMES` is validated against this same list, and the CLI
19
+ * usage/error copy is generated from it so the advertised set cannot drift.
20
+ */
21
+ exports.RUNTIME_AGENT_TYPES = [
22
+ "claude-code",
23
+ "codex",
24
+ "gemini",
25
+ "opencode",
26
+ "acp",
27
+ "qoder",
28
+ ];
15
29
  function isRuntimeAgentType(value) {
16
- return (value === "claude-code" ||
17
- value === "codex" ||
18
- value === "gemini" ||
19
- value === "opencode" ||
20
- value === "acp" ||
21
- value === "qoder");
30
+ return typeof value === "string" && exports.RUNTIME_AGENT_TYPES.includes(value);
31
+ }
32
+ /** Unrecognized ids in NAVARCH_RUNTIMES, deduplicated, in the order given. */
33
+ function unknownRuntimeIds(env) {
34
+ const ids = envList(env, "NAVARCH_RUNTIMES", []);
35
+ return [...new Set(ids.filter((runtime) => !isRuntimeAgentType(runtime)))];
22
36
  }
23
37
  function envInt(env, name, fallback) {
24
38
  const raw = env[name];
@@ -82,7 +96,11 @@ function loadRuntimeConfig(env = process.env) {
82
96
  // Multiple sessions share one machine; keeping each agent inside its own
83
97
  // worktree is the safe default, so disabling is the explicit opt-out.
84
98
  const worktreeGuard = !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? "");
85
- const requestedRuntimes = envList(env, "NAVARCH_RUNTIMES", [requestedAgentType]).filter(isRuntimeAgentType);
99
+ const requestedRuntimeIds = envList(env, "NAVARCH_RUNTIMES", [requestedAgentType]);
100
+ // Keep unknown ids visible so a typo surfaces (CLI warns) instead of the
101
+ // machine quietly shrinking the adapters it advertises.
102
+ const unknownRuntimes = unknownRuntimeIds(env);
103
+ const requestedRuntimes = requestedRuntimeIds.filter(isRuntimeAgentType);
86
104
  // OpenCode's CLI cannot express the host worktree boundary, so its adapter
87
105
  // fails closed there (adapters/opencode.cts). Advertising it anyway meant
88
106
  // the machine claimed OpenCode tasks it could only fail, one lease at a
@@ -114,6 +132,7 @@ function loadRuntimeConfig(env = process.env) {
114
132
  worktreeStaleAfterMs: envInt(env, "NAVARCH_WORKTREE_STALE_AFTER_MS", 24 * 60 * 60 * 1000),
115
133
  agentType,
116
134
  runtimes: runtimes.length > 0 ? runtimes : [agentType],
135
+ unknownRuntimes,
117
136
  claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
118
137
  claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
119
138
  codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
@@ -8,6 +8,7 @@ exports.loadMachineIdentity = loadMachineIdentity;
8
8
  exports.resolveMachineIdentity = resolveMachineIdentity;
9
9
  const node_fs_1 = require("node:fs");
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const config_cjs_1 = require("./config.cjs");
11
12
  function storePath(configDir) {
12
13
  return node_path_1.default.join(configDir, "machine.json");
13
14
  }
@@ -48,12 +49,10 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
48
49
  token: envToken,
49
50
  name: process.env.NAVARCH_MACHINE_NAME ?? envId,
50
51
  api_base: process.env.NAVARCH_API_BASE ?? apiBaseFallback,
51
- agent_type: process.env.NAVARCH_AGENT === "codex" ||
52
- process.env.NAVARCH_AGENT === "claude-code" ||
53
- process.env.NAVARCH_AGENT === "gemini" ||
54
- process.env.NAVARCH_AGENT === "opencode" ||
55
- process.env.NAVARCH_AGENT === "acp" ||
56
- process.env.NAVARCH_AGENT === "qoder"
52
+ // Validate against the one accepted adapter list so a new adapter cannot
53
+ // be added to `--agent`/NAVARCH_AGENT while this env-based identity path
54
+ // silently drops it.
55
+ agent_type: (0, config_cjs_1.isRuntimeAgentType)(process.env.NAVARCH_AGENT)
57
56
  ? process.env.NAVARCH_AGENT
58
57
  : undefined,
59
58
  };
package/dist/session.cjs CHANGED
@@ -94,7 +94,7 @@ async function runSession(deps, claimed, sessionId) {
94
94
  await api
95
95
  .completeLease(leaseId, {
96
96
  status: "failed",
97
- report: verificationFailureReport(task.task_type, failureSummary, "crashed"),
97
+ report: failureSummary,
98
98
  failure_summary: failureSummary,
99
99
  failed_before_start: true,
100
100
  evidence_urls: [],
@@ -174,7 +174,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
174
174
  const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
175
175
  await api.completeLease(leaseId, {
176
176
  status: "failed",
177
- report: verificationFailureReport(task.task_type, failureSummary, "crashed"),
177
+ report: failureSummary,
178
178
  failure_summary: failureSummary,
179
179
  failed_before_start: true,
180
180
  evidence_urls: [],
@@ -296,7 +296,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
296
296
  await api
297
297
  .completeLease(leaseId, {
298
298
  status: "failed",
299
- report: verificationFailureReport(task.task_type, "Docker sandbox unavailable on this machine.", "crashed"),
299
+ report: "Docker sandbox unavailable on this machine.",
300
300
  failure_summary: "Docker sandbox unavailable on this machine.",
301
301
  failed_before_start: true,
302
302
  evidence_urls: [],
@@ -634,9 +634,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
634
634
  const redactedReport = (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets);
635
635
  const completion = {
636
636
  status: mapping.leaseOutcome,
637
- report: mapping.leaseOutcome === "failed"
638
- ? verificationFailureReport(task.task_type, redactedReport, mapping.exitStatus)
639
- : redactedReport,
637
+ report: redactedReport,
640
638
  evidence_urls: mapping.evidenceUrls,
641
639
  ...(deliveryFailureSummary
642
640
  ? { failure_summary: (0, redact_cjs_1.redactText)(deliveryFailureSummary, knownSecrets) }
@@ -698,9 +696,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
698
696
  await api.completeLease(leaseId, {
699
697
  ...completion,
700
698
  status: "failed",
701
- report: verificationFailureReport(task.task_type, remediable
699
+ report: remediable
702
700
  ? redactedRejection
703
- : `${redactedRejection}\n\n---\n\n${completion.report}`, "failed"),
701
+ : `${redactedRejection}\n\n---\n\n${completion.report}`,
704
702
  failure_summary: redactedRejection,
705
703
  exit_status: "failed",
706
704
  });
@@ -732,9 +730,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
732
730
  await api
733
731
  .completeLease(leaseId, {
734
732
  status: "failed",
735
- report: verificationFailureReport(task.task_type, lastCompletion
733
+ report: lastCompletion
736
734
  ? `${lastCompletion.report}\n\n---\n\n${failureSummary}`
737
- : failureSummary, "crashed"),
735
+ : failureSummary,
738
736
  failure_summary: failureSummary,
739
737
  failed_before_start: !adapterStarted,
740
738
  evidence_urls: lastCompletion?.evidence_urls ?? [],
@@ -815,33 +813,6 @@ function sumReportedUsage(attempts, key) {
815
813
  });
816
814
  return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
817
815
  }
818
- /**
819
- * The control plane refuses to release a verify lease whose failed completion
820
- * does not open with `Verification verdict: FAIL` or `BLOCKED`
821
- * (verification_verdict_invalid), and that gate applies to runtime-authored
822
- * failure reports too. When the report carries no usable verdict — the agent
823
- * exited non-zero, or opened with PASS despite the failed outcome — label it
824
- * BLOCKED: the failure prevented an acceptance decision. Without this, the
825
- * failure completion is itself rejected, the session crashes, and the lease
826
- * dangles until expiry.
827
- *
828
- * A `killed` or `crashed` exit is left unlabelled: the control plane accepts
829
- * those verdict-less as the runtime's own failure and routes them through
830
- * failure attribution (an infrastructure retry). A BLOCKED label there would
831
- * misreport a transient timeout as a verifier verdict and escalate it to a
832
- * human; the control plane discards such a label, so posting it only muddies
833
- * the session report.
834
- */
835
- function verificationFailureReport(taskType, report, exitStatus) {
836
- if (taskType !== "verify")
837
- return report;
838
- if (exitStatus === "killed" || exitStatus === "crashed")
839
- return report;
840
- const verdict = (0, exit_conditions_cjs_1.leadingVerificationVerdict)(report);
841
- if (verdict === "fail" || verdict === "blocked")
842
- return report;
843
- return `Verification verdict: BLOCKED — the session ended before verification reached an acceptance decision.\n\n${report}`;
844
- }
845
816
  /** Rejection codes another agent turn in the same worktree can plausibly fix. */
846
817
  const REMEDIABLE_REJECTION_CODES = new Set([
847
818
  "review_fix_required",
@@ -871,7 +842,7 @@ const REMEDIABLE_REJECTION_CODES = new Set([
871
842
  // `Verification verdict:` line only has to restate its report; the
872
843
  // rejection prose spells out the exact format. Failed outcomes never reach
873
844
  // remediation (see the leaseOutcome === "completed" gate) — their reports
874
- // are verdict-labelled by verificationFailureReport before posting.
845
+ // are recorded as session failures when the verifier supplied no verdict.
875
846
  "verification_verdict_invalid",
876
847
  ]);
877
848
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",