@sagentlab/navarch-runtime 0.1.112 → 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 (56) hide show
  1. package/README.md +56 -18
  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/cli.cjs +4 -1
  7. package/dist/config.cjs +9 -1
  8. package/dist/conversation/capability.cjs +1 -1
  9. package/dist/conversation/loop.cjs +16 -0
  10. package/dist/conversation/mirror.cjs +225 -0
  11. package/dist/conversation/workspace.cjs +11 -4
  12. package/dist/exit-conditions.cjs +1 -1
  13. package/dist/muse-mcp-bridge.cjs +167 -0
  14. package/dist/session.cjs +30 -8
  15. package/dist/worktree-guard.cjs +137 -3
  16. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  17. package/node_modules/@muse-code/sdk/README.md +88 -0
  18. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  19. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  20. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  21. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  22. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  23. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  37. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  38. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  39. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  40. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  41. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  45. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  46. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  47. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  48. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  49. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  50. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  51. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  52. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  53. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  54. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  55. package/node_modules/@muse-code/sdk/package.json +47 -0
  56. package/package.json +8 -4
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # navarch-runtime
2
2
 
3
+ Conversation mirror isolation and lifecycle fixes are included in runtime 0.1.115.
4
+
3
5
  ## Managed visual evidence
4
6
 
5
7
  Host-mode build, verify, and QA leases requiring `browser-use` receive a
@@ -93,7 +95,8 @@ when the run finishes, so the task view can follow a connected run live.
93
95
  Live snapshots hold the current turn's stderr back until the turn ends.
94
96
 
95
97
  The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
96
- 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
97
100
  run any unpinned project task; task eligibility depends on capabilities and
98
101
  project gates. A task pinned to a runtime waits for a worker that advertises
99
102
  that adapter and runs on it — see "Choosing an agent" below.
@@ -116,7 +119,8 @@ Before generating the command, prepare the machine:
116
119
  - install Node.js 20 or later (the standard installation includes `npm` and
117
120
  `npx`) and Git;
118
121
  - install the coding-agent CLI selected in Navarch — `claude`, `codex`,
119
- `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
120
124
  - for Docker-backed sessions, install and start Docker and use an image that
121
125
  contains the selected agent CLI and the repository toolchain, with provider
122
126
  credentials supplied through the approved machine or project configuration.
@@ -239,11 +243,11 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
239
243
 
240
244
  | Command | Purpose |
241
245
  |---|---|
242
- | `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. |
243
- | `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. |
244
- | `start [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
245
- | `supervise [--agent claude-code\|codex\|antigravity\|opencode\|acp\|qoder]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
246
- | `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. |
247
251
 
248
252
  ### Running multiple agents on one machine
249
253
 
@@ -457,7 +461,7 @@ unchanged across the deployment.
457
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. |
458
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. |
459
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. |
460
- | `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. |
461
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. |
462
466
  | `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
463
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`. |
@@ -473,11 +477,14 @@ unchanged across the deployment.
473
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. |
474
478
  | `NAVARCH_QODER_BIN` | `qoder` | Path/name of the Qoder CLI binary. |
475
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. |
476
482
  | `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
477
- | `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. |
478
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. |
479
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). |
480
486
  | `NAVARCH_CONVERSATION_POLL_MS` | `5000` | Idle poll interval for queued conversation turns (1000–30000). |
487
+ | `NAVARCH_CONVERSATION_MIRROR` | on | Seed new conversation checkouts from a per-repository mirror under `<workspaceRoot>/conversation-mirrors/` (fetch + local clone with independent object files) instead of a full network clone. Set `off` to always clone directly. |
481
488
  | `NAVARCH_CONVERSATION_TURN_TIMEOUT_MS` | `540000` | Time limit for one conversation turn's agent process (at most nine minutes). |
482
489
  | `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. |
483
490
 
@@ -542,6 +549,18 @@ metadata directory.
542
549
  OpenCode receives a private, per-session config, project config discovery is
543
550
  disabled, and lease MCP headers are referenced through child-only environment
544
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).
545
564
  - **ACP agents:** the protocol client rejects every unattended permission
