@sagentlab/navarch-runtime 0.1.116 → 0.1.117

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.
Files changed (51) hide show
  1. package/README.md +43 -17
  2. package/bin/muse-mcp-bridge.cjs +3 -0
  3. package/dist/adapters/index.cjs +6 -1
  4. package/dist/adapters/muse.cjs +433 -0
  5. package/dist/agent-auth.cjs +2 -1
  6. package/dist/config.cjs +8 -1
  7. package/dist/conversation/capability.cjs +1 -1
  8. package/dist/exit-conditions.cjs +1 -1
  9. package/dist/muse-mcp-bridge.cjs +167 -0
  10. package/dist/session.cjs +30 -8
  11. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  12. package/node_modules/@muse-code/sdk/README.md +88 -0
  13. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  14. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  15. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  16. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  17. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  18. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  19. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  20. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  21. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  22. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  23. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  37. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  38. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  39. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  40. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  41. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  45. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  46. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  47. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  48. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  49. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  50. package/node_modules/@muse-code/sdk/package.json +47 -0
  51. package/package.json +8 -4
package/README.md CHANGED
@@ -95,7 +95,8 @@ when the run finishes, so the task view can follow a connected run live.
95
95
  Live snapshots hold the current turn's stderr back until the turn ends.
96
96
 
97
97
  The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
98
- Antigravity CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
98
+ Antigravity CLI**, **OpenCode**, an **Agent Client Protocol (ACP)** agent, **Qoder**,
99
+ **Pi (local)**, or **Muse Code (experimental)** when connecting a worker. Any selected agent can
99
100
  run any unpinned project task; task eligibility depends on capabilities and
100
101
  project gates. A task pinned to a runtime waits for a worker that advertises
101
102
  that adapter and runs on it — see "Choosing an agent" below.
@@ -118,7 +119,8 @@ Before generating the command, prepare the machine:
118
119
  - install Node.js 20 or later (the standard installation includes `npm` and
119
120
  `npx`) and Git;
120
121
  - install the coding-agent CLI selected in Navarch — `claude`, `codex`,
121
- `agy`, `opencode`, `dsh` for the default ACP integration, or `qoder` — and complete its normal authentication flow; and
122
+ `agy`, `opencode`, `dsh` for the default ACP integration, `qoder`, `pi`,
123
+ or `muse` — and complete its normal authentication flow; and
122
124
  - for Docker-backed sessions, install and start Docker and use an image that
123
125
  contains the selected agent CLI and the repository toolchain, with provider
124
126
  credentials supplied through the approved machine or project configuration.
@@ -241,11 +243,11 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
241
243
 
242
244
  | Command | Purpose |
243
245
  |---|---|
