@sagentlab/navarch-runtime 0.1.10 → 0.1.12
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 +115 -46
- package/dist/adapters/claude.cjs +1 -1
- package/dist/adapters/gemini.cjs +229 -0
- package/dist/adapters/index.cjs +7 -2
- package/dist/api.cjs +49 -6
- package/dist/claim-loop.cjs +21 -3
- package/dist/cli.cjs +5 -5
- package/dist/config.cjs +8 -3
- package/dist/machine-store.cjs +3 -1
- package/dist/session.cjs +77 -27
- package/dist/worktree-guard.cjs +51 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ it. Plain Node/TypeScript, zero production dependencies, no Next.js coupling
|
|
|
6
6
|
— this directory is a self-contained package you can `npx` on any fresh
|
|
7
7
|
machine.
|
|
8
8
|
|
|
9
|
-
The
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"Choosing an agent
|
|
9
|
+
The control plane selects **Claude Code**, **OpenAI Codex**, or **Google
|
|
10
|
+
Gemini CLI** from the project's default and any task override. Workers
|
|
11
|
+
advertise the adapters they can run, so tasks wait for an eligible runtime
|
|
12
|
+
and capability match — see "Choosing an agent" below.
|
|
13
13
|
|
|
14
14
|
See [`docs/agent-platform-project-plan.md`](../docs/agent-platform-project-plan.md)
|
|
15
15
|
§3.8/§3.9/§3.11 and [`docs/navarch/implementation-plan.md`](../docs/navarch/implementation-plan.md)
|
|
@@ -19,31 +19,66 @@ the API contract.
|
|
|
19
19
|
|
|
20
20
|
## Quick start on a fresh machine
|
|
21
21
|
|
|
22
|
+
For a project machine, use the command generated by **Onboard → Connect an
|
|
23
|
+
agent** or **Project settings → Connect an agent**. It includes a single-use,
|
|
24
|
+
project-scoped enrollment token and the correct control-plane origin. The
|
|
25
|
+
recommended npm package requires Node.js 20 or later:
|
|
26
|
+
|
|
22
27
|
```sh
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
# Run the two commands copied from Navarch. Their values resemble:
|
|
29
|
+
npx --yes @sagentlab/navarch-runtime@latest connect \
|
|
30
|
+
--token flmt_<single-use-token> --project <project-id> \
|
|
31
|
+
--name <machine-name> --agent codex \
|
|
32
|
+
--capabilities shell,browser-use --max-sessions 5 \
|
|
33
|
+
--api-base https://www.sagentlab.com --config-dir ~/.navarch/<project-slug>
|
|
34
|
+
npx --yes @sagentlab/navarch-runtime@latest supervise \
|
|
35
|
+
--config-dir ~/.navarch/<project-slug>
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
Keep the same config directory for `connect`, `supervise`, `doctor`, and later
|
|
39
|
+
upgrades. `connect` stores the machine identity in `<config-dir>/machine.json`
|
|
40
|
+
with mode `0600`; the raw machine token is not shown again. Do not copy that
|
|
41
|
+
file or the enrollment command into logs.
|
|
42
|
+
|
|
43
|
+
In another terminal, verify the machine without starting a second worker:
|
|
31
44
|
|
|
32
45
|
```sh
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
npm run supervise
|
|
46
|
+
npx --yes @sagentlab/navarch-runtime@latest doctor \
|
|
47
|
+
--config-dir ~/.navarch/<project-slug>
|
|
36
48
|
```
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
For the BYO-key sandbox path, follow the generated Claude/Docker command
|
|
51
|
+
exactly. Docker must be running, and `NAVARCH_DOCKER_IMAGE` must identify an
|
|
52
|
+
image containing Git, Node.js 20 or later, Claude Code 2.1.83 or later, and
|
|
53
|
+
the build/test tools required by the repository. The current default
|
|
54
|
+
`node:20-slim` image does not satisfy those requirements; a first-party image
|
|
55
|
+
is tracked in [issue #229](https://github.com/sagentlab/navarch/issues/229).
|
|
56
|
+
The complete public flow is in
|
|
57
|
+
[the Navarch quickstart](../docs/navarch/quickstart.md).
|
|
58
|
+
|
|
59
|
+
### Install from source
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
Use this path for runtime development or when validating an unreleased runtime
|
|
62
|
+
change:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git clone https://github.com/sagentlab/navarch.git
|
|
66
|
+
cd navarch/runtime
|
|
67
|
+
./install.sh
|
|
68
|
+
node bin/navarch.cjs connect <options copied from Navarch>
|
|
69
|
+
node bin/navarch.cjs supervise --config-dir ~/.navarch/<project-slug>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The installer checks Node, installs the locked package dependencies, and builds
|
|
73
|
+
the runtime. The UI-generated npm commands remain the source of truth for the
|
|
74
|
+
token, project, agent, capacity, API origin, and config directory; translate
|
|
75
|
+
those same options to `node bin/navarch.cjs` for a source install.
|
|
76
|
+
|
|
77
|
+
The operator-only `register` command enrolls a globally managed machine. It is
|
|
78
|
+
not the normal public onboarding path. `register` prints the machine auth token
|
|
79
|
+
once and stores the same `machine.json` identity used by `connect`. For
|
|
80
|
+
twelve-factor deployments (systemd `EnvironmentFile`, container secrets), set
|
|
81
|
+
`NAVARCH_MACHINE_TOKEN` and `NAVARCH_MACHINE_ID` directly.
|
|
47
82
|
|
|
48
83
|
### Connecting an agent to one project
|
|
49
84
|
|
|
@@ -60,7 +95,7 @@ project only (it will never be dispatched work from any other project).
|
|
|
60
95
|
|
|
61
96
|
```sh
|
|
62
97
|
npx @sagentlab/navarch-runtime connect --token flmt_<...> --project <project-id> \
|
|
63
|
-
--name my-agent-1 --agent codex --api-base https://
|
|
98
|
+
--name my-agent-1 --agent codex --api-base https://www.sagentlab.com \
|
|
64
99
|
--config-dir ~/.navarch/my-agent-1
|
|
65
100
|
npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/my-agent-1
|
|
66
101
|
```
|
|
@@ -74,10 +109,10 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
74
109
|
|
|
75
110
|
| Command | Purpose |
|
|
76
111
|
|---|---|
|
|
77
|
-
| `register --token <t> --name <n> [--agent claude-code\|codex] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
78
|
-
| `connect --token <t> --name <n> [--agent claude-code\|codex] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
79
|
-
| `start [--agent claude-code\|codex]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
80
|
-
| `supervise [--agent claude-code\|codex]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
112
|
+
| `register --token <t> --name <n> [--agent claude-code\|codex\|gemini] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
113
|
+
| `connect --token <t> --name <n> [--agent claude-code\|codex\|gemini] [--project <id>] […]` | Connects this machine to one project, saves its local agent choice, and prints the token once. |
|
|
114
|
+
| `start [--agent claude-code\|codex\|gemini]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
115
|
+
| `supervise [--agent claude-code\|codex\|gemini]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
81
116
|
| `doctor` | Prints resolved config + Docker/registration status; no side effects. |
|
|
82
117
|
|
|
83
118
|
### Running multiple agents on one machine
|
|
@@ -117,7 +152,7 @@ restart, not a new dispatch:
|
|
|
117
152
|
- The replacement turn uses the same worktree (and the same sandbox container
|
|
118
153
|
in Docker mode), so committed and uncommitted changes from the interrupted
|
|
119
154
|
turn remain available. It should inspect those changes before continuing.
|
|
120
|
-
- The replacement turn is not a resumed
|
|
155
|
+
- The replacement turn is not a resumed agent CLI conversation.
|
|
121
156
|
The corrected prompt carries the prior task context and guidance instead.
|
|
122
157
|
- Transcript, token, and cost accounting is cumulative across turns: the
|
|
123
158
|
upload includes every turn under a separate label, and reported token and
|
|
@@ -211,7 +246,7 @@ unchanged across the deployment.
|
|
|
211
246
|
| `NAVARCH_CONFIG_DIR` | `~/.navarch` | Per-instance state root. Equivalent to `--config-dir`; use a different directory for every agent on the same host. |
|
|
212
247
|
| `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Persistent bare repo caches plus isolated per-session worktrees. |
|
|
213
248
|
| `NAVARCH_MAX_SESSIONS` | `5` | Local concurrent-session capacity cap — see `src/capacity.cts`. |
|
|
214
|
-
| `NAVARCH_CAPABILITIES` | `shell` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. |
|
|
249
|
+
| `NAVARCH_CAPABILITIES` | `shell,browser-use` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. Docker mode does not advertise browser use until the configured image provides it. |
|
|
215
250
|
| `NAVARCH_OWNER_ZONE` | `sagentlab` | `sagentlab` or `customer-<slug>-premises` (project-plan.md §3.11). |
|
|
216
251
|
| `NAVARCH_POLL_INTERVAL_MS` | `5000` | Claim-loop poll interval. |
|
|
217
252
|
| `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
|
|
@@ -219,16 +254,19 @@ unchanged across the deployment.
|
|
|
219
254
|
| `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
|
|
220
255
|
| `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
|
|
221
256
|
| `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
|
|
222
|
-
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
|
|
223
|
-
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `
|
|
257
|
+
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. The image must contain the selected agent CLI, Git, Node.js 20+, and project build/test tools; the default is only a low-level runtime fallback and is not sufficient for BYO-key sandbox sessions ([issue #229](https://github.com/sagentlab/navarch/issues/229)). |
|
|
258
|
+
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code`, `codex`, or `gemini`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
259
|
+
| `NAVARCH_RUNTIMES` | selected `NAVARCH_AGENT` | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
|
|
224
260
|
| `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
|
|
225
261
|
| `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`. |
|
|
226
262
|
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
227
263
|
| `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`. |
|
|
228
264
|
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
229
265
|
| `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP `-c` overrides and `--json` (Codex). |
|
|
266
|
+
| `NAVARCH_GEMINI_BIN` | `gemini` | Path/name of the Google Gemini CLI binary. |
|
|
267
|
+
| `NAVARCH_GEMINI_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP settings, `stream-json`, and unattended defaults (Gemini). |
|
|
230
268
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
231
|
-
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode
|
|
269
|
+
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable. |
|
|
232
270
|
| `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. |
|
|
233
271
|
|
|
234
272
|
## Worktree boundary guard (host mode)
|
|
@@ -251,6 +289,12 @@ supported coding agents, using each CLI's native enforcement point:
|
|
|
251
289
|
user or checked-in legacy `sandbox_mode` from silently disabling the
|
|
252
290
|
generated profile; Codex authentication still comes from `CODEX_HOME`, and
|
|
253
291
|
repository instructions such as `AGENTS.md` still load.
|
|
292
|
+
- **Gemini:** the runtime uses Gemini CLI's native sandbox with only the
|
|
293
|
+
current worktree, shared gitdir, and approved extra roots mounted. The
|
|
294
|
+
lease MCP config is translated to a read-only temporary settings file;
|
|
295
|
+
header values remain environment-variable references rather than literals.
|
|
296
|
+
Docker-mode sessions disable Gemini's implicit YOLO sandbox to avoid nesting
|
|
297
|
+
it inside Navarch's already isolated session container.
|
|
254
298
|
|
|
255
299
|
The resulting boundary is:
|
|
256
300
|
|
|
@@ -284,12 +328,11 @@ The auto-review launch paths were verified live on 2026-07-20 with Claude Code
|
|
|
284
328
|
permission profile plus `approvals_reviewer="auto_review"`). Both completed an
|
|
285
329
|
unattended smoke task successfully.
|
|
286
330
|
|
|
287
|
-
## Choosing an agent
|
|
331
|
+
## Choosing an agent
|
|
288
332
|
|
|
289
|
-
Each
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
environment:
|
|
333
|
+
Each worker advertises the agent CLIs it can actually execute. Existing
|
|
334
|
+
single-runtime installs keep using `--agent`/`NAVARCH_AGENT`; multi-runtime
|
|
335
|
+
workers set `NAVARCH_RUNTIMES` to the installed and authenticated adapters:
|
|
293
336
|
|
|
294
337
|
```sh
|
|
295
338
|
# Claude Code (default) — requires the `claude` CLI installed and
|
|
@@ -300,12 +343,22 @@ export NAVARCH_AGENT=claude-code
|
|
|
300
343
|
# this machine (or NAVARCH_CODEX_BIN pointing at it), analogous to the
|
|
301
344
|
# Claude Code prerequisite above.
|
|
302
345
|
export NAVARCH_AGENT=codex
|
|
346
|
+
|
|
347
|
+
# Google Gemini CLI — requires an authenticated `gemini` CLI (or
|
|
348
|
+
# NAVARCH_GEMINI_BIN pointing at it).
|
|
349
|
+
export NAVARCH_AGENT=gemini
|
|
350
|
+
|
|
351
|
+
# Or let one worker serve tasks selected for any installed adapter.
|
|
352
|
+
export NAVARCH_RUNTIMES=claude-code,codex,gemini
|
|
303
353
|
```
|
|
304
354
|
|
|
305
|
-
|
|
306
|
-
`
|
|
355
|
+
For the legacy single-runtime setting, priority is `start --agent` →
|
|
356
|
+
`NAVARCH_AGENT` → the locally saved choice → `claude-code`.
|
|
357
|
+
`NAVARCH_RUNTIMES` expands what the worker advertises; the control plane then
|
|
358
|
+
resolves `tasks.runtime_override` → `projects.default_runtime` and returns the
|
|
359
|
+
selected adapter with the claim.
|
|
307
360
|
|
|
308
|
-
|
|
361
|
+
All three adapters implement the same `AgentAdapter` interface
|
|
309
362
|
(`src/adapters/types.cts`) and run either directly on the host or via
|
|
310
363
|
`docker exec` in the session's sandbox container, exactly like the Claude
|
|
311
364
|
adapter always has — `session.cts` picks one (`src/adapters/index.cts`'s
|
|
@@ -314,10 +367,12 @@ through to `complete()` unchanged by whatever happened during the run.
|
|
|
314
367
|
|
|
315
368
|
The control plane also resolves the project's model and the task's execution
|
|
316
369
|
profile on every claim. The runtime passes those values as per-session CLI
|
|
317
|
-
overrides (`codex exec --model ... -c model_reasoning_effort
|
|
318
|
-
`claude -p --model ... --effort ...`) and records the
|
|
319
|
-
and effort on completion.
|
|
320
|
-
|
|
370
|
+
overrides (`codex exec --model ... -c model_reasoning_effort=...`,
|
|
371
|
+
`claude -p --model ... --effort ...`, or `gemini --model ...`) and records the
|
|
372
|
+
effective model, profile, and effort on completion. Gemini currently uses its
|
|
373
|
+
`auto` model default and does not expose a reasoning-effort flag. Machine-wide
|
|
374
|
+
extra arguments still configure other CLI behavior; dispatched model policy
|
|
375
|
+
wins.
|
|
321
376
|
|
|
322
377
|
The Codex CLI invocation was verified against `codex-cli 0.144.1` on
|
|
323
378
|
2026-07-18. The runtime uses `codex exec "<prompt>" --json` and translates
|
|
@@ -327,6 +382,12 @@ not placed in argv. The JSONL parser accepts the verified top-level
|
|
|
327
382
|
`item.completed` / `turn.completed` shape and retains the older `msg`
|
|
328
383
|
envelope as a compatibility fallback.
|
|
329
384
|
|
|
385
|
+
The Gemini adapter uses official headless `--prompt` and `--output-format
|
|
386
|
+
stream-json` flags. It resets pre-tool assistant text, retains the final
|
|
387
|
+
post-tool answer as the report, and reads aggregate `input_tokens` and
|
|
388
|
+
`output_tokens` from the terminal `result` event. Gemini does not report USD
|
|
389
|
+
cost, so an unknown value remains absent rather than becoming zero.
|
|
390
|
+
|
|
330
391
|
## Architecture
|
|
331
392
|
|
|
332
393
|
```
|
|
@@ -346,6 +407,7 @@ cli.cts
|
|
|
346
407
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
347
408
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
|
|
348
409
|
- codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json -c mcp_servers.*=...`
|
|
410
|
+
- geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
|
|
349
411
|
heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
|
|
350
412
|
a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
|
|
351
413
|
5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
|
|
@@ -360,7 +422,7 @@ cli.cts
|
|
|
360
422
|
|
|
361
423
|
`adapter.cts` (top-level) is now a backward-compat re-export of
|
|
362
424
|
`adapters/claude.cts` — new code should import `adapters/index.cts` (for
|
|
363
|
-
`selectAdapter`) or
|
|
425
|
+
`selectAdapter`) or an adapter module directly.
|
|
364
426
|
|
|
365
427
|
`api.cts` is the single choke point for every HTTP call; nothing else in the
|
|
366
428
|
package talks to `fetch` directly for control-plane traffic.
|
|
@@ -411,8 +473,8 @@ tests cover:
|
|
|
411
473
|
plus Claude's and Codex's usage/report parsing (`tests/exit-conditions.test.cts`).
|
|
412
474
|
- `redact.cts` — exact-value and pattern-based redaction (`tests/redact.test.cts`).
|
|
413
475
|
- `capacity.cts` — capacity math and acquire/release bookkeeping (`tests/capacity.test.cts`).
|
|
414
|
-
- `config.cts` — env var parsing and defaults
|
|
415
|
-
|
|
476
|
+
- `config.cts` — env var parsing and defaults for every selectable adapter
|
|
477
|
+
(`tests/config.test.cts`).
|
|
416
478
|
- `sandbox.cts` — command construction (flags, env-via-stdin, credential-helper
|
|
417
479
|
argv hygiene) against an injected fake `CommandRunner`, plus `isDockerAvailable()`
|
|
418
480
|
degrading to `false` instead of throwing when Docker is absent (`tests/sandbox.test.cts`).
|
|
@@ -422,6 +484,11 @@ tests cover:
|
|
|
422
484
|
- `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
|
|
423
485
|
and the docker-exec path (fake `CommandRunner`), and usage/report-text
|
|
424
486
|
attachment from fixed JSONL fixtures (`tests/adapters/codex.test.cts`).
|
|
487
|
+
- `adapters/gemini.cts` — host/Docker invocation, cancellation, sandbox/MCP
|
|
488
|
+
credential hygiene, parser drift, and normalized report/usage extraction
|
|
489
|
+
(`tests/adapters/gemini.test.cts`).
|
|
490
|
+
- `adapters/types.cts` — the shared process-result boundary is exercised for
|
|
491
|
+
Claude, Codex, and Gemini (`tests/adapters/conformance.test.cts`).
|
|
425
492
|
- `worktree-guard.cts` + `bin/worktree-guard-hook.cjs` — generated settings/
|
|
426
493
|
config shape, native Codex permission-profile construction, and the hook's
|
|
427
494
|
containment verdicts (in-worktree vs. sibling session vs. home dir, symlink
|
|
@@ -454,6 +521,8 @@ secrets absent from disk after exit"):
|
|
|
454
521
|
flags, per-run MCP override keys, stdin behavior, and JSONL event/usage
|
|
455
522
|
shape are now verified locally; the next dogfood run covers their
|
|
456
523
|
production composition.
|
|
524
|
+
- A full Gemini task that uses an authenticated Gemini CLI to claim, edit,
|
|
525
|
+
open a PR, report usage/evidence, and clean up its session sandbox.
|
|
457
526
|
- Whether Docker-mode Codex should opt into
|
|
458
527
|
`--dangerously-bypass-approvals-and-sandbox`. It is intentionally not a
|
|
459
528
|
default: host mode is not an external sandbox, and silently disabling
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -12,7 +12,7 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
12
12
|
* exit-conditions.cts#parseClaudeJsonResult) for exit-conditions.cts to map
|
|
13
13
|
* onto a complete()/fail() call.
|
|
14
14
|
*
|
|
15
|
-
* This is one
|
|
15
|
+
* This is one implementation of the AgentAdapter interface
|
|
16
16
|
* (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
|
|
17
17
|
* session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
|
|
18
18
|
*
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.geminiAdapter = void 0;
|
|
4
|
+
exports.runGeminiAdapter = runGeminiAdapter;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
/**
|
|
8
|
+
* Headless Google Gemini CLI adapter. Gemini's `stream-json` output gives the
|
|
9
|
+
* same normalized boundary as the Claude and Codex adapters: raw process
|
|
10
|
+
* signals plus an optional final report and token counts. Gemini CLI does not
|
|
11
|
+
* report USD cost, so costUsd remains absent.
|
|
12
|
+
*
|
|
13
|
+
* The CLI reads MCP servers from settings rather than accepting Claude's
|
|
14
|
+
* `--mcp-config` flag. The adapter converts the lease-scoped config into a
|
|
15
|
+
* temporary system-settings file and restricts the run to those server names.
|
|
16
|
+
*/
|
|
17
|
+
async function runGeminiAdapter(options) {
|
|
18
|
+
const args = ["--prompt", options.prompt];
|
|
19
|
+
const env = { ...options.env };
|
|
20
|
+
if (!hasArg(options.extraArgs, "--output-format", "-o")) {
|
|
21
|
+
args.push("--output-format", "stream-json");
|
|
22
|
+
}
|
|
23
|
+
if (!hasArg(options.extraArgs, "--skip-trust")) {
|
|
24
|
+
args.push("--skip-trust");
|
|
25
|
+
}
|
|
26
|
+
if (!hasExplicitApprovalMode(options.extraArgs)) {
|
|
27
|
+
// Navarch sessions have no interactive operator. Host sessions still use
|
|
28
|
+
// Gemini's sandbox below when the worktree guard is enabled.
|
|
29
|
+
args.push("--approval-mode", "yolo");
|
|
30
|
+
}
|
|
31
|
+
const sandboxMounts = [...(options.geminiSandboxMounts ?? [])];
|
|
32
|
+
if (options.mcpConfigPath) {
|
|
33
|
+
const mcp = await prepareGeminiMcpSettings(options.mcpConfigPath, env);
|
|
34
|
+
env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = mcp.settingsPath;
|
|
35
|
+
sandboxMounts.push(`${mcp.settingsPath}:${mcp.settingsPath}:ro`);
|
|
36
|
+
if (mcp.serverNames.length > 0 && !hasArg(options.extraArgs, "--allowed-mcp-server-names")) {
|
|
37
|
+
args.push("--allowed-mcp-server-names", mcp.serverNames.join(","));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!hasArg(options.extraArgs, "--sandbox", "-s")) {
|
|
41
|
+
if (options.geminiSandboxMounts) {
|
|
42
|
+
args.push("--sandbox");
|
|
43
|
+
env.SANDBOX_MOUNTS = sandboxMounts.join(",");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// YOLO mode enables Gemini's own sandbox implicitly. Disable that
|
|
47
|
+
// nested sandbox when Navarch already supplied Docker isolation, or
|
|
48
|
+
// when an operator explicitly disabled the host worktree guard.
|
|
49
|
+
args.push("--sandbox=false");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
args.push(...options.extraArgs);
|
|
53
|
+
if (options.model)
|
|
54
|
+
args.push("--model", options.model);
|
|
55
|
+
const runOptions = { ...options, env };
|
|
56
|
+
const raw = runOptions.dockerExec
|
|
57
|
+
? await runViaDocker(runOptions, args)
|
|
58
|
+
: await runOnHost(runOptions, args);
|
|
59
|
+
return attachGeminiOutput(raw);
|
|
60
|
+
}
|
|
61
|
+
function hasArg(args, ...flags) {
|
|
62
|
+
return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
63
|
+
}
|
|
64
|
+
function hasExplicitApprovalMode(args) {
|
|
65
|
+
return hasArg(args, "--approval-mode", "--yolo", "-y");
|
|
66
|
+
}
|
|
67
|
+
async function prepareGeminiMcpSettings(mcpConfigPath, env) {
|
|
68
|
+
const parsed = JSON.parse(await node_fs_1.promises.readFile(mcpConfigPath, "utf8"));
|
|
69
|
+
const mcpServers = {};
|
|
70
|
+
for (const [name, server] of Object.entries(parsed.mcpServers ?? {})) {
|
|
71
|
+
const httpUrl = server.httpUrl ?? (server.type === "http" ? server.url : undefined);
|
|
72
|
+
if (!httpUrl && !server.url)
|
|
73
|
+
continue;
|
|
74
|
+
const headers = {};
|
|
75
|
+
let headerIndex = 0;
|
|
76
|
+
for (const [header, value] of Object.entries(server.headers ?? {})) {
|
|
77
|
+
const envName = `NAVARCH_GEMINI_MCP_${safeEnvSegment(name)}_${headerIndex++}`;
|
|
78
|
+
env[envName] = value;
|
|
79
|
+
headers[header] = `\${${envName}}`;
|
|
80
|
+
}
|
|
81
|
+
mcpServers[name] = {
|
|
82
|
+
...(httpUrl ? { httpUrl } : { url: server.url }),
|
|
83
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const settingsPath = `${mcpConfigPath}.gemini-settings.json`;
|
|
87
|
+
await node_fs_1.promises.writeFile(settingsPath, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
|
|
88
|
+
await node_fs_1.promises.chmod(settingsPath, 0o600);
|
|
89
|
+
return { settingsPath, serverNames: Object.keys(mcpServers) };
|
|
90
|
+
}
|
|
91
|
+
function safeEnvSegment(value) {
|
|
92
|
+
return value.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
93
|
+
}
|
|
94
|
+
function parseGeminiStream(stdout) {
|
|
95
|
+
const events = [];
|
|
96
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
97
|
+
if (!line.trim())
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const event = JSON.parse(line);
|
|
101
|
+
if (event && typeof event === "object")
|
|
102
|
+
events.push(event);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Parser drift must not turn a successfully completed CLI run into a
|
|
106
|
+
// runtime crash. Unknown lines remain available in the transcript.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return events;
|
|
110
|
+
}
|
|
111
|
+
function attachGeminiOutput(result) {
|
|
112
|
+
const events = parseGeminiStream(result.stdout);
|
|
113
|
+
if (events.length === 0)
|
|
114
|
+
return result;
|
|
115
|
+
let reportText = "";
|
|
116
|
+
let tokensIn;
|
|
117
|
+
let tokensOut;
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
if (event.type === "tool_use") {
|
|
120
|
+
// Gemini emits pre-tool assistant text as message deltas. Only retain
|
|
121
|
+
// the answer produced after the last tool call as the completion report.
|
|
122
|
+
reportText = "";
|
|
123
|
+
}
|
|
124
|
+
else if (event.type === "message" &&
|
|
125
|
+
event.role === "assistant" &&
|
|
126
|
+
typeof event.content === "string") {
|
|
127
|
+
reportText = event.delta === false ? event.content : reportText + event.content;
|
|
128
|
+
}
|
|
129
|
+
else if (event.type === "result" && event.stats) {
|
|
130
|
+
const reportedInput = nonNegativeMetric(event.stats.input_tokens);
|
|
131
|
+
const reportedOutput = nonNegativeMetric(event.stats.output_tokens);
|
|
132
|
+
if (reportedInput !== undefined)
|
|
133
|
+
tokensIn = reportedInput;
|
|
134
|
+
if (reportedOutput !== undefined)
|
|
135
|
+
tokensOut = reportedOutput;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...result,
|
|
140
|
+
...(tokensIn !== undefined ? { tokensIn } : {}),
|
|
141
|
+
...(tokensOut !== undefined ? { tokensOut } : {}),
|
|
142
|
+
...(reportText.trim() ? { reportText: reportText.trim() } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function nonNegativeMetric(value) {
|
|
146
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
147
|
+
}
|
|
148
|
+
async function runOnHost(options, args) {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
let stdout = "";
|
|
151
|
+
let stderr = "";
|
|
152
|
+
let timedOut = false;
|
|
153
|
+
let killedByLeaseLoss = false;
|
|
154
|
+
const child = (0, node_child_process_1.spawn)(options.bin, args, {
|
|
155
|
+
cwd: options.cwd,
|
|
156
|
+
env: { ...process.env, ...options.env },
|
|
157
|
+
});
|
|
158
|
+
child.stdin?.end();
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
timedOut = true;
|
|
161
|
+
child.kill("SIGKILL");
|
|
162
|
+
}, options.timeoutMs);
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
killedByLeaseLoss = true;
|
|
165
|
+
child.kill("SIGKILL");
|
|
166
|
+
};
|
|
167
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
168
|
+
child.stdout.on("data", (data) => {
|
|
169
|
+
stdout += data.toString();
|
|
170
|
+
});
|
|
171
|
+
child.stderr.on("data", (data) => {
|
|
172
|
+
stderr += data.toString();
|
|
173
|
+
});
|
|
174
|
+
child.on("error", (err) => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
177
|
+
stderr += `\n${String(err)}`;
|
|
178
|
+
resolve({ exitCode: null, timedOut, killedByLeaseLoss, stdout, stderr });
|
|
179
|
+
});
|
|
180
|
+
child.on("close", (code) => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
183
|
+
resolve({ exitCode: code, timedOut, killedByLeaseLoss, stdout, stderr });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async function runViaDocker(options, args) {
|
|
188
|
+
const { containerName, runner } = options.dockerExec;
|
|
189
|
+
const command = `[ -f /tmp/session.env ] && . /tmp/session.env; cd repo 2>/dev/null; ${[
|
|
190
|
+
options.bin,
|
|
191
|
+
...args,
|
|
192
|
+
].map(shellQuote).join(" ")}`;
|
|
193
|
+
let killedByLeaseLoss = false;
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
killedByLeaseLoss = true;
|
|
196
|
+
runner.run("docker", ["kill", containerName]).catch(() => undefined);
|
|
197
|
+
};
|
|
198
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
199
|
+
try {
|
|
200
|
+
const forwardedEnv = Object.keys(options.env).flatMap((name) => ["--env", name]);
|
|
201
|
+
const result = await runner.run("docker", ["exec", ...forwardedEnv, containerName, "sh", "-c", command], { timeoutMs: options.timeoutMs, env: { ...process.env, ...options.env } });
|
|
202
|
+
return {
|
|
203
|
+
exitCode: result.code,
|
|
204
|
+
timedOut: false,
|
|
205
|
+
killedByLeaseLoss,
|
|
206
|
+
stdout: result.stdout,
|
|
207
|
+
stderr: result.stderr,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
return {
|
|
212
|
+
exitCode: null,
|
|
213
|
+
timedOut: false,
|
|
214
|
+
killedByLeaseLoss,
|
|
215
|
+
stdout: "",
|
|
216
|
+
stderr: String(err),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function shellQuote(value) {
|
|
224
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
225
|
+
}
|
|
226
|
+
exports.geminiAdapter = {
|
|
227
|
+
agentType: "gemini",
|
|
228
|
+
run: runGeminiAdapter,
|
|
229
|
+
};
|
package/dist/adapters/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
3
|
+
exports.runGeminiAdapter = exports.geminiAdapter = exports.runCodexAdapter = exports.codexAdapter = exports.runClaudeCodeAdapter = exports.claudeCodeAdapter = void 0;
|
|
4
4
|
exports.selectAdapter = selectAdapter;
|
|
5
5
|
const claude_cjs_1 = require("./claude.cjs");
|
|
6
6
|
Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return claude_cjs_1.claudeCodeAdapter; } });
|
|
@@ -8,6 +8,9 @@ Object.defineProperty(exports, "runClaudeCodeAdapter", { enumerable: true, get:
|
|
|
8
8
|
const codex_cjs_1 = require("./codex.cjs");
|
|
9
9
|
Object.defineProperty(exports, "codexAdapter", { enumerable: true, get: function () { return codex_cjs_1.codexAdapter; } });
|
|
10
10
|
Object.defineProperty(exports, "runCodexAdapter", { enumerable: true, get: function () { return codex_cjs_1.runCodexAdapter; } });
|
|
11
|
+
const gemini_cjs_1 = require("./gemini.cjs");
|
|
12
|
+
Object.defineProperty(exports, "geminiAdapter", { enumerable: true, get: function () { return gemini_cjs_1.geminiAdapter; } });
|
|
13
|
+
Object.defineProperty(exports, "runGeminiAdapter", { enumerable: true, get: function () { return gemini_cjs_1.runGeminiAdapter; } });
|
|
11
14
|
/**
|
|
12
15
|
* Picks the AgentAdapter (adapters/types.cts) session.cts should run a
|
|
13
16
|
* session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
|
|
@@ -19,10 +22,12 @@ function selectAdapter(agentType) {
|
|
|
19
22
|
switch (agentType) {
|
|
20
23
|
case "codex":
|
|
21
24
|
return codex_cjs_1.codexAdapter;
|
|
25
|
+
case "gemini":
|
|
26
|
+
return gemini_cjs_1.geminiAdapter;
|
|
22
27
|
case "claude-code":
|
|
23
28
|
return claude_cjs_1.claudeCodeAdapter;
|
|
24
29
|
default: {
|
|
25
|
-
// Exhaustiveness guard: config.cts only ever produces the
|
|
30
|
+
// Exhaustiveness guard: config.cts only ever produces the values
|
|
26
31
|
// above, but fall back to Claude Code rather than throwing if this
|
|
27
32
|
// widens in the future without every caller being updated.
|
|
28
33
|
const _exhaustive = agentType;
|
package/dist/api.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NavarchApiClient = exports.NavarchApiError = void 0;
|
|
3
|
+
exports.NavarchApiClient = exports.NavarchTransportError = exports.NavarchApiError = void 0;
|
|
4
4
|
class NavarchApiError extends Error {
|
|
5
5
|
status;
|
|
6
6
|
body;
|
|
@@ -12,6 +12,18 @@ class NavarchApiError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
exports.NavarchApiError = NavarchApiError;
|
|
15
|
+
/** A request that failed before the control plane returned an HTTP response. */
|
|
16
|
+
class NavarchTransportError extends Error {
|
|
17
|
+
method;
|
|
18
|
+
endpoint;
|
|
19
|
+
constructor(method, endpoint, cause) {
|
|
20
|
+
super(`Navarch API ${method} ${endpoint} transport failed: ${describeError(cause)}`, { cause });
|
|
21
|
+
this.method = method;
|
|
22
|
+
this.endpoint = endpoint;
|
|
23
|
+
this.name = "NavarchTransportError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.NavarchTransportError = NavarchTransportError;
|
|
15
27
|
/**
|
|
16
28
|
* Typed client for the Navarch control-plane API surface WP-07 depends on:
|
|
17
29
|
* dispatch/claim, per-lease heartbeat, complete, and broker/issue
|
|
@@ -44,11 +56,20 @@ class NavarchApiClient {
|
|
|
44
56
|
}
|
|
45
57
|
headers.authorization = `Bearer ${this.token}`;
|
|
46
58
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
const requestUrl = `${this.baseUrl}${pathname}`;
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await this.fetchImpl(requestUrl, {
|
|
63
|
+
method,
|
|
64
|
+
headers,
|
|
65
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Include enough request/cause context to diagnose DNS, connection, and
|
|
70
|
+
// TLS failures. Deliberately omit headers and URL credentials/query data.
|
|
71
|
+
throw new NavarchTransportError(method, safeEndpoint(requestUrl, pathname), err);
|
|
72
|
+
}
|
|
52
73
|
if (response.status === 204)
|
|
53
74
|
return null;
|
|
54
75
|
const text = await response.text();
|
|
@@ -133,3 +154,25 @@ function safeJsonParse(text) {
|
|
|
133
154
|
return null;
|
|
134
155
|
}
|
|
135
156
|
}
|
|
157
|
+
function safeEndpoint(requestUrl, pathname) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = new URL(requestUrl);
|
|
160
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return pathname;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function describeError(err) {
|
|
167
|
+
if (!(err instanceof Error))
|
|
168
|
+
return String(err);
|
|
169
|
+
let detail = `${err.name}: ${err.message}`;
|
|
170
|
+
const cause = err.cause;
|
|
171
|
+
if (cause instanceof Error) {
|
|
172
|
+
detail += `; cause: ${cause.name}: ${cause.message}`;
|
|
173
|
+
}
|
|
174
|
+
else if (cause && typeof cause === "object" && "code" in cause) {
|
|
175
|
+
detail += `; cause code: ${String(cause.code)}`;
|
|
176
|
+
}
|
|
177
|
+
return detail;
|
|
178
|
+
}
|
package/dist/claim-loop.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
5
5
|
const api_cjs_1 = require("./api.cjs");
|
|
6
6
|
const logger_cjs_1 = require("./logger.cjs");
|
|
7
7
|
const log = (0, logger_cjs_1.createLogger)("claim");
|
|
8
|
+
const MAX_CLAIM_BACKOFF_MS = 60_000;
|
|
8
9
|
/**
|
|
9
10
|
* Polls dispatch/claim on an interval, gated by available capacity
|
|
10
11
|
* (implementation-plan.md WP-07 "Claim loop: poll dispatch, receive context
|
|
@@ -20,6 +21,8 @@ class ClaimLoop {
|
|
|
20
21
|
timer = null;
|
|
21
22
|
stopped = false;
|
|
22
23
|
claimInFlight = false;
|
|
24
|
+
consecutiveFailures = 0;
|
|
25
|
+
nextClaimAt = 0;
|
|
23
26
|
quiescenceWaiters = new Set();
|
|
24
27
|
constructor(api, config, capacity, runSession) {
|
|
25
28
|
this.api = api;
|
|
@@ -31,6 +34,8 @@ class ClaimLoop {
|
|
|
31
34
|
if (this.timer)
|
|
32
35
|
return;
|
|
33
36
|
this.stopped = false;
|
|
37
|
+
this.consecutiveFailures = 0;
|
|
38
|
+
this.nextClaimAt = 0;
|
|
34
39
|
this.timer = setInterval(() => void this.tick(), this.config.pollIntervalMs);
|
|
35
40
|
}
|
|
36
41
|
stop() {
|
|
@@ -52,7 +57,10 @@ class ClaimLoop {
|
|
|
52
57
|
await new Promise((resolve) => this.quiescenceWaiters.add(resolve));
|
|
53
58
|
}
|
|
54
59
|
async tick() {
|
|
55
|
-
if (this.stopped ||
|
|
60
|
+
if (this.stopped ||
|
|
61
|
+
this.claimInFlight ||
|
|
62
|
+
!this.capacity.hasCapacity() ||
|
|
63
|
+
Date.now() < this.nextClaimAt)
|
|
56
64
|
return;
|
|
57
65
|
this.claimInFlight = true;
|
|
58
66
|
try {
|
|
@@ -64,8 +72,14 @@ class ClaimLoop {
|
|
|
64
72
|
available_capacity: this.capacity.available(),
|
|
65
73
|
capabilities: this.config.capabilities,
|
|
66
74
|
agent_type: this.config.agentType,
|
|
75
|
+
runtimes: this.config.runtimes,
|
|
67
76
|
session_id: sessionId,
|
|
68
77
|
});
|
|
78
|
+
if (this.consecutiveFailures > 0) {
|
|
79
|
+
log.info(`claim polling recovered after ${this.consecutiveFailures} failed attempt(s)`);
|
|
80
|
+
}
|
|
81
|
+
this.consecutiveFailures = 0;
|
|
82
|
+
this.nextClaimAt = 0;
|
|
69
83
|
if (!claimed)
|
|
70
84
|
return;
|
|
71
85
|
this.capacity.acquire(claimed.lease_id);
|
|
@@ -75,16 +89,20 @@ class ClaimLoop {
|
|
|
75
89
|
.finally(() => this.capacity.release(claimed.lease_id));
|
|
76
90
|
}
|
|
77
91
|
catch (err) {
|
|
92
|
+
this.consecutiveFailures += 1;
|
|
93
|
+
const retryDelayMs = Math.min(MAX_CLAIM_BACKOFF_MS, this.config.pollIntervalMs * 2 ** this.consecutiveFailures);
|
|
94
|
+
this.nextClaimAt = Date.now() + retryDelayMs;
|
|
78
95
|
// NavarchApiError's message is only the status line ("… failed with
|
|
79
96
|
// 500"); the control plane's actual error text lives in `.body`. Log it
|
|
80
97
|
// so a server-side claim failure is diagnosable from the runtime alone
|
|
81
98
|
// instead of an opaque bare status.
|
|
82
99
|
if (err instanceof api_cjs_1.NavarchApiError) {
|
|
83
100
|
const detail = typeof err.body === "string" ? err.body : JSON.stringify(err.body);
|
|
84
|
-
log.warn(`claim failed
|
|
101
|
+
log.warn(`claim failed (attempt ${this.consecutiveFailures}; retrying in ${retryDelayMs}ms): ` +
|
|
102
|
+
`${err.message}${detail ? ` — ${detail}` : ""}`);
|
|
85
103
|
}
|
|
86
104
|
else {
|
|
87
|
-
log.warn(`claim failed: ${String(err)}`);
|
|
105
|
+
log.warn(`claim failed (attempt ${this.consecutiveFailures}; retrying in ${retryDelayMs}ms): ${String(err)}`);
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
finally {
|
package/dist/cli.cjs
CHANGED
|
@@ -65,7 +65,7 @@ function agentFromFlag(flags) {
|
|
|
65
65
|
if (value === undefined)
|
|
66
66
|
return undefined;
|
|
67
67
|
if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
|
|
68
|
-
throw new Error("--agent must be
|
|
68
|
+
throw new Error("--agent must be one of 'claude-code', 'codex', or 'gemini'.");
|
|
69
69
|
}
|
|
70
70
|
return value;
|
|
71
71
|
}
|
|
@@ -276,11 +276,11 @@ function helpText() {
|
|
|
276
276
|
|
|
277
277
|
Usage:
|
|
278
278
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
279
|
-
[--config-dir <path>] [--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
279
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
280
280
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
281
|
-
[--config-dir <path>] [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
282
|
-
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex]
|
|
283
|
-
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex]
|
|
281
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
282
|
+
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini]
|
|
283
|
+
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini]
|
|
284
284
|
navarch-runtime doctor [--config-dir <path>]
|
|
285
285
|
|
|
286
286
|
Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
|
package/dist/config.cjs
CHANGED
|
@@ -8,7 +8,7 @@ exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
10
|
function isRuntimeAgentType(value) {
|
|
11
|
-
return value === "claude-code" || value === "codex";
|
|
11
|
+
return value === "claude-code" || value === "codex" || value === "gemini";
|
|
12
12
|
}
|
|
13
13
|
function envInt(env, name, fallback) {
|
|
14
14
|
const raw = env[name];
|
|
@@ -38,7 +38,9 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
38
38
|
// default. Docker isolation is an explicit operator opt-in, not a
|
|
39
39
|
// prerequisite for claiming ordinary shell work.
|
|
40
40
|
const sandboxMode = env.NAVARCH_SANDBOX_MODE === "docker" ? "docker" : "host";
|
|
41
|
-
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell"];
|
|
41
|
+
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell", "browser-use"];
|
|
42
|
+
const agentType = isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code";
|
|
43
|
+
const runtimes = envList(env, "NAVARCH_RUNTIMES", [agentType]).filter(isRuntimeAgentType);
|
|
42
44
|
return {
|
|
43
45
|
apiBase: env.NAVARCH_API_BASE ?? "http://localhost:3000",
|
|
44
46
|
workspaceRoot: env.NAVARCH_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
|
|
@@ -52,11 +54,14 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
52
54
|
// interval must stay comfortably under that TTL.
|
|
53
55
|
leaseHeartbeatIntervalMs,
|
|
54
56
|
sessionTimeoutMs: envInt(env, "NAVARCH_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
|
|
55
|
-
agentType
|
|
57
|
+
agentType,
|
|
58
|
+
runtimes: runtimes.length > 0 ? runtimes : [agentType],
|
|
56
59
|
claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
|
|
57
60
|
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
58
61
|
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
59
62
|
codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
|
|
63
|
+
geminiBin: env.NAVARCH_GEMINI_BIN ?? "gemini",
|
|
64
|
+
geminiExtraArgs: envList(env, "NAVARCH_GEMINI_EXTRA_ARGS", []),
|
|
60
65
|
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
61
66
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
62
67
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
package/dist/machine-store.cjs
CHANGED
|
@@ -48,7 +48,9 @@ async function resolveMachineIdentity(configDir, apiBaseFallback) {
|
|
|
48
48
|
token: envToken,
|
|
49
49
|
name: process.env.NAVARCH_MACHINE_NAME ?? envId,
|
|
50
50
|
api_base: process.env.NAVARCH_API_BASE ?? apiBaseFallback,
|
|
51
|
-
agent_type: process.env.NAVARCH_AGENT === "codex" ||
|
|
51
|
+
agent_type: process.env.NAVARCH_AGENT === "codex" ||
|
|
52
|
+
process.env.NAVARCH_AGENT === "claude-code" ||
|
|
53
|
+
process.env.NAVARCH_AGENT === "gemini"
|
|
52
54
|
? process.env.NAVARCH_AGENT
|
|
53
55
|
: undefined,
|
|
54
56
|
};
|
package/dist/session.cjs
CHANGED
|
@@ -22,7 +22,7 @@ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
|
22
22
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
23
23
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
24
24
|
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
25
|
-
const
|
|
25
|
+
const COMPLETION_REMEDIATION_RETRIES = 2;
|
|
26
26
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
27
27
|
/**
|
|
28
28
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
@@ -30,8 +30,8 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
30
30
|
* 2. fetch secrets from the broker at session start (managed GitHub git
|
|
31
31
|
* credentials are subsequently refreshed by a lease-scoped helper)
|
|
32
32
|
* 3. optionally stand up a Docker sandbox when explicitly configured
|
|
33
|
-
* 4. run the
|
|
34
|
-
*
|
|
33
|
+
* 4. run the adapter selected by the claim (Claude Code, Codex, or Gemini;
|
|
34
|
+
* adapters/index.cts#selectAdapter), heartbeating the
|
|
35
35
|
* lease throughout
|
|
36
36
|
* 5. redact + upload the transcript, map the exit condition, complete the
|
|
37
37
|
* lease (recording which agent_type ran it)
|
|
@@ -46,9 +46,14 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
46
46
|
async function runSession(deps, claimed, sessionId) {
|
|
47
47
|
const { api, config } = deps;
|
|
48
48
|
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
49
|
+
const runtime = claimed.runtime ?? config.agentType;
|
|
49
50
|
const execution = bundle.execution ?? {
|
|
50
51
|
profile: task.execution_profile ?? "standard",
|
|
51
|
-
model:
|
|
52
|
+
model: runtime === "codex"
|
|
53
|
+
? "gpt-5.6-sol"
|
|
54
|
+
: runtime === "gemini"
|
|
55
|
+
? "auto"
|
|
56
|
+
: "best",
|
|
52
57
|
reasoning_effort: "medium",
|
|
53
58
|
};
|
|
54
59
|
const executionReport = {
|
|
@@ -98,7 +103,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
98
103
|
evidence_urls: [],
|
|
99
104
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
100
105
|
exit_status: "crashed",
|
|
101
|
-
agent_type:
|
|
106
|
+
agent_type: runtime,
|
|
102
107
|
...executionReport,
|
|
103
108
|
});
|
|
104
109
|
secrets = {};
|
|
@@ -122,6 +127,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
122
127
|
let pendingGuidance = [];
|
|
123
128
|
let activeAbortController = null;
|
|
124
129
|
let leaseLost = false;
|
|
130
|
+
let leaseGone = false;
|
|
125
131
|
let heartbeatInFlight = null;
|
|
126
132
|
const pollLease = () => {
|
|
127
133
|
if (heartbeatInFlight)
|
|
@@ -146,6 +152,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
146
152
|
.catch((err) => {
|
|
147
153
|
log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
|
|
148
154
|
leaseLost = true;
|
|
155
|
+
leaseGone = isTerminalLeaseHeartbeatError(err);
|
|
149
156
|
activeAbortController?.abort();
|
|
150
157
|
})
|
|
151
158
|
.finally(() => {
|
|
@@ -167,7 +174,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
167
174
|
evidence_urls: [],
|
|
168
175
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
169
176
|
exit_status: "crashed",
|
|
170
|
-
agent_type:
|
|
177
|
+
agent_type: runtime,
|
|
171
178
|
...executionReport,
|
|
172
179
|
})
|
|
173
180
|
.catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
|
|
@@ -208,11 +215,12 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
208
215
|
// machine. Host-mode Claude gets a generated PreToolUse hook; host-mode
|
|
209
216
|
// Codex gets an OS-enforced native permission profile over the same roots.
|
|
210
217
|
// Docker mode already has a container boundary. NAVARCH_WORKTREE_GUARD=off
|
|
211
|
-
// opts out for
|
|
218
|
+
// opts out for any agent.
|
|
212
219
|
let claudeSettingsPath = null;
|
|
213
220
|
let codexGuardArgs;
|
|
221
|
+
let geminiGuardMounts;
|
|
214
222
|
if (config.sandboxMode === "host" && config.worktreeGuard) {
|
|
215
|
-
if (
|
|
223
|
+
if (runtime === "claude-code") {
|
|
216
224
|
if (config.claudeExtraArgs.includes("--settings")) {
|
|
217
225
|
log.warn("NAVARCH_CLAUDE_EXTRA_ARGS supplies --settings; skipping the generated worktree-guard settings for this session.");
|
|
218
226
|
}
|
|
@@ -227,7 +235,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
227
235
|
claudeSettingsPath = guard.settingsPath;
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
|
-
else {
|
|
238
|
+
else if (runtime === "codex") {
|
|
231
239
|
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
232
240
|
workDir,
|
|
233
241
|
worktreePath: gitWorktree.worktreePath,
|
|
@@ -236,6 +244,15 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
236
244
|
extraRoots: config.guardExtraRoots,
|
|
237
245
|
});
|
|
238
246
|
}
|
|
247
|
+
else {
|
|
248
|
+
geminiGuardMounts = (0, worktree_guard_cjs_1.geminiSandboxMounts)({
|
|
249
|
+
workDir,
|
|
250
|
+
worktreePath: gitWorktree.worktreePath,
|
|
251
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
252
|
+
workspaceRoot: config.workspaceRoot,
|
|
253
|
+
extraRoots: config.guardExtraRoots,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
239
256
|
}
|
|
240
257
|
try {
|
|
241
258
|
await gitWorktree.prepare();
|
|
@@ -243,17 +260,16 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
243
260
|
await sandbox.create();
|
|
244
261
|
await sandbox.injectEnv(toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail));
|
|
245
262
|
}
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
263
|
+
// The control plane resolves the project default/task override before
|
|
264
|
+
// claim and returns one of this worker's advertised runtimes.
|
|
265
|
+
// All adapters implement the same AgentAdapter.run() shape
|
|
249
266
|
// (adapters/types.cts), so nothing else in this function branches on
|
|
250
267
|
// which agent is running.
|
|
251
|
-
const adapter = (0, index_cjs_1.selectAdapter)(
|
|
252
|
-
const bin
|
|
253
|
-
const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
|
|
268
|
+
const adapter = (0, index_cjs_1.selectAdapter)(runtime);
|
|
269
|
+
const { bin, extraArgs } = adapterCommand(config, runtime);
|
|
254
270
|
const attempts = [];
|
|
255
271
|
let nextPrompt = null;
|
|
256
|
-
let
|
|
272
|
+
let completionRemediationRetries = 0;
|
|
257
273
|
while (true) {
|
|
258
274
|
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
259
275
|
// It is already included in deliveredGuidance, so clear the pending
|
|
@@ -279,6 +295,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
279
295
|
env: toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail),
|
|
280
296
|
settingsPath: claudeSettingsPath,
|
|
281
297
|
codexGuardArgs,
|
|
298
|
+
geminiSandboxMounts: geminiGuardMounts,
|
|
282
299
|
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
283
300
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
284
301
|
signal: activeAbortController.signal,
|
|
@@ -287,8 +304,15 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
287
304
|
attempts.push(turnResult);
|
|
288
305
|
// Close the small race between a naturally completed turn and the next
|
|
289
306
|
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
290
|
-
// lease can be completed.
|
|
291
|
-
|
|
307
|
+
// lease can be completed. A terminal heartbeat response means the
|
|
308
|
+
// control plane has already disposed of the lease, so neither retry the
|
|
309
|
+
// heartbeat nor attempt completion against that stale lease.
|
|
310
|
+
if (!leaseGone)
|
|
311
|
+
await pollLease();
|
|
312
|
+
if (leaseGone) {
|
|
313
|
+
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
292
316
|
if (!leaseLost && pendingGuidance.length > 0)
|
|
293
317
|
continue;
|
|
294
318
|
const result = {
|
|
@@ -349,7 +373,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
349
373
|
},
|
|
350
374
|
transcript_url: transcriptUrl,
|
|
351
375
|
exit_status: mapping.exitStatus,
|
|
352
|
-
agent_type:
|
|
376
|
+
agent_type: runtime,
|
|
353
377
|
...executionReport,
|
|
354
378
|
};
|
|
355
379
|
try {
|
|
@@ -357,17 +381,21 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
357
381
|
break;
|
|
358
382
|
}
|
|
359
383
|
catch (err) {
|
|
360
|
-
|
|
384
|
+
if (isAlreadyReleasedCompletionError(err)) {
|
|
385
|
+
log.warn(`completion skipped for ${leaseId}: the lease was already released.`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const rejection = mapping.leaseOutcome === "completed" ? completionRemediationMessage(err) : null;
|
|
361
389
|
if (!rejection)
|
|
362
390
|
throw err;
|
|
363
391
|
const redactedRejection = (0, redact_cjs_1.redactText)(rejection, knownSecrets);
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
log.warn(`completion for ${leaseId}
|
|
392
|
+
if (completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
|
|
393
|
+
completionRemediationRetries += 1;
|
|
394
|
+
log.warn(`completion for ${leaseId} needs more work; restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
367
395
|
nextPrompt = redactedRejection;
|
|
368
396
|
continue;
|
|
369
397
|
}
|
|
370
|
-
log.warn(`completion for ${leaseId} still
|
|
398
|
+
log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries; failing with the control-plane rejection.`);
|
|
371
399
|
await api.completeLease(leaseId, {
|
|
372
400
|
...completion,
|
|
373
401
|
status: "failed",
|
|
@@ -390,7 +418,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
390
418
|
evidence_urls: [],
|
|
391
419
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
392
420
|
exit_status: "crashed",
|
|
393
|
-
agent_type:
|
|
421
|
+
agent_type: runtime,
|
|
394
422
|
...executionReport,
|
|
395
423
|
})
|
|
396
424
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
@@ -405,6 +433,16 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
405
433
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
406
434
|
}
|
|
407
435
|
}
|
|
436
|
+
function adapterCommand(config, runtime) {
|
|
437
|
+
switch (runtime) {
|
|
438
|
+
case "claude-code":
|
|
439
|
+
return { bin: config.claudeBin, extraArgs: config.claudeExtraArgs };
|
|
440
|
+
case "codex":
|
|
441
|
+
return { bin: config.codexBin, extraArgs: config.codexExtraArgs };
|
|
442
|
+
case "gemini":
|
|
443
|
+
return { bin: config.geminiBin, extraArgs: config.geminiExtraArgs };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
408
446
|
function sumReportedUsage(attempts, key) {
|
|
409
447
|
const reported = attempts.flatMap((attempt) => {
|
|
410
448
|
const value = attempt[key];
|
|
@@ -412,16 +450,28 @@ function sumReportedUsage(attempts, key) {
|
|
|
412
450
|
});
|
|
413
451
|
return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
|
|
414
452
|
}
|
|
415
|
-
function
|
|
453
|
+
function completionRemediationMessage(err) {
|
|
416
454
|
if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
|
|
417
455
|
return null;
|
|
418
456
|
if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
|
|
419
457
|
return null;
|
|
420
458
|
const body = err.body;
|
|
421
|
-
return body.code === "pr_required"
|
|
459
|
+
return (body.code === "pr_required" || body.code === "pr_not_ready") &&
|
|
460
|
+
typeof body.error === "string"
|
|
422
461
|
? body.error
|
|
423
462
|
: null;
|
|
424
463
|
}
|
|
464
|
+
function isTerminalLeaseHeartbeatError(err) {
|
|
465
|
+
return err instanceof api_cjs_1.NavarchApiError && [403, 404, 410].includes(err.status);
|
|
466
|
+
}
|
|
467
|
+
function isAlreadyReleasedCompletionError(err) {
|
|
468
|
+
if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
|
|
469
|
+
return false;
|
|
470
|
+
if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
|
|
471
|
+
return false;
|
|
472
|
+
const body = err.body;
|
|
473
|
+
return body.error === "lease already released";
|
|
474
|
+
}
|
|
425
475
|
/** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
|
|
426
476
|
function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
|
|
427
477
|
// Keep the existing `(secrets, authorName, authorEmail)` call shape while
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -5,9 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.guardHookScriptPath = guardHookScriptPath;
|
|
7
7
|
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
8
|
+
exports.codexToolReadRoots = codexToolReadRoots;
|
|
8
9
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
10
|
+
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
9
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
12
|
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
14
|
const CODEX_GUARD_PROFILE = "navarch-worktree";
|
|
12
15
|
/**
|
|
13
16
|
* Tools the hook screens. Everything else — the lease-scoped Navarch MCP
|
|
@@ -55,6 +58,33 @@ async function prepareWorktreeGuard(options) {
|
|
|
55
58
|
await node_fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
56
59
|
return { settingsPath, configPath, hookScriptPath };
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Read-only roots required by Codex's own tools and installed skills.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately exclude the CODEX_HOME root itself: it may contain auth.json
|
|
65
|
+
* and user configuration. Package executables, installed skill instructions,
|
|
66
|
+
* plugin code, the operator's executable directory, and browser/toolchain
|
|
67
|
+
* installations are sufficient for helpers such as apply_patch, npm, gh, and
|
|
68
|
+
* browser evidence tooling.
|
|
69
|
+
*/
|
|
70
|
+
function codexToolReadRoots(env = process.env) {
|
|
71
|
+
const homeDir = env.HOME?.trim() || node_os_1.default.homedir();
|
|
72
|
+
const codexHome = env.CODEX_HOME?.trim() || node_path_1.default.join(homeDir, ".codex");
|
|
73
|
+
return [
|
|
74
|
+
node_path_1.default.join(codexHome, "packages"),
|
|
75
|
+
node_path_1.default.join(codexHome, "skills"),
|
|
76
|
+
node_path_1.default.join(codexHome, "plugins"),
|
|
77
|
+
node_path_1.default.join(homeDir, ".agents", "skills"),
|
|
78
|
+
node_path_1.default.join(homeDir, ".local", "bin"),
|
|
79
|
+
"/opt/homebrew",
|
|
80
|
+
// AppKit loads this bundle even for headless Chromium. Without it,
|
|
81
|
+
// Playwright browsers abort before launch on macOS with
|
|
82
|
+
// "required built-in appearance SystemAppearance not found".
|
|
83
|
+
"/System/Library/CoreServices/SystemAppearance.bundle",
|
|
84
|
+
node_path_1.default.join(homeDir, "Library", "Caches", "ms-playwright"),
|
|
85
|
+
node_path_1.default.join(homeDir, ".cache", "ms-playwright"),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
58
88
|
/**
|
|
59
89
|
* Builds one-off Codex permission-profile arguments for a host session.
|
|
60
90
|
*
|
|
@@ -78,6 +108,12 @@ function codexWorktreeGuardArgs(options) {
|
|
|
78
108
|
[node_path_1.default.resolve(options.worktreePath)]: "write",
|
|
79
109
|
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
80
110
|
};
|
|
111
|
+
for (const root of codexToolReadRoots()) {
|
|
112
|
+
const resolved = node_path_1.default.resolve(root);
|
|
113
|
+
if (isPathInside(resolved, node_path_1.default.resolve(options.workspaceRoot)))
|
|
114
|
+
continue;
|
|
115
|
+
filesystem[resolved] = "read";
|
|
116
|
+
}
|
|
81
117
|
for (const root of options.extraRoots ?? []) {
|
|
82
118
|
const resolved = node_path_1.default.resolve(root);
|
|
83
119
|
// Match the Claude hook's denied-root precedence: an extra root cannot
|
|
@@ -106,6 +142,21 @@ function codexWorktreeGuardArgs(options) {
|
|
|
106
142
|
`permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
|
|
107
143
|
];
|
|
108
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Mounts only the current session roots into Gemini CLI's native sandbox.
|
|
147
|
+
* Gemini mounts cwd itself; explicit same-path mounts keep the shared gitdir
|
|
148
|
+
* and approved external roots reachable without exposing sibling sessions.
|
|
149
|
+
*/
|
|
150
|
+
function geminiSandboxMounts(options) {
|
|
151
|
+
const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot);
|
|
152
|
+
const roots = [options.worktreePath, options.repositoryPath];
|
|
153
|
+
for (const root of options.extraRoots ?? []) {
|
|
154
|
+
const resolved = node_path_1.default.resolve(root);
|
|
155
|
+
if (!isPathInside(resolved, workspaceRoot))
|
|
156
|
+
roots.push(resolved);
|
|
157
|
+
}
|
|
158
|
+
return [...new Set(roots.map((root) => node_path_1.default.resolve(root)))].map((root) => `${root}:${root}:rw`);
|
|
159
|
+
}
|
|
109
160
|
function isPathInside(candidate, root) {
|
|
110
161
|
const relative = node_path_1.default.relative(root, candidate);
|
|
111
162
|
return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Navarch machine-side session manager:
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, or Gemini CLI.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
"ai-agent",
|
|
16
16
|
"claude-code",
|
|
17
17
|
"codex",
|
|
18
|
+
"gemini-cli",
|
|
18
19
|
"task-runner"
|
|
19
20
|
],
|
|
20
21
|
"bin": {
|
|
21
|
-
"navarch-runtime": "
|
|
22
|
+
"navarch-runtime": "bin/navarch.cjs"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"dist",
|