@sagentlab/navarch-runtime 0.1.52 → 0.1.53

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
@@ -152,10 +152,10 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
152
152
 
153
153
  | Command | Purpose |
154
154
  |---|---|
155
- | `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
156
- | `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
157
- | `start [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
158
- | `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
155
+ | `register --token <t> --name <n> [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
156
+ | `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. |
157
+ | `start [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
158
+ | `supervise [--agent claude-code\|codex\|gemini\|opencode\|acp\|qoder]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
159
159
  | `doctor` | Prints resolved config + Docker/registration status; no side effects. |
160
160
 
161
161
  ### Running multiple agents on one machine
@@ -368,7 +368,7 @@ unchanged across the deployment.
368
368
  | `NAVARCH_SANDBOX_PROFILE` | `trusted-development` | Named security profile for Docker sessions (`src/sandbox-profile.cts`): `trusted-development` (image-default user, uncapped, open egress), `untrusted-code` (non-root, 2 CPU / 4g / 512 PIDs, deny-by-default egress, read-only shared git), `elevated-verification` (non-root, 4 CPU / 8g / 2048 PIDs, egress limited to GitHub plus package registries). The default is the exact pre-profile flag set. |
369
369
  | `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. |
370
370
  | `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. |
371
- | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `gemini`, `opencode`, or `acp`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
371
+ | `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. |
372
372
  | `NAVARCH_RUNTIMES` | resolved local agent choice | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
373
373
  | `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
374
374
  | `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`. |
@@ -382,6 +382,8 @@ unchanged across the deployment.
382
382
  | `NAVARCH_OPENCODE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `run`, the prompt, and the generated `--format json` argument. Explicit `--format`, `--model`, or `--variant` values replace the corresponding per-session default. |
383
383
  | `NAVARCH_ACP_BIN` | `dsh` | Path/name of an Agent Client Protocol v1 stdio server. DeepSeek Harness is the default implementation. |
384
384
  | `NAVARCH_ACP_EXTRA_ARGS` | `--profile,acp` | Comma list of arguments used to start the ACP server. Override this together with `NAVARCH_ACP_BIN` for another ACP-compatible coding agent. |
385
+ | `NAVARCH_QODER_BIN` | `qoder` | Path/name of the Qoder CLI binary. |
386
+ | `NAVARCH_QODER_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the unattended defaults (`--setting-sources`, `--permission-mode auto`, `--output-format json`). An explicit `--output-format`, `--permission-mode`, `--settings`, `--model`, or `--reasoning-effort` here replaces the per-session default. Qoder selects its own account models; pin one via `--model` here or `connect --model` to override. |
385
387
  | `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
386
388
  | `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable it for every runtime on the machine — required to run OpenCode on the host. |
387
389
  | `NAVARCH_GUARD_EXTRA_ROOTS` | — | `path.delimiter`-separated (`:` on POSIX) extra directories the worktree guard allows beyond the session worktree, shared bare repo, and temp dirs. |
@@ -514,10 +516,15 @@ export NAVARCH_AGENT=opencode
514
516
  # Override NAVARCH_ACP_BIN / NAVARCH_ACP_EXTRA_ARGS for another ACP v1 server.
515
517
  export NAVARCH_AGENT=acp
516
518
 
519
+ # Qoder — requires an authenticated `qoder` CLI (or NAVARCH_QODER_BIN pointing
520
+ # at it). Model selection is account-owned; NAVARCH_QODER_EXTRA_ARGS or
521
+ # `connect --model` can pin one explicitly.
522
+ export NAVARCH_AGENT=qoder
523
+
517
524
  # Advanced compatibility mode: advertise every installed adapter. On a guarded
518
525
  # host, `opencode` is dropped from this list (see the boundary notes above);
519
526
  # the rest are advertised as written.