546
565
  escalation. The agent remains responsible for enforcing its declared file
547
566
  sandbox; use Navarch Docker mode when the ACP server itself does not provide
@@ -578,7 +597,16 @@ Because bubblewrap starts from an empty `/` and Codex re-executes its own binary
578
597
  inside it, the profile also grants read access to the Codex executable's
579
598
  install directory (resolved from `NAVARCH_CODEX_BIN` or `PATH`, following
580
599
  symlinks) plus mise and nvm tool installs, so a Codex or Node installed under
581
- `$HOME` still starts. `$HOME` and `CODEX_HOME` themselves are never granted.
600
+ `$HOME` still starts. Codex also keeps per-run `codex-linux-sandbox` helper links in
601
+ `$CODEX_HOME/tmp/arg0` and re-executes them by name inside that view; without a
602
+ read grant on that one directory every sandboxed command fails with
603
+ `bwrap: execvp codex-linux-sandbox: No such file or directory` (reproduced on
604
+ Codex 0.158.0, 0.160.0, and 0.162.1). For hoisted npm installs the sibling
605
+ `@openai/codex-<platform>` package holding the native binary is granted too.
606
+ `$HOME` and `CODEX_HOME` themselves are never granted. Helper-directory aliases
607
+ and unresolvable paths are refused; native-package grants require the official
608
+ package layout, and canonical workspace overlap is rejected even through aliases.
609
+ These checks assume the operator trusts the installed CLI/package contents.
582
610
  To verify the linked-worktree boundary on a Linux host with Codex installed,
583
611
  run `NAVARCH_TEST_REAL_CODEX=1 npm test -- tests/codex-linux-sandbox.test.cts`
584
612
  from `runtime/`. The smoke test uses local shell commands and makes no model calls.
@@ -631,10 +659,15 @@ export NAVARCH_AGENT=acp
631
659
  # `connect --model` can pin one explicitly.
632
660
  export NAVARCH_AGENT=qoder
633
661
 
634
- # Advanced compatibility mode: advertise every installed adapter. On a guarded
635
- # host, `opencode` and `antigravity` are dropped from this list (see the boundary notes above);
636
- # the rest are advertised as written.
637
- 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
638
671
  ```
639
672
 
640
673
  For the legacy single-runtime setting, priority is `start --agent` →
@@ -648,10 +681,10 @@ or `claude-code`-pinned tasks. List only adapters this worker can really run:
648
681
  in Docker mode that means CLIs installed in `NAVARCH_DOCKER_IMAGE`, otherwise
649
682
  pinned tasks for a missing adapter are claimed and then fail.
650
683
 
651
- All five BYO adapters implement the same `AgentAdapter` interface
652
- (`src/adapters/types.cts`) and run either directly on the host or via
653
- `docker exec` in the session's sandbox container, exactly like the Claude
654
- 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
655
688
  `selectAdapter`) at the start of each session and passes `agent_type`
656
689
  through to `complete()` unchanged by whatever happened during the run.
657
690
 
@@ -666,6 +699,10 @@ reference (the platform default is `opencode/x-preview-f-free`, Ox Alpha Free
666
699
  (Unlimited) on OpenCode Zen), or `default` to preserve the authenticated
667
700
  account's selection. ACP sessions use the agent's advertised default model and
668
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.
669
706
  Machine-wide extra arguments still configure other CLI behavior; dispatched
670
707
  model policy wins.
671
708
 
@@ -826,6 +863,7 @@ cli.cts
826
863
  - openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
827
864
  - acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
828
865
  - qoderAdapter (adapters/qoder.cts) — `qoder -p --output-format json < prompt`
866
+ - museAdapter (adapters/muse.cts) — experimental Muse Session Protocol via `muse serve`
829
867
  heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
830
868
  a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
831
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) {