244
- | `register --token <t> --name <n> [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
245
- | `connect --token <t> --name <n> [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
246
- | `start [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
247
- | `supervise [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
248
- | `doctor [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder]` | Prints resolved config + Docker/registration status; no side effects. |
246
+ | `register --token <t> --name <n> [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder\|muse\|pi-local] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
247
+ | `connect --token <t> --name <n> [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder\|muse\|pi-local] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
248
+ | `start [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder\|muse\|pi-local]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
249
+ | `supervise [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder\|muse\|pi-local]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
250
+ | `doctor [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder\|muse\|pi-local]` | Prints resolved config + Docker/registration status; no side effects. |
249
251
 
250
252
  ### Running multiple agents on one machine
251
253
 
@@ -459,7 +461,7 @@ unchanged across the deployment.
459
461
  | `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. |
460
462
  | `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. |
461
463
  | `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. |
462
- | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `agy`, `opencode`, `acp`, or `qoder`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
464
+ | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent adapter: `claude-code`, `codex`, `antigravity`, `opencode`, `acp`, `qoder`, `muse`, or `pi-local`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
463
465
  | `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. |
464
466
  | `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
465
467
  | `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`. |
@@ -475,8 +477,10 @@ unchanged across the deployment.
475
477
  | `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. |
476
478
  | `NAVARCH_QODER_BIN` | `qoder` | Path/name of the Qoder CLI binary. |
477
479
  | `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. |
480
+ | `NAVARCH_MUSE_BIN` | `muse` | Path/name of the native Muse Code CLI. Requires authenticated experimental `muse serve` SDK access; the npm SDK does not install the CLI. See [Muse Code setup](../docs/navarch/muse-code.md). |
481
+ | `NAVARCH_MUSE_EXTRA_ARGS` | — | Comma list of arguments passed to `muse serve`. Permission/sandbox bypass flags are rejected. Configure narrow native approval rules rather than enabling blanket approval. |
478
482
  | `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
479
- | `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. |
483
+ | `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 or Muse on an already-isolated host. |
480
484
  | `NAVARCH_NAVI_CONVERSATIONS` | on | Serve read-only Navi repository conversations on this machine. Only the `claude-code` adapter on the host sandbox with the worktree guard on is supported; `navarch-runtime doctor` prints the `navi_chat:` state and reason. Set `off` to stop polling for conversations. |
481
485
  | `NAVARCH_NAVI_WORKSPACE_CHAT` | off | Explicitly enable writable Navi chat on a supported Claude Code host runtime. Owners can choose **Edit and run commands**; automatic permission review and the worktree guard remain active. Commands run on this host, not in a hard sandbox. Changes stay under `<workspaceRoot>/chat-workspaces/<conversationId>/repo` until the operator removes them. See [workspace chat](../docs/navarch/navi-workspace-chat.md). |
482
486
  | `NAVARCH_CONVERSATION_POLL_MS` | `5000` | Idle poll interval for queued conversation turns (1000–30000). |
@@ -545,6 +549,18 @@ metadata directory.
545
549
  OpenCode receives a private, per-session config, project config discovery is
546
550
  disabled, and lease MCP headers are referenced through child-only environment
547
551
  variables instead of being copied into its config file or argv.
552
+ - **Muse Code (experimental):** guarded hosts do not advertise Muse; a
553
+ Muse-only guarded worker fails closed before claiming. Use a custom Docker
554
+ image with native Muse, its authentication, Node.js, and the matching Navarch
555
+ runtime's `navarch-muse-mcp-bridge` executable. Use a dedicated clean Muse
556
+ configuration with no unrelated MCP servers, plugins, or hooks: session MCP
557
+ is additive to native configuration. The default image contains
558
+ neither Muse nor its bridge. A host guard opt-out is an explicit operator
559
+ choice for an already-isolated worker and removes the boundary machine-wide;
560
+ generated Muse setup commands never make that choice. Muse uses
561
+ `denyUnmatched`, verified by a session readback, and rejects interactive
562
+ approval requests. Preconfigure narrow rules for required shell/write actions
563
+ and the actual Navarch MCP tool namespace. See [Muse Code setup](../docs/navarch/muse-code.md).
548
564
  - **ACP agents:** the protocol client rejects every unattended permission
549
565
  escalation. The agent remains responsible for enforcing its declared file
550
566
  sandbox; use Navarch Docker mode when the ACP server itself does not provide
@@ -643,10 +659,15 @@ export NAVARCH_AGENT=acp
643
659
  # `connect --model` can pin one explicitly.
644
660
  export NAVARCH_AGENT=qoder
645
661
 
646
- # Advanced compatibility mode: advertise every installed adapter. On a guarded
647
- # host, `opencode` and `antigravity` are dropped from this list (see the boundary notes above);
648
- # the rest are advertised as written.
649
- export NAVARCH_RUNTIMES=claude-code,codex,antigravity,opencode,acp,qoder
662
+ # Muse Code (experimental, runtime 0.1.117+) — see the dedicated setup
663
+ # guide before starting: native authenticated `muse serve`, narrow approval
664
+ # rules, clean native configuration, and deliberate isolation are required.
665
+ export NAVARCH_AGENT=muse
666
+
667
+ # Advanced compatibility mode: advertise every installed, configured adapter.
668
+ # On a guarded host, `opencode`, `antigravity` and `muse` are dropped (see the
669
+ # boundary notes above); the rest are advertised as written.
670
+ export NAVARCH_RUNTIMES=claude-code,codex,antigravity,opencode,acp,qoder,muse
650
671
  ```
651
672
 
652
673
  For the legacy single-runtime setting, priority is `start --agent` →
@@ -660,10 +681,10 @@ or `claude-code`-pinned tasks. List only adapters this worker can really run:
660
681
  in Docker mode that means CLIs installed in `NAVARCH_DOCKER_IMAGE`, otherwise
661
682
  pinned tasks for a missing adapter are claimed and then fail.
662
683
 
663
- All five BYO adapters implement the same `AgentAdapter` interface
664
- (`src/adapters/types.cts`) and run either directly on the host or via
665
- `docker exec` in the session's sandbox container, exactly like the Claude
666
- adapter always has — `session.cts` picks one (`src/adapters/index.cts`'s
684
+ The BYO adapters implement the same `AgentAdapter` interface
685
+ (`src/adapters/types.cts`). Supported isolation modes are adapter-specific;
686
+ Muse uses `muse serve` over stdio on an explicitly isolated host or through
687
+ `docker exec -i` in a prepared custom container — `session.cts` picks one (`src/adapters/index.cts`'s
667
688
  `selectAdapter`) at the start of each session and passes `agent_type`
668
689
  through to `complete()` unchanged by whatever happened during the run.
669
690
 
@@ -678,6 +699,10 @@ reference (the platform default is `opencode/x-preview-f-free`, Ox Alpha Free
678
699
  (Unlimited) on OpenCode Zen), or `default` to preserve the authenticated
679
700
  account's selection. ACP sessions use the agent's advertised default model and
680
701
  set the standard `reasoning_effort` configuration option when it is available.
702
+ Muse uses the account-owned `default` model; Navarch adds no Muse project-model
703
+ column or public model catalog. Its SDK integration is experimental and has no
704
+ Navi conversation capability. Read [Muse Code setup and limitations](../docs/navarch/muse-code.md)
705
+ before using private repositories, especially Muse Contributor data settings.
681
706
  Machine-wide extra arguments still configure other CLI behavior; dispatched
682
707
  model policy wins.
683
708
 
@@ -838,6 +863,7 @@ cli.cts
838
863
  - openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
839
864
  - acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
840
865
  - qoderAdapter (adapters/qoder.cts) — `qoder -p --output-format json < prompt`
866
+ - museAdapter (adapters/muse.cts) — experimental Muse Session Protocol via `muse serve`
841
867
  heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
842
868
  a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
843
869
  5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ require("../dist/muse-mcp-bridge.cjs").main();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runLocalPiAdapter = exports.localPiAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runAntigravityAdapter = exports.antigravityAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
3
+ exports.runMuseAdapter = exports.museAdapter = exports.runQoderAdapter = exports.qoderAdapter = exports.runAcpAdapter = exports.acpAdapter = exports.runPiAdapter = exports.piAdapter = exports.runLocalPiAdapter = exports.localPiAdapter = exports.runOpenCodeAdapter = exports.openCodeAdapter = exports.runGeminiAdapter = exports.geminiAdapter = exports.runAntigravityAdapter = exports.antigravityAdapter = 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; } });
@@ -29,6 +29,9 @@ Object.defineProperty(exports, "runAcpAdapter", { enumerable: true, get: functio
29
29
  const qoder_cjs_1 = require("./qoder.cjs");
30
30
  Object.defineProperty(exports, "qoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.qoderAdapter; } });
31
31
  Object.defineProperty(exports, "runQoderAdapter", { enumerable: true, get: function () { return qoder_cjs_1.runQoderAdapter; } });
32
+ const muse_cjs_1 = require("./muse.cjs");
33
+ Object.defineProperty(exports, "museAdapter", { enumerable: true, get: function () { return muse_cjs_1.museAdapter; } });
34
+ Object.defineProperty(exports, "runMuseAdapter", { enumerable: true, get: function () { return muse_cjs_1.runMuseAdapter; } });
32
35
  /**
33
36
  * Picks the AgentAdapter (adapters/types.cts) session.cts should run a
34
37
  * session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
@@ -50,6 +53,8 @@ function selectAdapter(agentType) {
50
53
  return acp_cjs_1.acpAdapter;
51
54
  case "qoder":
52
55
  return qoder_cjs_1.qoderAdapter;
56
+ case "muse":
57
+ return muse_cjs_1.museAdapter;
53
58
  case "claude-code":
54
59
  return claude_cjs_1.claudeCodeAdapter;
55
60
  case "pi-local":
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.museAdapter = exports.MUSE_HOST_GUARD_MESSAGE = void 0;
7
+ exports.runMuseAdapter = runMuseAdapter;
8
+ exports.sumMuseSessionCost = sumMuseSessionCost;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_crypto_1 = require("node:crypto");
13
+ const kill_agent_cjs_1 = require("../kill-agent.cjs");
14
+ const version_cjs_1 = require("../version.cjs");
15
+ const CANCEL_GRACE_MS = 2_000;
16
+ const SHUTDOWN_GRACE_MS = 30_000;
17
+ const TERMINATE_GRACE_MS = 2_000;
18
+ exports.MUSE_HOST_GUARD_MESSAGE = "Muse Code cannot yet prove Navarch's host worktree boundary. Use a custom " +
19
+ "NAVARCH_SANDBOX_MODE=docker image with Muse installed, or explicitly set " +
20
+ "NAVARCH_WORKTREE_GUARD=off only on an otherwise isolated machine.";
21
+ function record(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
23
+ }
24
+ function count(value) {
25
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
26
+ }
27
+ function deferred() {
28
+ let resolve;
29
+ const promise = new Promise((done) => { resolve = done; });
30
+ return { promise, resolve };
31
+ }
32
+ async function waitWithin(promise, ms) {
33
+ let timer;
34
+ await Promise.race([promise, new Promise((resolve) => { timer = setTimeout(resolve, ms); })]);
35
+ clearTimeout(timer);
36
+ }
37
+ /**
38
+ * Muse Session Protocol through the pinned official SDK's framed Connection.
39
+ * Own the child here (rather than the facade's private child) so Navarch can
40
+ * register its PID, stream raw output and kill its complete process group.
41
+ * The facade intentionally does not forward session-scoped MCP configuration.
42
+ * No `exec --json` fallback: its event contract is a different surface.
43
+ */
44
+ async function runMuseAdapter(options) {
45
+ const empty = { exitCode: null, timedOut: false, killedByLeaseLoss: false, stdout: "", stderr: "" };
46
+ if (options.signal?.aborted)
47
+ return { ...empty, killedByLeaseLoss: true };
48
+ if (!options.dockerExec && options.museHostGuard !== false)
49
+ throw new Error(exports.MUSE_HOST_GUARD_MESSAGE);
50
+ if (!options.cwd || !node_path_1.default.isAbsolute(options.cwd))
51
+ throw new Error("Muse requires an absolute session worktree.");
52
+ if (options.extraArgs.some((arg) => /^(--yolo|--disable-sandbox|--disable-approval)(=|$)/.test(arg))) {
53
+ throw new Error("Muse permission and sandbox bypass flags are not supported by Navarch.");
54
+ }
55
+ const sourceServers = await readMcpServers(options.mcpConfigPath, options.env);
56
+ const sdk = await import("@muse-code/sdk");
57
+ const bridge = await prepareBridge(sourceServers, options);
58
+ try {
59
+ return await runMuseProcess(options, bridge.servers, sdk);
60
+ }
61
+ finally {
62
+ if (bridge.dir)
63
+ await node_fs_1.promises.rm(bridge.dir, { recursive: true, force: true });
64
+ }
65
+ }
66
+ async function runMuseProcess(options, mcpServers, sdk) {
67
+ const child = spawnMuse(options);
68
+ child.stdout.setEncoding("utf8");
69
+ child.stderr.setEncoding("utf8");
70
+ child.stdin.on("error", () => undefined); // A dead host is handled by its exit/EOF.
71
+ let stdout = "";
72
+ let stderr = "";
73
+ let timedOut = false;
74
+ let killedByLeaseLoss = false;
75
+ let processCode = null;
76
+ let sessionId;
77
+ let turnId;
78
+ let durable = false;
79
+ let terminal;
80
+ let authError;
81
+ let protocolFailed = false;
82
+ const closed = deferred();
83
+ const stopped = deferred();
84
+ const completed = deferred();
85
+ const items = new Map();
86
+ const usage = new Map();
87
+ const pending = [];
88
+ let cancelling;
89
+ const diagnostic = (message) => { stderr += `${stderr ? "\n" : ""}${message}`; };
90
+ child.on("error", (error) => { diagnostic(`Muse process failed: ${error.message}`); closed.resolve(); });
91
+ child.on("exit", (code) => {
92
+ processCode = code;
93
+ // A descendant may keep stdio open after its root exits.
94
+ if (!options.dockerExec)
95
+ (0, kill_agent_cjs_1.killAgent)(child);
96
+ });
97
+ child.on("close", (code) => { processCode = code; closed.resolve(); });
98
+ child.stderr.on("data", (chunk) => {
99
+ stderr += chunk;
100
+ options.onActivity?.();
101
+ options.onOutput?.("stderr", chunk);
102
+ });
103
+ const connection = new sdk.Connection({
104
+ incoming: (async function* () {
105
+ for await (const chunk of child.stdout) {
106
+ const text = String(chunk);
107
+ stdout += text;
108
+ options.onActivity?.();
109
+ options.onOutput?.("stdout", text);
110
+ yield text;
111
+ }
112
+ })(),
113
+ write: (chunk) => new Promise((resolve, reject) => {
114
+ child.stdin.write(chunk, (error) => error ? reject(error) : resolve());
115
+ }),
116
+ });
117
+ const cancel = () => cancelling ??= (async () => {
118
+ if (sessionId && turnId) {
119
+ // Admission is not cancellation completion. Give the host a bounded
120
+ // chance to settle before killing; never keep a lost lease alive.
121
+ void connection.command("turn/cancel", { sessionId, turnId }, { maxAttempts: 1 }).catch(() => undefined);
122
+ await waitWithin(Promise.race([completed.promise, closed.promise]), CANCEL_GRACE_MS);
123
+ }
124
+ if (options.dockerExec) {
125
+ await options.dockerExec.runner.run("docker", ["kill", options.dockerExec.containerName], { timeoutMs: 10_000 }).catch(() => undefined);
126
+ }
127
+ (0, kill_agent_cjs_1.killAgent)(child);
128
+ })();
129
+ const stop = () => { stopped.resolve(); void cancel(); };
130
+ const onAbort = () => { killedByLeaseLoss = true; stop(); };
131
+ const timer = options.timeoutMs > 0 ? setTimeout(() => { timedOut = true; stop(); }, options.timeoutMs) : undefined;
132
+ options.signal?.addEventListener("abort", onAbort, { once: true });
133
+ if (options.signal?.aborted)
134
+ onAbort();
135
+ const consume = (notification) => {
136
+ const params = record(notification.params);
137
+ if (!params || params.sessionId !== sessionId)
138
+ return;
139
+ if (notification.method === "view/gap") {
140
+ protocolFailed = true;
141
+ diagnostic("Muse event stream has a gap; refusing an incomplete result.");
142
+ stop();
143
+ return;
144
+ }
145
+ if (notification.method === "item/completed") {
146
+ const item = record(params.item);
147
+ if (!item || item.turnId !== turnId || item.kind !== "agentMessage" || typeof item.itemId !== "string")
148
+ return;
149
+ const previous = items.get(item.itemId);
150
+ if (!previous || Number(item.revision) >= Number(previous.revision))
151
+ items.set(item.itemId, item);
152
+ }
153
+ else if (params.turnId === turnId && notification.method === "session/tokenUsage") {
154
+ if (typeof params.viewCursor === "string")
155
+ usage.set(params.viewCursor, params);
156
+ }
157
+ else if (params.turnId === turnId && notification.method === "turn/completed") {
158
+ terminal = params;
159
+ const error = record(params.error);
160
+ if (params.terminal === "failed" && error?.kind === "authRequired") {
161
+ authError = "Muse authentication is required. Open muse and run /login on this machine.";
162
+ }
163
+ completed.resolve();
164
+ }
165
+ };
166
+ connection.onNotification((notification) => {
167
+ if (!turnId) {
168
+ // Events can precede the acknowledgement. Do not assume commandId is
169
+ // turnId; use the authoritative ack, then drain in wire order.
170
+ if (pending.length >= 10_000) {
171
+ protocolFailed = true;
172
+ diagnostic("Muse sent too many events before acknowledging the turn.");
173
+ stop();
174
+ }
175
+ else
176
+ pending.push(notification);
177
+ }
178
+ else
179
+ consume(notification);
180
+ });
181
+ connection.onProtocolError(() => {
182
+ protocolFailed = true;
183
+ diagnostic("Muse sent an invalid MSP frame; refusing to continue.");
184
+ stop();
185
+ });
186
+ // Unattended agents cannot invent approval or answer interactive questions.
187
+ // denyUnmatched is also selected below; unexpected requests abort the turn.
188
+ connection.onServerRequest(async () => {
189
+ protocolFailed = true;
190
+ diagnostic("Muse requested interactive approval or input; operator action is required.");
191
+ stop();
192
+ throw new sdk.MspError({ code: -32601, message: "Navarch cannot approve interactive requests", data: { kind: "methodNotFound", retryable: false } });
193
+ });
194
+ try {
195
+ options.onProcessStarted?.(child.pid);
196
+ const run = async () => {
197
+ const initialized = await connection.request("initialize", {
198
+ clientInfo: { name: "navarch_runtime", version: version_cjs_1.RUNTIME_VERSION },
199
+ capabilities: { requestedCapabilities: Object.keys(mcpServers).length ? ["sessionMcp"] : [], userInputDialogs: false },
200
+ });
201
+ const schema = record(initialized.schema);
202
+ if (typeof schema?.fingerprint !== "string")
203
+ throw new Error("Muse initialization did not provide a schema fingerprint.");
204
+ if (sdk.checkServedFingerprint(schema.fingerprint))
205
+ diagnostic("Muse schema fingerprint differs from the pinned SDK; unknown fields are tolerated.");
206
+ if (Object.keys(mcpServers).length && (!Array.isArray(initialized.grantedCapabilities) || !initialized.grantedCapabilities.includes("sessionMcp"))) {
207
+ throw new Error("Muse did not grant sessionMcp; the task cannot run without its lease-scoped tools.");
208
+ }
209
+ durable = initialized.sessionDurability === undefined || initialized.sessionDurability === "durable";
210
+ connection.notify("initialized");
211
+ await connection.flush();
212
+ if (timedOut || killedByLeaseLoss || protocolFailed)
213
+ return;
214
+ const config = Object.keys(mcpServers).length ? { mcpServers } : undefined;
215
+ const opened = options.resumeSessionId
216
+ ? await connection.command("session/resume", { sessionId: options.resumeSessionId, excludeItems: true, ...(config ? { config } : {}) }, { maxAttempts: 1 })
217
+ : await connection.command("session/start", {
218
+ workspaceRoot: options.cwd,
219
+ approvalMode: "denyUnmatched",
220
+ ...(options.model && options.model !== "default" ? { modelId: options.model } : {}),
221
+ ...(config ? { config } : {}),
222
+ }, { maxAttempts: 1 });
223
+ const session = record(opened.session);
224
+ if (typeof session?.sessionId !== "string" || typeof session.workspaceRoot !== "string"
225
+ || !node_path_1.default.isAbsolute(session.workspaceRoot) || await node_fs_1.promises.realpath(session.workspaceRoot) !== await node_fs_1.promises.realpath(options.cwd)
226
+ || session.activeTurnId !== null) {
227
+ throw new Error("Muse returned an unexpected session identity, worktree, or active turn.");
228
+ }
229
+ if (options.resumeSessionId && session.sessionId !== options.resumeSessionId)
230
+ throw new Error("Muse resumed a different session.");
231
+ sessionId = session.sessionId;
232
+ if (options.resumeSessionId) {
233
+ if (!durable)
234
+ throw new Error("Muse cannot resume this session on an ephemeral host.");
235
+ if (record(opened.history)?.mode !== "none")
236
+ throw new Error("Muse resume returned unexpected history; refusing replay.");
237
+ await connection.command("session/setApprovalMode", { sessionId, mode: "denyUnmatched" }, { maxAttempts: 1 });
238
+ if (options.model && options.model !== "default" && session.modelId !== options.model) {
239
+ throw new Error("Muse resumed with a different model than the dispatched policy.");
240
+ }
241
+ }
242
+ // An older host may ignore additive fields. Prove the selected approval
243
+ // posture before sending any model-visible prompt.
244
+ let approvalMode = options.resumeSessionId ? undefined : record(session.approvalMode)?.mode;
245
+ if (approvalMode !== "denyUnmatched" && durable) {
246
+ const state = await connection.request("session/read", { sessionId, excludeItems: true });
247
+ approvalMode = record(record(state.session)?.approvalMode)?.mode;
248
+ }
249
+ if (approvalMode !== "denyUnmatched") {
250
+ throw new Error("Muse did not confirm denyUnmatched approval mode; refusing to start a turn.");
251
+ }
252
+ if (timedOut || killedByLeaseLoss || protocolFailed)
253
+ return;
254
+ const ack = await connection.command("turn/start", {
255
+ sessionId,
256
+ input: [{ type: "text", text: options.prompt }],
257
+ ...(options.reasoningEffort ? { reasoningEffort: options.reasoningEffort } : {}),
258
+ }, { maxAttempts: 1 });
259
+ if (typeof ack.turnId !== "string" || ack.disposition !== "started")
260
+ throw new Error("Muse did not start the requested turn.");
261
+ turnId = ack.turnId;
262
+ for (const notification of pending.splice(0))
263
+ consume(notification);
264
+ await Promise.race([completed.promise, connection.closed]);
265
+ if (!terminal && !timedOut && !killedByLeaseLoss)
266
+ throw new Error("Muse closed before the turn completed.");
267
+ };
268
+ await Promise.race([run(), stopped.promise]);
269
+ }
270
+ catch (error) {
271
+ if (error instanceof sdk.MspError && error.kind === "authRequired") {
272
+ authError = "Muse authentication is required. Open muse and run /login on this machine.";
273
+ }
274
+ if (!timedOut && !killedByLeaseLoss)
275
+ diagnostic(error instanceof Error ? error.message : "Muse protocol request failed.");
276
+ }
277
+ finally {
278
+ await cancelling;
279
+ // EOF releases Muse's writer lease. A bounded shutdown always reaps the
280
+ // child, including a host that ignores EOF or died during a request.
281
+ child.stdin.end();
282
+ await waitWithin(Promise.race([closed.promise, stopped.promise]), SHUTDOWN_GRACE_MS);
283
+ await cancelling;
284
+ if (child.exitCode === null && child.signalCode === null) {
285
+ (0, kill_agent_cjs_1.killAgent)(child, "SIGTERM");
286
+ await waitWithin(closed.promise, TERMINATE_GRACE_MS);
287
+ if (child.exitCode === null && child.signalCode === null)
288
+ (0, kill_agent_cjs_1.killAgent)(child);
289
+ if (options.dockerExec)
290
+ await options.dockerExec.runner.run("docker", ["kill", options.dockerExec.containerName], { timeoutMs: 10_000 }).catch(() => undefined);
291
+ await waitWithin(closed.promise, TERMINATE_GRACE_MS);
292
+ }
293
+ // The root may have exited while detached shell/MCP descendants remain.
294
+ // Reap our owned host process group even after a clean root exit.
295
+ if (!options.dockerExec)
296
+ (0, kill_agent_cjs_1.killAgent)(child);
297
+ await waitWithin(closed.promise, TERMINATE_GRACE_MS);
298
+ await connection.close().catch(() => undefined);
299
+ clearTimeout(timer);
300
+ options.signal?.removeEventListener("abort", onAbort);
301
+ }
302
+ if (processCode === 5)
303
+ diagnostic("This Muse build has the experimental SDK/serve surface disabled. No CLI fallback was attempted.");
304
+ const successful = !protocolFailed && terminal?.terminal === "completed" && processCode === 0;
305
+ if (terminal && terminal.terminal !== "completed" && !timedOut && !killedByLeaseLoss && !authError) {
306
+ diagnostic(`Muse turn did not complete successfully (${String(terminal.terminal)}).`);
307
+ }
308
+ const reportText = [...items.values()].flatMap((item) => typeof item.text === "string" ? [item.text] : []).join("\n").trim();
309
+ return {
310
+ exitCode: timedOut || killedByLeaseLoss ? null : successful ? 0 : processCode === null ? null : 1,
311
+ timedOut, killedByLeaseLoss, stdout, stderr,
312
+ ...(reportText ? { reportText } : {}),
313
+ ...(authError ? { authError } : {}),
314
+ ...(durable && sessionId && terminal ? { agentSessionId: sessionId } : {}),
315
+ ...normalizedUsage([...usage.values()], !options.resumeSessionId),
316
+ };
317
+ }
318
+ function normalizedUsage(events, fresh) {
319
+ if (!events.length)
320
+ return {};
321
+ let tokensIn = 0;
322
+ let tokensOut = 0;
323
+ let cacheHitTokensIn = 0;
324
+ let completeCache = true;
325
+ for (const event of events) {
326
+ const usage = record(event.usage);
327
+ const input = count(event.promptTokens);
328
+ const output = count(usage?.outputTokens);
329
+ if (input === undefined || output === undefined)
330
+ return {}; // No fabricated partial total.
331
+ tokensIn += input;
332
+ tokensOut += output;
333
+ const cache = count(usage?.cacheReadTokens);
334
+ if (cache === undefined || cache > input)
335
+ completeCache = false;
336
+ else
337
+ cacheHitTokensIn += cache;
338
+ }
339
+ if (!Number.isSafeInteger(tokensIn) || !Number.isSafeInteger(tokensOut))
340
+ return {};
341
+ const cost = record(record(events.at(-1)?.cumulative)?.cost);
342
+ return {
343
+ tokensIn, tokensOut,
344
+ ...(completeCache ? { cacheHitTokensIn } : {}),
345
+ ...(cost?.partial === false && typeof cost.usd === "number" && Number.isFinite(cost.usd) && cost.usd >= 0
346
+ ? { sessionCostUsd: cost.usd, ...(fresh ? { costUsd: cost.usd } : {}) } : {}),
347
+ };
348
+ }
349
+ /** Replace each provider session's cumulative estimate, never sum it per turn. */
350
+ function sumMuseSessionCost(attempts) {
351
+ const sessions = new Map();
352
+ attempts.forEach((attempt, index) => sessions.set(attempt.agentSessionId ?? `attempt:${index}`, attempt.sessionCostUsd));
353
+ if (!sessions.size || [...sessions.values()].some((cost) => cost === undefined))
354
+ return undefined;
355
+ return [...sessions.values()].reduce((sum, cost) => sum + cost, 0);
356
+ }
357
+ async function prepareBridge(source, options) {
358
+ if (!Object.keys(source).length)
359
+ return { servers: {} };
360
+ if (!options.mcpMetadataDir || !node_path_1.default.isAbsolute(options.mcpMetadataDir))
361
+ throw new Error("Muse MCP needs a task-owned metadata directory.");
362
+ const dir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(options.mcpMetadataDir, "muse-mcp-"));
363
+ try {
364
+ const configPath = node_path_1.default.join(dir, "servers.json");
365
+ const content = JSON.stringify(source);
366
+ await node_fs_1.promises.writeFile(configPath, content, { mode: 0o600 });
367
+ const digest = (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
368
+ const servers = Object.fromEntries(Object.keys(source).map((name) => [name, {
369
+ transport: "stdio", framing: "lineDelimitedJson", mode: "required",
370
+ command: options.dockerExec ? "navarch-muse-mcp-bridge" : process.execPath,
371
+ args: [...(options.dockerExec ? [] : [node_path_1.default.resolve(__dirname, "../../bin/muse-mcp-bridge.cjs")]), configPath, name, digest],
372
+ }]));
373
+ return { servers, dir };
374
+ }
375
+ catch (error) {
376
+ await node_fs_1.promises.rm(dir, { recursive: true, force: true });
377
+ throw error;
378
+ }
379
+ }
380
+ async function readMcpServers(configPath, env) {
381
+ if (!configPath)
382
+ return {};
383
+ const parsed = record(JSON.parse(await node_fs_1.promises.readFile(configPath, "utf8")));
384
+ const source = record(parsed?.mcpServers);
385
+ if (!source)
386
+ throw new Error("Muse MCP configuration must contain mcpServers.");
387
+ const result = Object.create(null);
388
+ const strings = (value) => {
389
+ const entries = value === undefined ? {} : record(value);
390
+ if (!entries)
391
+ throw new Error("Muse MCP environment/headers must be string maps.");
392
+ return Object.fromEntries(Object.entries(entries).map(([key, raw]) => {
393
+ if (typeof raw !== "string")
394
+ throw new Error("Muse MCP environment/headers must be string maps.");
395
+ const name = raw.match(/^\{env:([A-Z_][A-Z0-9_]*)\}$/)?.[1];
396
+ const value = name ? env[name] ?? process.env[name] : raw;
397
+ if (value === undefined)
398
+ throw new Error(`Muse MCP configuration references missing environment variable ${name}.`);
399
+ return [key, value];
400
+ }));
401
+ };
402
+ for (const [name, value] of Object.entries(source)) {
403
+ const server = record(value);
404
+ if (!server)
405
+ throw new Error("Muse MCP server configuration is invalid.");
406
+ const url = server.httpUrl ?? server.url;
407
+ if (typeof url === "string" && /^https?:\/\//.test(url) && (!server.type || ["http", "streamable-http"].includes(String(server.type)))) {
408
+ result[name] = { transport: "streamableHttp", url, headers: strings(server.headers), mode: "required" };
409
+ }
410
+ else if (typeof server.command === "string" && (!server.type || server.type === "stdio") && (!server.args || Array.isArray(server.args) && server.args.every((arg) => typeof arg === "string"))) {
411
+ result[name] = { transport: "stdio", command: server.command, args: server.args, env: strings(server.env), mode: "required" };
412
+ }
413
+ else
414
+ throw new Error("Muse MCP server transport is unsupported; no servers were silently dropped.");
415
+ }
416
+ return result;
417
+ }
418
+ function spawnMuse(options) {
419
+ const env = { ...process.env, ...options.env };
420
+ delete env.MUSE_PERMISSION_PROFILE_FAIL_OPEN;
421
+ const args = ["serve", ...options.extraArgs];
422
+ if (!options.dockerExec)
423
+ return (0, node_child_process_1.spawn)(options.bin, args, { cwd: options.cwd, env, detached: process.platform !== "win32", stdio: "pipe" });
424
+ return (0, node_child_process_1.spawn)("docker", [
425
+ "exec", "-i", ...Object.keys(options.env).filter((name) => name !== "MUSE_PERMISSION_PROFILE_FAIL_OPEN").flatMap((name) => ["--env", name]),
426
+ options.dockerExec.containerName, "sh", "-c",
427
+ '[ ! -f /tmp/session.env ] || . /tmp/session.env; unset MUSE_PERMISSION_PROFILE_FAIL_OPEN; cd "$1" || exit 1; shift; exec "$@"',
428
+ "sh", options.cwd, options.bin, ...args,
429
+ ], { env, detached: process.platform !== "win32", stdio: "pipe" });
430
+ }
431
+ // Read-only Navi conversations are deliberately absent: an approval mode is
432
+ // not a filesystem read boundary, and the SDK cannot yet prove that contract.
433
+ exports.museAdapter = { agentType: "muse", run: runMuseAdapter };
@@ -43,7 +43,8 @@ function matchingLine(text) {
43
43
  function detectAgentAuthFailure(result) {
44
44
  if (result.killedByLeaseLoss || result.timedOut || result.capacityError)
45
45
  return null;
46
- if (result.authError)
46
+ // Muse exposes authRequired as a typed terminal error, never assistant prose.
47
+ if (result.authError && result.exitCode !== 0)
47
48
  return { detail: result.authError };
48
49
  const claude = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ?? (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
49
50
  if (claude) {
package/dist/config.cjs CHANGED
@@ -10,6 +10,7 @@ exports.opencodeBlockedOnHost = opencodeBlockedOnHost;
10
10
  exports.loadRuntimeConfig = loadRuntimeConfig;
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
12
  const node_os_1 = __importDefault(require("node:os"));
13
+ const muse_cjs_1 = require("./adapters/muse.cjs");
13
14
  const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
14
15
  /** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
15
16
  exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
@@ -25,6 +26,7 @@ exports.RUNTIME_AGENT_TYPES = [
25
26
  "opencode",
26
27
  "acp",
27
28
  "qoder",
29
+ "muse",
28
30
  "pi-local",
29
31
  ];
30
32
  function isRuntimeAgentType(value) {
@@ -116,10 +118,13 @@ function loadRuntimeConfig(env = process.env) {
116
118
  ? sandboxMode === "host" && !worktreeGuard
117
119
  : runtime === "antigravity"
118
120
  ? process.platform !== "win32" && !opencodeBlockedOnHost(sandboxMode, worktreeGuard)
119
- : runtime !== "opencode" || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
121
+ : (runtime !== "opencode" && runtime !== "muse") || !opencodeBlockedOnHost(sandboxMode, worktreeGuard));
120
122
  if (requestedRuntimes.includes("pi-local") && runnableRuntimes.length === 0) {
121
123
  throw new Error("Pi (local) requires NAVARCH_SANDBOX_MODE=host and NAVARCH_WORKTREE_GUARD=off");
122
124
  }
125
+ if (requestedRuntimes.includes("muse") && runnableRuntimes.length === 0) {
126
+ throw new Error(muse_cjs_1.MUSE_HOST_GUARD_MESSAGE);
127
+ }
123
128
  if (requestedRuntimes.includes("antigravity") && runnableRuntimes.length === 0) {
124
129
  if (process.platform === "win32")
125
130
  throw new Error("Antigravity's private lease MCP relay requires a Linux or macOS worker; Windows is unsupported.");
@@ -168,6 +173,8 @@ function loadRuntimeConfig(env = process.env) {
168
173
  piExtraArgs: envList(env, "NAVARCH_PI_EXTRA_ARGS", []),
169
174
  qoderBin: env.NAVARCH_QODER_BIN ?? "qoder",
170
175
  qoderExtraArgs: envList(env, "NAVARCH_QODER_EXTRA_ARGS", []),
176
+ museBin: env.NAVARCH_MUSE_BIN ?? "muse",
177
+ museExtraArgs: envList(env, "NAVARCH_MUSE_EXTRA_ARGS", []),
171
178
  gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
172
179
  gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
173
180
  mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,