520
- export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp
527
+ export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp,qoder
521
528
  ```
522
529
 
523
530
  For the legacy single-runtime setting, priority is `start --agent` →
@@ -642,6 +649,7 @@ cli.cts
642
649
  - geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
643
650
  - openCodeAdapter (adapters/opencode.cts) — `opencode run <prompt> --format json`
644
651
  - acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
652
+ - qoderAdapter (adapters/qoder.cts) — `qoder -p <prompt> --output-format json`
645
653
  heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
646
654
  a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
647
655
  5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
3
+ exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
4
4
  exports.selectAdapter = selectAdapter;
5
5
  const claude_cjs_1 = require("./claude.cjs");
6
6
  Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
@@ -20,6 +20,9 @@ Object.defineProperty(exports, "runPiAdapter", { enumerable: true, get: function
20
20
  const acp_cjs_1 = require("./acp.cjs");
21
21
  Object.defineProperty(exports, "acpAdapter", { enumerable: true, get: function () { return acp_cjs_1.acpAdapter; } });
22
22
  Object.defineProperty(exports, "runAcpAdapter", { enumerable: true, get: function () { return acp_cjs_1.runAcpAdapter; } });
23
+ const qoder_cjs_1 = require("./qoder.cjs");
24
+ Object.defineProperty(exports, "qoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.qoderAdapter; } });
25
+ Object.defineProperty(exports, "runQoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.runQoderAdapter; } });
23
26
  /**
24
27
  * Picks the AgentAdapter (adapters/types.cts) session.cts should run a
25
28
  * session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
@@ -37,6 +40,8 @@ function selectAdapter(agentType) {
37
40
  return opencode_cjs_1.openCodeAdapter;
38
41
  case "acp":
39
42
  return acp_cjs_1.acpAdapter;
43
+ case "qoder":
44
+ return qoder_cjs_1.qoderAdapter;
40
45
  case "claude-code":
41
46
  return claude_cjs_1.claudeCodeAdapter;
42
47
  case "pi":
@@ -41,8 +41,11 @@ async function runOpenCodeAdapter(options) {
41
41
  args.push("--format", "json");
42
42
  args.push(...options.extraArgs);
43
43
  if (options.model && options.model !== "default" && !hasArg(options.extraArgs, "--model", "-m")) {
44
- // OpenCode V2 selects a reasoning variant through the model reference
45
- // (`provider/model#variant`) rather than a separate flag.
44
+ // OpenCode V2 folds the reasoning variant into the model reference
45
+ // (`provider/model#variant`) and hard-fails on a variant the model does
46
+ // not advertise. The claim route clamps the effort to the model's
47
+ // advertised variants, so a present value is always valid; absent means
48
+ // use the model's own default.
46
49
  args.push("--model", options.reasoningEffort ? `${options.model}#${options.reasoningEffort}` : options.model);
47
50
  }
