@sagentlab/navarch-runtime 0.1.107 → 0.1.111
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 +97 -59
- package/dist/adapters/antigravity-mcp.cjs +221 -0
- package/dist/adapters/antigravity.cjs +348 -0
- package/dist/adapters/gemini.cjs +7 -322
- package/dist/adapters/index.cjs +6 -1
- package/dist/agent-auth.cjs +2 -0
- package/dist/config.cjs +18 -3
- package/dist/conversation/capability.cjs +1 -1
- package/dist/exit-conditions.cjs +1 -1
- package/dist/session.cjs +30 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Host-mode build, verify, and QA leases requiring `browser-use` receive a
|
|
6
6
|
`navarch_browser` MCP server with `capture_visual_evidence` in Codex, Claude,
|
|
7
|
-
|
|
7
|
+
Antigravity, OpenCode, and ACP sessions. It runs Chromium in the runtime process's
|
|
8
8
|
environment, outside the coding agent's OS sandbox, with a fresh browser
|
|
9
9
|
context for desktop and touch-enabled mobile captures. It neither uses the
|
|
10
10
|
operator's browser profile nor executes agent-provided scripts.
|
|
@@ -93,7 +93,7 @@ when the run finishes, so the task view can follow a connected run live.
|
|
|
93
93
|
Live snapshots hold the current turn's stderr back until the turn ends.
|
|
94
94
|
|
|
95
95
|
The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
|
|
96
|
-
|
|
96
|
+
Antigravity CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
|
|
97
97
|
run any unpinned project task; task eligibility depends on capabilities and
|
|
98
98
|
project gates. A task pinned to a runtime waits for a worker that advertises
|
|
99
99
|
that adapter and runs on it — see "Choosing an agent" below.
|
|
@@ -116,7 +116,7 @@ Before generating the command, prepare the machine:
|
|
|
116
116
|
- install Node.js 20 or later (the standard installation includes `npm` and
|
|
117
117
|
`npx`) and Git;
|
|
118
118
|
- install the coding-agent CLI selected in Navarch — `claude`, `codex`,
|
|
119
|
-
`
|
|
119
|
+
`agy`, `opencode`, `dsh` for the default ACP integration, or `qoder` — and complete its normal authentication flow; and
|
|
120
120
|
- for Docker-backed sessions, install and start Docker and use an image that
|
|
121
121
|
contains the selected agent CLI and the repository toolchain, with provider
|
|
122
122
|
credentials supplied through the approved machine or project configuration.
|
|
@@ -131,7 +131,7 @@ node --version # 20 or later
|
|
|
131
131
|
npm --version
|
|
132
132
|
npx --version
|
|
133
133
|
git --version
|
|
134
|
-
codex --version # or: claude /
|
|
134
|
+
codex --version # or: claude / agy / opencode / qoder / dsh --version
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
Every command above must succeed before the long-running worker starts. See
|
|
@@ -239,11 +239,11 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
239
239
|
|
|
240
240
|
| Command | Purpose |
|
|
241
241
|
|---|---|
|
|
242
|
-
| `register --token <t> --name <n> [--agent claude-code\|codex\|
|
|
243
|
-
| `connect --token <t> --name <n> [--agent claude-code\|codex\|
|
|
244
|
-
| `start [--agent claude-code\|codex\|
|
|
245
|
-
| `supervise [--agent claude-code\|codex\|
|
|
246
|
-
| `doctor [--agent claude-code\|codex\|
|
|
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. |
|
|
247
247
|
|
|
248
248
|
### Running multiple agents on one machine
|
|
249
249
|
|
|
@@ -457,7 +457,7 @@ unchanged across the deployment.
|
|
|
457
457
|
| `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
458
|
| `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
459
|
| `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`, `
|
|
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. |
|
|
461
461
|
| `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
462
|
| `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
|
|
463
463
|
| `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`. |
|
|
@@ -465,8 +465,8 @@ unchanged across the deployment.
|
|
|
465
465
|
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). `--allowedTools`/`--disallowedTools` layer rules onto auto mode; an explicit `--permission-mode`, `--permission-prompt-tool`, or bypass flag replaces the unattended default `--permission-mode auto`. Runtime sessions default to an empty `--setting-sources` list so machine/user/project hooks cannot leak into temporary checkouts. The generated host-mode settings retain only the user-level `apiKeyHelper` needed for authentication; supply `--setting-sources=<sources>` here to opt into other settings deliberately. |
|
|
466
466
|
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
467
467
|
| `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP `-c` overrides and `--json` (Codex). |
|
|
468
|
-
| `
|
|
469
|
-
| `
|
|
468
|
+
| `NAVARCH_ANTIGRAVITY_BIN` | `agy` | Path/name of the Antigravity CLI binary, version 1.3.1+. |
|
|
469
|
+
| `NAVARCH_ANTIGRAVITY_EXTRA_ARGS` | — | Comma list: only `--agent`, `--model`, `--effort`, `--sandbox`, `--dangerously-skip-permissions`; protocol, deadline and resume flags are owned by Navarch. |
|
|
470
470
|
| `NAVARCH_OPENCODE_BIN` | `opencode` | Path/name of the OpenCode CLI binary. |
|
|
471
471
|
| `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. |
|
|
472
472
|
| `NAVARCH_ACP_BIN` | `dsh` | Path/name of an Agent Client Protocol v1 stdio server. DeepSeek Harness is the default implementation. |
|
|
@@ -484,15 +484,14 @@ unchanged across the deployment.
|
|
|
484
484
|
## Worktree boundary guard (host mode)
|
|
485
485
|
|
|
486
486
|
A machine typically runs several sessions concurrently (`NAVARCH_MAX_SESSIONS`),
|
|
487
|
-
each in its own git worktree. The runtime enforces
|
|
488
|
-
|
|
487
|
+
each in its own git worktree. The runtime enforces adapter-specific boundaries where verified; unsupported guarded paths fail
|
|
488
|
+
closed rather than claim equivalent isolation:
|
|
489
489
|
|
|
490
490
|
Every adapter receives an empty, session-owned `GH_CONFIG_DIR` and, when the
|
|
491
491
|
project supplies GitHub credentials, the lease-scoped `GITHUB_TOKEN`. This
|
|
492
492
|
keeps `gh` independent of the operator's selected account and prevents tasks
|
|
493
493
|
from reading unrelated GitHub credentials. Docker already mounts the session
|
|
494
|
-
metadata directory
|
|
495
|
-
as an explicit read-only mount.
|
|
494
|
+
metadata directory.
|
|
496
495
|
|
|
497
496
|
- **Claude Code:** the runtime uses `--permission-mode auto`, which sends
|
|
498
497
|
approval decisions through Claude Code's background safety classifier. It
|
|
@@ -525,12 +524,13 @@ as an explicit read-only mount.
|
|
|
525
524
|
the same write roots. Without these, iOS builds and simulator tests fail
|
|
526
525
|
under the sandbox with
|
|
527
526
|
`access to /Library/Developer/PrivateFrameworks/CoreSimulator.framework is blocked`.
|
|
528
|
-
- **
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
527
|
+
- **Antigravity:** guarded host execution and read-only Navi are unsupported.
|
|
528
|
+
Use an isolated Docker image containing authenticated `agy` 1.3.1+ and Node,
|
|
529
|
+
or explicitly disable the machine-wide guard only on a dedicated trusted
|
|
530
|
+
worker. Antigravity's global MCP servers, permissions, plugins and hooks
|
|
531
|
+
remain active: the lease relay is not an exclusive tool boundary. Navarch
|
|
532
|
+
does not automatically add `--dangerously-skip-permissions`; preconfigure
|
|
533
|
+
scoped permissions or explicitly opt into that flag on an isolated worker.
|
|
534
534
|
- **OpenCode:** the CLI cannot currently express a host-side boundary for all
|
|
535
535
|
shell side effects, so with the guard enabled it is resolved out of the
|
|
536
536
|
machine's advertised runtimes at startup — and a machine that offers only
|
|
@@ -611,9 +611,10 @@ export NAVARCH_AGENT=claude-code
|
|
|
611
611
|
# Claude Code prerequisite above.
|
|
612
612
|
export NAVARCH_AGENT=codex
|
|
613
613
|
|
|
614
|
-
# Google
|
|
615
|
-
#
|
|
616
|
-
|
|
614
|
+
# Google Antigravity CLI — requires authenticated agy 1.3.1+.
|
|
615
|
+
# Use Docker with agy + Node installed, or an explicit guard opt-out only
|
|
616
|
+
# on a dedicated trusted worker. Gemini auth/config is not transferred.
|
|
617
|
+
export NAVARCH_AGENT=antigravity
|
|
617
618
|
|
|
618
619
|
# OpenCode — requires an authenticated `opencode` CLI (or
|
|
619
620
|
# NAVARCH_OPENCODE_BIN pointing at it). A guarded host refuses to start with
|
|
@@ -631,16 +632,16 @@ export NAVARCH_AGENT=acp
|
|
|
631
632
|
export NAVARCH_AGENT=qoder
|
|
632
633
|
|
|
633
634
|
# Advanced compatibility mode: advertise every installed adapter. On a guarded
|
|
634
|
-
# host, `opencode`
|
|
635
|
+
# host, `opencode` and `antigravity` are dropped from this list (see the boundary notes above);
|
|
635
636
|
# the rest are advertised as written.
|
|
636
|
-
export NAVARCH_RUNTIMES=claude-code,codex,
|
|
637
|
+
export NAVARCH_RUNTIMES=claude-code,codex,antigravity,opencode,acp,qoder
|
|
637
638
|
```
|
|
638
639
|
|
|
639
640
|
For the legacy single-runtime setting, priority is `start --agent` →
|
|
640
641
|
`NAVARCH_AGENT` → the locally saved choice → `claude-code`.
|
|
641
642
|
`NAVARCH_RUNTIMES` expands what the worker advertises. For normal projects,
|
|
642
643
|
the locally selected `NAVARCH_AGENT` handles every unpinned task. A task with a
|
|
643
|
-
runtime override (for example `
|
|
644
|
+
runtime override (for example `antigravity`) is claimed only by a worker that
|
|
644
645
|
advertises that adapter, and the claim returns it as the session runtime.
|
|
645
646
|
Sandbox projects remain constrained to Claude Code, so they claim only unpinned
|
|
646
647
|
or `claude-code`-pinned tasks. List only adapters this worker can really run:
|
|
@@ -657,10 +658,10 @@ through to `complete()` unchanged by whatever happened during the run.
|
|
|
657
658
|
The control plane also resolves the project's model and the task's execution
|
|
658
659
|
profile on every claim. The runtime passes those values as per-session CLI
|
|
659
660
|
overrides (`codex exec --model ... -c model_reasoning_effort=...`,
|
|
660
|
-
`claude -p --model ... --effort ...`, `
|
|
661
|
+
`claude -p --model ... --effort ...`, `agy --model ...`, or `opencode run
|
|
661
662
|
--model ... --variant ...`) and records the effective model, profile, and
|
|
662
|
-
effort on completion.
|
|
663
|
-
|
|
663
|
+
effort on completion. Antigravity uses the `default` sentinel to preserve the CLI account model;
|
|
664
|
+
explicit low/medium/high effort is supported without inventing higher levels. OpenCode accepts any `provider/model`
|
|
664
665
|
reference (the platform default is `opencode/x-preview-f-free`, Ox Alpha Free
|
|
665
666
|
(Unlimited) on OpenCode Zen), or `default` to preserve the authenticated
|
|
666
667
|
account's selection. ACP sessions use the agent's advertised default model and
|
|
@@ -709,24 +710,66 @@ not placed in argv. The JSONL parser accepts the verified top-level
|
|
|
709
710
|
`item.completed` / `turn.completed` shape and retains the older `msg`
|
|
710
711
|
envelope as a compatibility fallback.
|
|
711
712
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
`
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
`
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
713
|
+
### Antigravity CLI contract and migration
|
|
714
|
+
|
|
715
|
+
The Antigravity adapter requires **agy 1.3.1 or later**. It validates
|
|
716
|
+
`agy --version` locally before each turn, then runs
|
|
717
|
+
`agy --input-format stream-json --output-format stream-json`, writes one
|
|
718
|
+
`{"event":"user","message":{"content":"..."}}` line, and closes stdin.
|
|
719
|
+
Only text prompts are supported. It never sends `control_request` or
|
|
720
|
+
`control_response`, which the CLI explicitly rejects. The final response
|
|
721
|
+
comes from `event: "result"` / `result.response`, not accumulated tool or
|
|
722
|
+
assistant progress. Raw `init`, `step_update` (including tool progress), and
|
|
723
|
+
`result` events remain available in the live transcript.
|
|
724
|
+
|
|
725
|
+
An OS exit of zero is insufficient: a missing/malformed/duplicate result,
|
|
726
|
+
wrong conversation ID, non-`SUCCESS` status, `AGY_ERROR`, nonempty or malformed
|
|
727
|
+
`denied_actions`, permission-denial notices, or a partial-timeout warning
|
|
728
|
+
fails the turn. Antigravity may soft-deny a required action while exiting zero.
|
|
729
|
+
The runtime owns deadlines; it does not set `--print-timeout`, whose expiration
|
|
730
|
+
can return partial output with exit zero. The vendor changelog states the
|
|
731
|
+
headless default became unlimited in 1.2.6, despite the older reference table.
|
|
732
|
+
Cancellation sends SIGINT to the owned process group, escalates to SIGKILL,
|
|
733
|
+
and closes lease MCP sockets and generated configuration; Docker cancellation
|
|
734
|
+
stops the session container.
|
|
735
|
+
|
|
736
|
+
Each corrective turn resumes only the verified Antigravity conversation from
|
|
737
|
+
this lease, using `--conversation <id>`. Provider, workspace, model, turn count
|
|
738
|
+
and monotonic cumulative usage are checked. No implicit `--continue`, arbitrary
|
|
739
|
+
extra-argument session ID, Gemini ID, persisted cross-lease ID, or credential
|
|
740
|
+
migration is accepted. Cumulative input/output counters are differenced on
|
|
741
|
+
resume. USD cost stays unknown; cache-read inclusion is not sufficiently
|
|
742
|
+
specified to invent an inclusive cache split. Raw counters remain in the
|
|
743
|
+
transcript.
|
|
744
|
+
|
|
745
|
+
MCP definitions use Antigravity's documented workspace `.agents/mcp_config.json`
|
|
746
|
+
stdio format. The runtime exclusively creates a secret-free config pointing to
|
|
747
|
+
a short-lived private Unix-socket relay. MCP-specific HTTP lease headers stay in the parent runtime's memory, not the AG config, argv, or relay child's environment. The existing managed Git credential helper still receives the machine token needed for its authorized refresh endpoint; this bridge does not eliminate all child-process credentials. The
|
|
748
|
+
source staging JSON uses Navarch-owned environment references, resolved by
|
|
749
|
+
Navarch itself; no Antigravity interpolation or alternate config-root API is
|
|
750
|
+
assumed. The repository must already ignore `/.agents/mcp_config.json` in a local `.gitignore`; Navarch never edits ignore rules. A post-run index check rejects forced staging/committing without rewriting history. Existing `.agents/mcp_config.json`, symlinked directories, unsupported
|
|
751
|
+
transports, or socket paths over 100 bytes fail closed. Configuration and
|
|
752
|
+
sockets are removed after the child exits; changed files are preserved and
|
|
753
|
+
reported as cleanup failures. Existing global/user config is never edited.
|
|
754
|
+
Host sockets use a short private system-temp directory. Docker requires a short session metadata path (socket pathname at most 100 bytes), the same-path metadata mount, and Node in the image;
|
|
755
|
+
Windows is unsupported for this relay. Live authenticated AG/Docker compatibility
|
|
756
|
+
has not been verified by the offline suite.
|
|
757
|
+
|
|
758
|
+
`gemini` is a retired identity, not an alias. Existing machine configuration,
|
|
759
|
+
credentials, task pins, session history and provider IDs remain untouched.
|
|
760
|
+
Selecting or advertising `gemini` fails with migration guidance; a stale claim
|
|
761
|
+
hits a tombstone adapter that starts no process. Install from the
|
|
762
|
+
[official Antigravity instructions](https://antigravity.google/docs/cli/install/),
|
|
763
|
+
authenticate in an interactive `agy` session, then deliberately choose
|
|
764
|
+
`--agent antigravity` / `NAVARCH_AGENT=antigravity`, update `NAVARCH_RUNTIMES`
|
|
765
|
+
and existing project/task pins. `NAVARCH_GEMINI_*` values are not reused.
|
|
766
|
+
API-key mode requires the operator's `modelProvider: "gemini"` settings and
|
|
767
|
+
`GEMINI_API_KEY`; Navarch does not create keys or rewrite auth settings.
|
|
768
|
+
|
|
769
|
+
Primary contracts checked October 12, 2026:
|
|
770
|
+
[headless input/output](https://antigravity.google/docs/cli/headless/),
|
|
771
|
+
[release changes](https://antigravity.google/docs/changelog), and
|
|
772
|
+
[MCP configuration](https://antigravity.google/docs/mcp/).
|
|
730
773
|
|
|
731
774
|
The OpenCode adapter uses `opencode run --format json` with the prompt on
|
|
732
775
|
stdin, and normalizes `text` and `step_finish` JSONL events into
|
|
@@ -759,9 +802,6 @@ rejects any single argv or environment string over 128 KiB
|
|
|
759
802
|
(`MAX_ARG_STRLEN`), so an argv prompt made large review sessions fail with
|
|
760
803
|
`spawn E2BIG` before the agent started. Docker-mode sessions forward the same
|
|
761
804
|
stdin through `docker exec -i`.
|
|
762
|
-
Gemini's own sandbox relaunch is an exception to this transport guarantee,
|
|
763
|
-
as described above; moving the initial spawn to stdin does not remove its
|
|
764
|
-
internal argv limit.
|
|
765
805
|
|
|
766
806
|
## Architecture
|
|
767
807
|
|
|
@@ -782,7 +822,7 @@ cli.cts
|
|
|
782
822
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
783
823
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p --mcp-config <path> --output-format stream-json --verbose < prompt`
|
|
784
824
|
- codexAdapter (adapters/codex.cts) — `codex exec - --json -c mcp_servers.*=... < prompt`
|
|
785
|
-
-
|
|
825
|
+
- antigravityAdapter (adapters/antigravity.cts) — `agy --input-format stream-json --output-format stream-json`
|
|
786
826
|
- openCodeAdapter (adapters/opencode.cts) — `opencode run --format json < prompt`
|
|
787
827
|
- acpAdapter (adapters/acp.cts) — ACP v1 JSON-RPC/stdio; defaults to `dsh --profile acp`
|
|
788
828
|
- qoderAdapter (adapters/qoder.cts) — `qoder -p --output-format json < prompt`
|
|
@@ -878,15 +918,14 @@ tests cover:
|
|
|
878
918
|
- `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
|
|
879
919
|
and the docker-exec path (fake `CommandRunner`), and usage/report-text
|
|
880
920
|
attachment from fixed JSONL fixtures (`tests/adapters/codex.test.cts`).
|
|
881
|
-
- `adapters/
|
|
882
|
-
|
|
883
|
-
(`tests/adapters/gemini.test.cts`).
|
|
921
|
+
- `adapters/antigravity.cts` and `antigravity-mcp.cts` — strict streams, denial/timeout false-green prevention, usage/resume identity, private lease relay and cleanup (`tests/adapters/antigravity*.test.cts`).
|
|
922
|
+
- `adapters/gemini.cts` — retired-provider tombstone (`tests/adapters/gemini.test.cts`).
|
|
884
923
|
- `adapters/opencode.cts` — real fake-binary invocation covering the prompt,
|
|
885
924
|
isolated config and MCP header references, JSONL normalization, timeout,
|
|
886
925
|
lease cancellation, and fail-closed host boundary
|
|
887
926
|
(`tests/adapters/opencode.test.cts`).
|
|
888
927
|
- `adapters/types.cts` — the shared process-result boundary is exercised for
|
|
889
|
-
Claude, Codex,
|
|
928
|
+
Claude, Codex, and OpenCode (`tests/adapters/conformance.test.cts`).
|
|
890
929
|
- `worktree-guard.cts` + `bin/worktree-guard-hook.cjs` — generated settings/
|
|
891
930
|
config shape, native Codex permission-profile construction, and the hook's
|
|
892
931
|
containment verdicts (in-worktree vs. sibling session vs. home dir, symlink
|
|
@@ -920,8 +959,7 @@ secrets absent from disk after exit"):
|
|
|
920
959
|
flags, per-run MCP override keys, stdin behavior, and JSONL event/usage
|
|
921
960
|
shape are now verified locally; the next dogfood run covers their
|
|
922
961
|
production composition.
|
|
923
|
-
- A full
|
|
924
|
-
open a PR, report usage/evidence, and clean up its session sandbox.
|
|
962
|
+
- A full authenticated Antigravity 1.3.1+ task with scoped permissions, real AG stdio MCP loading, existing global customizations, Docker image, resume, PR delivery and cancellation. Mock/relay tests are not that proof.
|
|
925
963
|
- A full OpenCode task in an operator-owned Docker image that contains an
|
|
926
964
|
authenticated OpenCode CLI, including lease MCP access, cancellation, PR
|
|
927
965
|
creation, attribution, and cleanup.
|
|
@@ -0,0 +1,221 @@
|
|
|
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.ANTIGRAVITY_STDIO_RELAY = void 0;
|
|
7
|
+
exports.prepareAntigravityMcp = prepareAntigravityMcp;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_util_1 = require("node:util");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
14
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/shared/stdio.js");
|
|
15
|
+
/** No auth values in this code, config, argv or socket path. The stdio child
|
|
16
|
+
* has no HTTP credentials: it relays bytes to the owning runtime process.
|
|
17
|
+
*/
|
|
18
|
+
exports.ANTIGRAVITY_STDIO_RELAY = `const net=require('node:net');const socket=net.createConnection(process.argv[1]);socket.on('error',()=>{process.stderr.write('Navarch MCP relay unavailable\\n');process.exitCode=1;process.stdin.destroy();});process.stdin.pipe(socket);socket.pipe(process.stdout);socket.on('close',()=>process.stdin.destroy());`;
|
|
19
|
+
/**
|
|
20
|
+
* AG has no documented per-run MCP-config flag or env interpolation. Use its
|
|
21
|
+
* documented workspace stdio format, exclusively created and removed here.
|
|
22
|
+
* Global AG configuration still loads; this is NOT an exclusive tool boundary.
|
|
23
|
+
*/
|
|
24
|
+
async function prepareAntigravityMcp(options) {
|
|
25
|
+
if (!options.mcpConfigPath)
|
|
26
|
+
return { close: async () => undefined, failed: () => false };
|
|
27
|
+
if (process.platform === "win32")
|
|
28
|
+
throw new Error("Unix socket isolation required");
|
|
29
|
+
const cwd = node_path_1.default.resolve(options.cwd);
|
|
30
|
+
const parent = node_path_1.default.dirname(node_path_1.default.resolve(options.mcpConfigPath));
|
|
31
|
+
if (await node_fs_1.promises.realpath(cwd) !== cwd || await node_fs_1.promises.realpath(parent) !== parent)
|
|
32
|
+
throw new Error("Symlinked MCP parent refused");
|
|
33
|
+
const parsed = JSON.parse(await node_fs_1.promises.readFile(options.mcpConfigPath, "utf8"));
|
|
34
|
+
if (!record(parsed) || !record(parsed.mcpServers))
|
|
35
|
+
throw new Error("Invalid MCP source");
|
|
36
|
+
const servers = Object.entries(parsed.mcpServers).map(([name, raw]) => {
|
|
37
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name) || !record(raw))
|
|
38
|
+
throw new Error("Invalid MCP server");
|
|
39
|
+
// Navarch's lease servers use Streamable HTTP. Never silently convert SSE
|
|
40
|
+
// or executable configs to a different transport.
|
|
41
|
+
if (raw.type !== "http" || typeof raw.url !== "string")
|
|
42
|
+
throw new Error("Unsupported lease MCP transport");
|
|
43
|
+
const url = new URL(raw.url);
|
|
44
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
45
|
+
throw new Error("Invalid lease MCP URL");
|
|
46
|
+
if (url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
47
|
+
throw new Error("Insecure remote MCP refused");
|
|
48
|
+
if (raw.headers !== undefined && !record(raw.headers))
|
|
49
|
+
throw new Error("Invalid MCP headers");
|
|
50
|
+
const headers = {};
|
|
51
|
+
for (const [key, value] of Object.entries(raw.headers ?? {})) {
|
|
52
|
+
if (typeof value !== "string")
|
|
53
|
+
throw new Error("Invalid MCP header");
|
|
54
|
+
// This is Navarch's staging format, resolved by OUR bridge, not AG.
|
|
55
|
+
const ref = /^\{env:([A-Z_][A-Z0-9_]*)\}$/.exec(value);
|
|
56
|
+
if (ref && options.env[ref[1]] === undefined)
|
|
57
|
+
throw new Error("Missing lease MCP value");
|
|
58
|
+
headers[key] = ref ? options.env[ref[1]] : value;
|
|
59
|
+
}
|
|
60
|
+
return [name, { url, headers }];
|
|
61
|
+
});
|
|
62
|
+
if (servers.length === 0)
|
|
63
|
+
return { close: async () => undefined, failed: () => false };
|
|
64
|
+
if (await node_fs_1.promises.lstat(node_path_1.default.join(cwd, ".git")).catch(() => null)) {
|
|
65
|
+
// A normal git add -A must not publish the transient relay config. Require
|
|
66
|
+
// repository-local ignore coverage, which is also visible in the mounted
|
|
67
|
+
// Docker checkout. Do not rewrite tracked .gitignore or global excludes.
|
|
68
|
+
const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)("git", ["-C", cwd, "check-ignore", "--no-index", "--verbose", "--", ".agents/mcp_config.json"], { timeout: 5_000, maxBuffer: 1024 * 1024 });
|
|
69
|
+
const match = /^(\.gitignore|\.agents\/\.gitignore):\d+:([^\t\n]+)\t\.agents\/mcp_config\.json\r?\n?$/.exec(stdout);
|
|
70
|
+
if (!match || match[2].startsWith("!"))
|
|
71
|
+
throw new Error("Repository-local MCP ignore rule required");
|
|
72
|
+
}
|
|
73
|
+
const agentsDir = node_path_1.default.join(cwd, ".agents");
|
|
74
|
+
const configPath = node_path_1.default.join(agentsDir, "mcp_config.json");
|
|
75
|
+
let createdAgentsDir = false;
|
|
76
|
+
try {
|
|
77
|
+
await node_fs_1.promises.mkdir(agentsDir, { mode: 0o700 });
|
|
78
|
+
createdAgentsDir = true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code !== "EEXIST")
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const agentsStat = await node_fs_1.promises.lstat(agentsDir);
|
|
85
|
+
if (!agentsStat.isDirectory() || agentsStat.isSymbolicLink())
|
|
86
|
+
throw new Error("Unsafe .agents directory");
|
|
87
|
+
// Reserve before making any sockets. wx refuses user-owned files/symlinks.
|
|
88
|
+
const config = await node_fs_1.promises.open(configPath, "wx", 0o600);
|
|
89
|
+
const owned = await config.stat();
|
|
90
|
+
let configText = "";
|
|
91
|
+
let tempDir;
|
|
92
|
+
let relayFailed = false;
|
|
93
|
+
let closed = false;
|
|
94
|
+
const relays = [];
|
|
95
|
+
const close = async () => {
|
|
96
|
+
if (closed)
|
|
97
|
+
return;
|
|
98
|
+
closed = true;
|
|
99
|
+
await config.close().catch(() => undefined);
|
|
100
|
+
for (const relay of relays) {
|
|
101
|
+
for (const socket of relay.sockets)
|
|
102
|
+
socket.destroy();
|
|
103
|
+
await Promise.allSettled([...relay.transports].map((transport) => transport.close()));
|
|
104
|
+
await new Promise((resolve) => relay.server.close(() => resolve()));
|
|
105
|
+
}
|
|
106
|
+
if (tempDir)
|
|
107
|
+
await node_fs_1.promises.rm(tempDir, { recursive: true, force: true });
|
|
108
|
+
// Do not erase a replacement supplied by the agent/user. No secret is in
|
|
109
|
+
// this file, but failure to remove our exact config is still a failed run.
|
|
110
|
+
let cleanupError = false;
|
|
111
|
+
// The transient config must never become a delivered repository file.
|
|
112
|
+
// Detect index/commit contamination without rewriting either user-owned
|
|
113
|
+
// history or the index; cleanup still removes our owned working file.
|
|
114
|
+
if (await node_fs_1.promises.lstat(node_path_1.default.join(cwd, ".git")).catch(() => null)) {
|
|
115
|
+
try {
|
|
116
|
+
await (0, node_util_1.promisify)(node_child_process_1.execFile)("git", ["--literal-pathspecs", "-C", cwd, "ls-files", "--error-unmatch", "--", ".agents/mcp_config.json"], { timeout: 5_000, maxBuffer: 1024 * 1024 });
|
|
117
|
+
cleanupError = true;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code !== 1)
|
|
121
|
+
cleanupError = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const current = await node_fs_1.promises.lstat(configPath);
|
|
126
|
+
if (current.isFile() && !current.isSymbolicLink() && current.ino === owned.ino && current.dev === owned.dev && await node_fs_1.promises.readFile(configPath, "utf8") === configText) {
|
|
127
|
+
await node_fs_1.promises.unlink(configPath);
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
cleanupError = true;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error.code !== "ENOENT")
|
|
134
|
+
cleanupError = true;
|
|
135
|
+
}
|
|
136
|
+
if (createdAgentsDir)
|
|
137
|
+
await node_fs_1.promises.rmdir(agentsDir).catch(() => undefined);
|
|
138
|
+
if (cleanupError)
|
|
139
|
+
throw new Error("MCP config ownership changed");
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
// Containers need the same-path session mount. Host workers use the
|
|
143
|
+
// short system temp root so ordinary per-project config directories do
|
|
144
|
+
// not exceed AF_UNIX's path limit. Neither directory holds credentials.
|
|
145
|
+
const socketRoot = options.dockerExec ? parent : await node_fs_1.promises.realpath("/tmp");
|
|
146
|
+
tempDir = await node_fs_1.promises.mkdtemp(node_path_1.default.join(socketRoot, "agy-"));
|
|
147
|
+
await node_fs_1.promises.chmod(tempDir, 0o700);
|
|
148
|
+
const mcpServers = {};
|
|
149
|
+
for (const [index, [name, target]] of servers.entries()) {
|
|
150
|
+
const socketPath = node_path_1.default.join(tempDir, `${index}.sock`);
|
|
151
|
+
if (Buffer.byteLength(socketPath) > 100)
|
|
152
|
+
throw new Error("Session metadata path too long for Unix socket");
|
|
153
|
+
const sockets = new Set();
|
|
154
|
+
const transports = new Set();
|
|
155
|
+
const server = node_net_1.default.createServer({ allowHalfOpen: true }, (socket) => {
|
|
156
|
+
sockets.add(socket);
|
|
157
|
+
const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(target.url, { requestInit: { headers: target.headers }, reconnectionOptions: { maxReconnectionDelay: 1000, initialReconnectionDelay: 100, reconnectionDelayGrowFactor: 1.5, maxRetries: 1 } });
|
|
158
|
+
transports.add(transport);
|
|
159
|
+
const fail = () => { if (!closed)
|
|
160
|
+
relayFailed = true; socket.destroy(); void transport.close().catch(() => undefined); };
|
|
161
|
+
transport.onerror = fail;
|
|
162
|
+
let initializeId;
|
|
163
|
+
transport.onmessage = (message) => {
|
|
164
|
+
if ("id" in message && message.id === initializeId && "result" in message && record(message.result) && typeof message.result.protocolVersion === "string") {
|
|
165
|
+
transport.setProtocolVersion(message.result.protocolVersion);
|
|
166
|
+
}
|
|
167
|
+
if (!socket.destroyed)
|
|
168
|
+
socket.write((0, stdio_js_1.serializeMessage)(message));
|
|
169
|
+
};
|
|
170
|
+
transport.onclose = () => socket.end();
|
|
171
|
+
const started = transport.start();
|
|
172
|
+
let ready = started;
|
|
173
|
+
const pending = new Set();
|
|
174
|
+
const buffer = new stdio_js_1.ReadBuffer({ maxBufferSize: 16 * 1024 * 1024 });
|
|
175
|
+
socket.on("data", (chunk) => {
|
|
176
|
+
try {
|
|
177
|
+
buffer.append(chunk);
|
|
178
|
+
let message;
|
|
179
|
+
while ((message = buffer.readMessage()) !== null) {
|
|
180
|
+
const current = message;
|
|
181
|
+
if ("method" in current && current.method === "initialize" && "id" in current)
|
|
182
|
+
initializeId = current.id;
|
|
183
|
+
// Do not serialize complete HTTP requests: cancellation must be
|
|
184
|
+
// able to overtake an outstanding tools/call.
|
|
185
|
+
const operation = ready.then(() => transport.send(current));
|
|
186
|
+
if ("method" in current && ["initialize", "notifications/initialized"].includes(current.method))
|
|
187
|
+
ready = operation;
|
|
188
|
+
pending.add(operation);
|
|
189
|
+
void operation.then(() => pending.delete(operation), () => { pending.delete(operation); fail(); });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
fail();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
void started.catch(fail);
|
|
197
|
+
socket.on("end", () => { void Promise.all([...pending]).then(() => socket.end(), fail); });
|
|
198
|
+
socket.on("error", fail);
|
|
199
|
+
socket.on("close", () => { sockets.delete(socket); transports.delete(transport); void transport.close().catch(() => undefined); });
|
|
200
|
+
});
|
|
201
|
+
relays.push({ server, sockets, transports });
|
|
202
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(socketPath, () => { server.removeListener("error", reject); resolve(); }); });
|
|
203
|
+
server.on("error", () => { if (!closed)
|
|
204
|
+
relayFailed = true; });
|
|
205
|
+
await node_fs_1.promises.chmod(socketPath, 0o600);
|
|
206
|
+
mcpServers[name] = {
|
|
207
|
+
command: options.dockerExec ? "node" : process.execPath,
|
|
208
|
+
args: ["-e", exports.ANTIGRAVITY_STDIO_RELAY, socketPath],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
configText = JSON.stringify({ mcpServers }, null, 2);
|
|
212
|
+
await config.writeFile(configText, "utf8");
|
|
213
|
+
await config.close();
|
|
214
|
+
return { close, failed: () => relayFailed };
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
await close();
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function record(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|