48
51
  const runOptions = { ...options, env };
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.qoderAdapter = void 0;
4
+ exports.runQoderAdapter = runQoderAdapter;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
7
+ const kill_agent_cjs_1 = require("../kill-agent.cjs");
8
+ function hasFlag(args, flag) {
9
+ return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
10
+ }
11
+ /**
12
+ * Headless Qoder CLI adapter: `qoder -p "<prompt>" --output-format json`.
13
+ * Qoder's print mode emits the same single-JSON result shape as Claude Code
14
+ * (verified against qodercli 1.1.56), so usage/cost parsing reuses
15
+ * parseClaudeJsonResult and its result text lands in the ordinary
16
+ * exit-conditions path.
17
+ *
18
+ * Model selection is account-owned (`qoder --list-models`), so the claim-time
19
+ * `"default"` sentinel (execution-policy.ts / session.cts) is passed through
20
+ * as no `--model` flag; an explicit pin from `--model` at enroll or
21
+ * NAVARCH_QODER_EXTRA_ARGS still reaches the CLI.
22
+ */
23
+ async function runQoderAdapter(options) {
24
+ if (options.signal?.aborted) {
25
+ return { exitCode: null, timedOut: false, killedByLeaseLoss: true, stdout: "", stderr: "" };
26
+ }
27
+ const args = ["-p", options.prompt];
28
+ // Runtime sessions must not inherit an operator's personal or project-local
29
+ // Qoder hooks, for the same reason as the Claude adapter: they make
30
+ // execution machine-dependent and can fail otherwise-good sessions. The
31
+ // empty source list still permits an explicit --settings file below.
32
+ if (!hasFlag(options.extraArgs, "--setting-sources")) {
33
+ args.push("--setting-sources", "");
34
+ }
35
+ // Unattended sessions route permission decisions through Qoder's native
36
+ // auto classifier rather than prompting or bypassing checks.
37
+ if (!["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => hasFlag(options.extraArgs, flag))) {
38
+ args.push("--permission-mode", "auto");
39
+ }
40
+ if (options.settingsPath && !hasFlag(options.extraArgs, "--settings")) {
41
+ args.push("--settings", options.settingsPath);
42
+ }
43
+ if (options.mcpConfigPath) {
44
+ args.push("--mcp-config", options.mcpConfigPath);
45
+ }
46
+ if (!hasFlag(options.extraArgs, "--output-format")) {
47
+ args.push("--output-format", "json");
48
+ }
49
+ args.push(...options.extraArgs);
50
+ if (options.model && options.model !== "default" && !hasFlag(options.extraArgs, "--model")) {
51
+ args.push("--model", options.model);
52
+ }
53
+ if (options.reasoningEffort && !hasFlag(options.extraArgs, "--reasoning-effort")) {
54
+ args.push("--reasoning-effort", options.reasoningEffort);
55
+ }
56
+ const raw = options.dockerExec ? await runViaDocker(options, args) : await runOnHost(options, args);
57
+ return attachUsage(raw);
58
+ }
59
+ /** Parses stdout for `qoder -p --output-format json` usage and folds it onto the raw result (best-effort). */
60
+ function attachUsage(result) {
61
+ const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout);
62
+ if (!parsed)
63
+ return result;
64
+ const usage = (0, exit_conditions_cjs_1.extractUsageFromClaudeJson)(parsed);
65
+ return {
66
+ ...result,
67
+ tokensIn: usage.tokensIn,
68
+ tokensOut: usage.tokensOut,
69
+ cacheHitTokensIn: usage.cacheHitTokensIn,
70
+ costUsd: usage.costUsd,
71
+ };
72
+ }
73
+ async function runOnHost(options, args) {
74
+ return new Promise((resolve) => {
75
+ let stdout = "";
76
+ let stderr = "";
77
+ let timedOut = false;
78
+ let killedByLeaseLoss = false;
79
+ const child = (0, node_child_process_1.spawn)(options.bin, args, {
80
+ detached: process.platform !== "win32",
81
+ cwd: options.cwd,
82
+ env: { ...process.env, ...options.env },
83
+ });
84
+ // `qoder -p` checks stdin for a piped prompt; close it so the child does
85
+ // not wait when the complete prompt was supplied as an argument.
86
+ child.stdin?.end();
87
+ options.onProcessStarted?.(child.pid);
88
+ const timer = options.timeoutMs > 0 ? setTimeout(() => {
89
+ timedOut = true;
90
+ (0, kill_agent_cjs_1.killAgent)(child);
91
+ }, options.timeoutMs) : undefined;
92
+ const onAbort = () => {
93
+ killedByLeaseLoss = true;
94
+ (0, kill_agent_cjs_1.killAgent)(child);
95
+ };
96
+ options.signal?.addEventListener("abort", onAbort, { once: true });
97
+ if (options.signal?.aborted)
98
+ onAbort();
99
+ child.stdout.on("data", (d) => {
100
+ options.onActivity?.();
101
+ stdout += d.toString();
102
+ });
103
+ child.stderr.on("data", (d) => {
104
+ options.onActivity?.();
105
+ stderr += d.toString();
106
+ });
107
+ child.on("error", (err) => {
108
+ clearTimeout(timer);
109
+ options.signal?.removeEventListener("abort", onAbort);
110
+ stderr += `\n${String(err)}`;
111
+ resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr });
112
+ });
113
+ child.on("close", (code) => {
114
+ clearTimeout(timer);
115
+ options.signal?.removeEventListener("abort", onAbort);
116
+ resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr });
117
+ });
118
+ });
119
+ }
120
+ async function runViaDocker(options, args) {
121
+ const { containerName, runner } = options.dockerExec;
122
+ const quoted = [options.bin, ...args].map(shellQuote).join(" ");
123
+ const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${quoted}`;
124
+ let killedByLeaseLoss = false;
125
+ const onAbort = () => {
126
+ killedByLeaseLoss = true;
127
+ runner.run("docker", ["kill", containerName], { timeoutMs: 10_000 }).catch(() => undefined);
128
+ };
129
+ options.signal?.addEventListener("abort", onAbort, { once: true });
130
+ if (options.signal?.aborted)
131
+ onAbort();
132
+ try {
133
+ const result = await runner.run("docker", ["exec", containerName, "sh", "-c", command], {
134
+ timeoutMs: options.timeoutMs,
135
+ onActivity: options.onActivity,
136
+ signal: options.signal,
137
+ });
138
+ return {
139
+ exitCode: result.code,
140
+ timedOut: false,
141
+ killedByLeaseLoss,
142
+ stdout: result.stdout,
143
+ stderr: result.stderr,
144
+ };
145
+ }
146
+ catch (err) {
147
+ return {
148
+ exitCode: null,
149
+ timedOut: false,
150
+ killedByLeaseLoss,
151
+ stdout: "",
152
+ stderr: String(err),
153
+ };
154
+ }
155
+ finally {
156
+ options.signal?.removeEventListener("abort", onAbort);
157
+ }
158
+ }
159
+ function shellQuote(value) {
160
+ return `'${value.replace(/'/g, `'\\''`)}'`;
161
+ }
162
+ /** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=qoder. */
163
+ exports.qoderAdapter = {
164
+ agentType: "qoder",
165
+ run: runQoderAdapter,
166
+ };
package/dist/cli.cjs CHANGED
@@ -78,7 +78,7 @@ function agentFromFlag(flags) {
78
78
  if (value === undefined)
79
79
  return undefined;
80
80
  if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
81
- throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', or 'acp'.");
81
+ throw new Error("--agent must be one of 'claude-code', 'codex', 'gemini', 'opencode', 'acp', or 'qoder'.");
82
82
  }
83
83
  return value;
84
84
  }
package/dist/config.cjs CHANGED
@@ -17,7 +17,8 @@ function isRuntimeAgentType(value) {
17
17
  value === "codex" ||
18
18
  value === "gemini" ||
19
19
  value === "opencode" ||
20
- value === "acp");
20
+ value === "acp" ||
21
+ value === "qoder");
21
22
  }
22
23
  function envInt(env, name, fallback) {
23
24
  const raw = env[name];
@@ -123,6 +124,8 @@ function loadRuntimeConfig(env = process.env) {
123
124
  opencodeExtraArgs: envList(env, "NAVARCH_OPENCODE_EXTRA_ARGS", []),
124
125
  acpBin: env.NAVARCH_ACP_BIN ?? "dsh",
125
126
  acpExtraArgs: envList(env, "NAVARCH_ACP_EXTRA_ARGS", ["--profile", "acp"]),
127
+ qoderBin: env.NAVARCH_QODER_BIN ?? "qoder",
128
+ qoderExtraArgs: envList(env, "NAVARCH_QODER_EXTRA_ARGS", []),
126
129
  gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
127
130
  gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
128
131
  mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
@@ -52,7 +52,8 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
52
52
  process.env.NAVARCH_AGENT === "claude-code" ||
53
53
  process.env.NAVARCH_AGENT === "gemini" ||
54
54
  process.env.NAVARCH_AGENT === "opencode" ||
55
- process.env.NAVARCH_AGENT === "acp"
55
+ process.env.NAVARCH_AGENT === "acp" ||
56
+ process.env.NAVARCH_AGENT === "qoder"
56
57
  ? process.env.NAVARCH_AGENT
57
58
  : undefined,
58
59
  };
package/dist/session.cjs CHANGED
@@ -44,7 +44,7 @@ function resolveSessionExecution(config, claimed) {
44
44
  profile: claimed.task.execution_profile ?? "standard",
45
45
  model: runtime === "codex" ? "gpt-6-astra"
46
46
  : runtime === "gemini" ? "auto"
47
- : runtime === "opencode" || runtime === "acp" ? "default"
47
+ : runtime === "opencode" || runtime === "acp" || runtime === "qoder" ? "default"
48
48
  : "claude-opus-5",
49
49
  reasoning_effort: "medium",
50
50
  };
@@ -763,6 +763,8 @@ function adapterCommand(config, runtime) {
763
763
  return { bin: config.opencodeBin, extraArgs: config.opencodeExtraArgs };
764
764
  case "acp":
765
765
  return { bin: config.acpBin, extraArgs: config.acpExtraArgs };
766
+ case "qoder":
767
+ return { bin: config.qoderBin, extraArgs: config.qoderExtraArgs };
766
768
  }
767
769
  }
768
770
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
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",