@sagentlab/navarch-runtime 0.1.11 → 0.1.13
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 +119 -47
- package/dist/adapter-capacity.cjs +75 -0
- package/dist/adapters/claude.cjs +16 -1
- package/dist/adapters/gemini.cjs +229 -0
- package/dist/adapters/index.cjs +7 -2
- package/dist/claim-loop.cjs +10 -1
- package/dist/cli.cjs +5 -5
- package/dist/config.cjs +12 -4
- package/dist/machine-store.cjs +3 -1
- package/dist/session.cjs +84 -27
- package/dist/worktree-guard.cjs +20 -0
- package/package.json +3 -2
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` | `
|
|
223
|
-
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `
|
|
257
|
+
| `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. |
|
|
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
|
-
| `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`. |
|
|
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`. Runtime sessions default to an empty `--setting-sources` list so machine/user/project hooks cannot leak into temporary checkouts; supply `--setting-sources=<sources>` here to opt in deliberately. |
|
|
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)
|
|
@@ -242,6 +280,9 @@ supported coding agents, using each CLI's native enforcement point:
|
|
|
242
280
|
does not blanket-preapprove the lease-scoped Navarch MCP tools. A generated
|
|
243
281
|
settings file (`src/worktree-guard.cts`, passed as `--settings`) also installs
|
|
244
282
|
`bin/worktree-guard-hook.cjs` as a fail-closed `PreToolUse` boundary hook.
|
|
283
|
+
User, project, and local settings sources are disabled by default, preventing
|
|
284
|
+
host-only hooks and plugins from leaking into unattended sessions; the
|
|
285
|
+
explicit generated settings file remains active.
|
|
245
286
|
- **Codex:** the runtime passes a one-off native permission profile with
|
|
246
287
|
`approval_policy="on-request"` and `approvals_reviewer="auto_review"`.
|
|
247
288
|
Codex's OS sandbox grants read/write access only to the allowed roots and
|
|
@@ -251,6 +292,12 @@ supported coding agents, using each CLI's native enforcement point:
|
|
|
251
292
|
user or checked-in legacy `sandbox_mode` from silently disabling the
|
|
252
293
|
generated profile; Codex authentication still comes from `CODEX_HOME`, and
|
|
253
294
|
repository instructions such as `AGENTS.md` still load.
|
|
295
|
+
- **Gemini:** the runtime uses Gemini CLI's native sandbox with only the
|
|
296
|
+
current worktree, shared gitdir, and approved extra roots mounted. The
|
|
297
|
+
lease MCP config is translated to a read-only temporary settings file;
|
|
298
|
+
header values remain environment-variable references rather than literals.
|
|
299
|
+
Docker-mode sessions disable Gemini's implicit YOLO sandbox to avoid nesting
|
|
300
|
+
it inside Navarch's already isolated session container.
|
|
254
301
|
|
|
255
302
|
The resulting boundary is:
|
|
256
303
|
|
|
@@ -284,12 +331,11 @@ The auto-review launch paths were verified live on 2026-07-20 with Claude Code
|
|
|
284
331
|
permission profile plus `approvals_reviewer="auto_review"`). Both completed an
|
|
285
332
|
unattended smoke task successfully.
|
|
286
333
|
|
|
287
|
-
## Choosing an agent
|
|
334
|
+
## Choosing an agent
|
|
288
335
|
|
|
289
|
-
Each
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
environment:
|
|
336
|
+
Each worker advertises the agent CLIs it can actually execute. Existing
|
|
337
|
+
single-runtime installs keep using `--agent`/`NAVARCH_AGENT`; multi-runtime
|
|
338
|
+
workers set `NAVARCH_RUNTIMES` to the installed and authenticated adapters:
|
|
293
339
|
|
|
294
340
|
```sh
|
|
295
341
|
# Claude Code (default) — requires the `claude` CLI installed and
|
|
@@ -300,12 +346,22 @@ export NAVARCH_AGENT=claude-code
|
|
|
300
346
|
# this machine (or NAVARCH_CODEX_BIN pointing at it), analogous to the
|
|
301
347
|
# Claude Code prerequisite above.
|
|
302
348
|
export NAVARCH_AGENT=codex
|
|
349
|
+
|
|
350
|
+
# Google Gemini CLI — requires an authenticated `gemini` CLI (or
|
|
351
|
+
# NAVARCH_GEMINI_BIN pointing at it).
|
|
352
|
+
export NAVARCH_AGENT=gemini
|
|
353
|
+
|
|
354
|
+
# Or let one worker serve tasks selected for any installed adapter.
|
|
355
|
+
export NAVARCH_RUNTIMES=claude-code,codex,gemini
|
|
303
356
|
```
|
|
304
357
|
|
|
305
|
-
|
|
306
|
-
`
|
|
358
|
+
For the legacy single-runtime setting, priority is `start --agent` →
|
|
359
|
+
`NAVARCH_AGENT` → the locally saved choice → `claude-code`.
|
|
360
|
+
`NAVARCH_RUNTIMES` expands what the worker advertises; the control plane then
|
|
361
|
+
resolves `tasks.runtime_override` → `projects.default_runtime` and returns the
|
|
362
|
+
selected adapter with the claim.
|
|
307
363
|
|
|
308
|
-
|
|
364
|
+
All three adapters implement the same `AgentAdapter` interface
|
|
309
365
|
(`src/adapters/types.cts`) and run either directly on the host or via
|
|
310
366
|
`docker exec` in the session's sandbox container, exactly like the Claude
|
|
311
367
|
adapter always has — `session.cts` picks one (`src/adapters/index.cts`'s
|
|
@@ -314,10 +370,12 @@ through to `complete()` unchanged by whatever happened during the run.
|
|
|
314
370
|
|
|
315
371
|
The control plane also resolves the project's model and the task's execution
|
|
316
372
|
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
|
-
|
|
373
|
+
overrides (`codex exec --model ... -c model_reasoning_effort=...`,
|
|
374
|
+
`claude -p --model ... --effort ...`, or `gemini --model ...`) and records the
|
|
375
|
+
effective model, profile, and effort on completion. Gemini currently uses its
|
|
376
|
+
`auto` model default and does not expose a reasoning-effort flag. Machine-wide
|
|
377
|
+
extra arguments still configure other CLI behavior; dispatched model policy
|
|
378
|
+
wins.
|
|
321
379
|
|
|
322
380
|
The Codex CLI invocation was verified against `codex-cli 0.144.1` on
|
|
323
381
|
2026-07-18. The runtime uses `codex exec "<prompt>" --json` and translates
|
|
@@ -327,6 +385,12 @@ not placed in argv. The JSONL parser accepts the verified top-level
|
|
|
327
385
|
`item.completed` / `turn.completed` shape and retains the older `msg`
|
|
328
386
|
envelope as a compatibility fallback.
|
|
329
387
|
|
|
388
|
+
The Gemini adapter uses official headless `--prompt` and `--output-format
|
|
389
|
+
stream-json` flags. It resets pre-tool assistant text, retains the final
|
|
390
|
+
post-tool answer as the report, and reads aggregate `input_tokens` and
|
|
391
|
+
`output_tokens` from the terminal `result` event. Gemini does not report USD
|
|
392
|
+
cost, so an unknown value remains absent rather than becoming zero.
|
|
393
|
+
|
|
330
394
|
## Architecture
|
|
331
395
|
|
|
332
396
|
```
|
|
@@ -346,6 +410,7 @@ cli.cts
|
|
|
346
410
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
347
411
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
|
|
348
412
|
- codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json -c mcp_servers.*=...`
|
|
413
|
+
- geminiAdapter (adapters/gemini.cts) — `gemini --prompt <prompt> --output-format stream-json`
|
|
349
414
|
heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
|
|
350
415
|
a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
|
|
351
416
|
5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
|
|
@@ -360,7 +425,7 @@ cli.cts
|
|
|
360
425
|
|
|
361
426
|
`adapter.cts` (top-level) is now a backward-compat re-export of
|
|
362
427
|
`adapters/claude.cts` — new code should import `adapters/index.cts` (for
|
|
363
|
-
`selectAdapter`) or
|
|
428
|
+
`selectAdapter`) or an adapter module directly.
|
|
364
429
|
|
|
365
430
|
`api.cts` is the single choke point for every HTTP call; nothing else in the
|
|
366
431
|
package talks to `fetch` directly for control-plane traffic.
|
|
@@ -411,8 +476,8 @@ tests cover:
|
|
|
411
476
|
plus Claude's and Codex's usage/report parsing (`tests/exit-conditions.test.cts`).
|
|
412
477
|
- `redact.cts` — exact-value and pattern-based redaction (`tests/redact.test.cts`).
|
|
413
478
|
- `capacity.cts` — capacity math and acquire/release bookkeeping (`tests/capacity.test.cts`).
|
|
414
|
-
- `config.cts` — env var parsing and defaults
|
|
415
|
-
|
|
479
|
+
- `config.cts` — env var parsing and defaults for every selectable adapter
|
|
480
|
+
(`tests/config.test.cts`).
|
|
416
481
|
- `sandbox.cts` — command construction (flags, env-via-stdin, credential-helper
|
|
417
482
|
argv hygiene) against an injected fake `CommandRunner`, plus `isDockerAvailable()`
|
|
418
483
|
degrading to `false` instead of throwing when Docker is absent (`tests/sandbox.test.cts`).
|
|
@@ -422,6 +487,11 @@ tests cover:
|
|
|
422
487
|
- `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
|
|
423
488
|
and the docker-exec path (fake `CommandRunner`), and usage/report-text
|
|
424
489
|
attachment from fixed JSONL fixtures (`tests/adapters/codex.test.cts`).
|
|
490
|
+
- `adapters/gemini.cts` — host/Docker invocation, cancellation, sandbox/MCP
|
|
491
|
+
credential hygiene, parser drift, and normalized report/usage extraction
|
|
492
|
+
(`tests/adapters/gemini.test.cts`).
|
|
493
|
+
- `adapters/types.cts` — the shared process-result boundary is exercised for
|
|
494
|
+
Claude, Codex, and Gemini (`tests/adapters/conformance.test.cts`).
|
|
425
495
|
- `worktree-guard.cts` + `bin/worktree-guard-hook.cjs` — generated settings/
|
|
426
496
|
config shape, native Codex permission-profile construction, and the hook's
|
|
427
497
|
containment verdicts (in-worktree vs. sibling session vs. home dir, symlink
|
|
@@ -454,6 +524,8 @@ secrets absent from disk after exit"):
|
|
|
454
524
|
flags, per-run MCP override keys, stdin behavior, and JSONL event/usage
|
|
455
525
|
shape are now verified locally; the next dogfood run covers their
|
|
456
526
|
production composition.
|
|
527
|
+
- A full Gemini task that uses an authenticated Gemini CLI to claim, edit,
|
|
528
|
+
open a PR, report usage/evidence, and clean up its session sandbox.
|
|
457
529
|
- Whether Docker-mode Codex should opt into
|
|
458
530
|
`--dangerously-bypass-approvals-and-sandbox`. It is intentionally not a
|
|
459
531
|
default: host mode is not an external sandbox, and silently disabling
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectAdapterCapacityLimit = detectAdapterCapacityLimit;
|
|
4
|
+
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
5
|
+
const DEFAULT_CAPACITY_COOLDOWN_MS = 15 * 60 * 1000;
|
|
6
|
+
const RESET_GRACE_MS = 30 * 1000;
|
|
7
|
+
const MAX_RESET_SEARCH_MINUTES = 48 * 60;
|
|
8
|
+
/**
|
|
9
|
+
* Detects a provider-side capacity response that applies beyond one task
|
|
10
|
+
* lease. Claude Code reports account session exhaustion as a successful CLI
|
|
11
|
+
* process containing a structured 429 result, so process exit status alone
|
|
12
|
+
* cannot distinguish it from an ordinary failed task.
|
|
13
|
+
*/
|
|
14
|
+
function detectAdapterCapacityLimit(result, nowMs = Date.now()) {
|
|
15
|
+
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ??
|
|
16
|
+
(0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
|
|
17
|
+
if (parsed?.api_error_status !== 429)
|
|
18
|
+
return null;
|
|
19
|
+
const detail = parsed.result ?? "";
|
|
20
|
+
if (!/\b(?:session|usage|rate)\s+limit\b|\btoo many requests\b/i.test(detail)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
retryAtMs: parseResetTime(detail, nowMs) ??
|
|
25
|
+
nowMs + DEFAULT_CAPACITY_COOLDOWN_MS,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolves messages such as "resets 9:50pm (America/New_York)" without
|
|
30
|
+
* assuming the runtime host uses the provider's timezone. Searching minute
|
|
31
|
+
* boundaries also handles UTC offsets and daylight-saving transitions using
|
|
32
|
+
* the platform's IANA timezone database.
|
|
33
|
+
*/
|
|
34
|
+
function parseResetTime(detail, nowMs) {
|
|
35
|
+
const match = detail.match(/\bresets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i);
|
|
36
|
+
if (!match)
|
|
37
|
+
return null;
|
|
38
|
+
const hour12 = Number(match[1]);
|
|
39
|
+
const minute = Number(match[2] ?? "0");
|
|
40
|
+
const meridiem = match[3]?.toLowerCase();
|
|
41
|
+
const timeZone = match[4]?.trim();
|
|
42
|
+
if (!Number.isInteger(hour12) ||
|
|
43
|
+
hour12 < 1 ||
|
|
44
|
+
hour12 > 12 ||
|
|
45
|
+
!Number.isInteger(minute) ||
|
|
46
|
+
minute < 0 ||
|
|
47
|
+
minute > 59 ||
|
|
48
|
+
!timeZone) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const targetHour = hour12 % 12 + (meridiem === "pm" ? 12 : 0);
|
|
52
|
+
let formatter;
|
|
53
|
+
try {
|
|
54
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
55
|
+
timeZone,
|
|
56
|
+
hour: "2-digit",
|
|
57
|
+
minute: "2-digit",
|
|
58
|
+
hourCycle: "h23",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const firstMinuteMs = Math.ceil((nowMs + 1) / 60_000) * 60_000;
|
|
65
|
+
for (let offset = 0; offset < MAX_RESET_SEARCH_MINUTES; offset += 1) {
|
|
66
|
+
const candidateMs = firstMinuteMs + offset * 60_000;
|
|
67
|
+
const parts = formatter.formatToParts(new Date(candidateMs));
|
|
68
|
+
const hour = Number(parts.find((part) => part.type === "hour")?.value);
|
|
69
|
+
const candidateMinute = Number(parts.find((part) => part.type === "minute")?.value);
|
|
70
|
+
if (hour === targetHour && candidateMinute === minute) {
|
|
71
|
+
return candidateMs + RESET_GRACE_MS;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
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
|
*
|
|
@@ -26,7 +26,22 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
26
26
|
*/
|
|
27
27
|
async function runClaudeCodeAdapter(options) {
|
|
28
28
|
const args = ["-p", options.prompt];
|
|
29
|
+
const hasSettingSources = options.extraArgs.some((arg) => arg === "--setting-sources" || arg.startsWith("--setting-sources="));
|
|
29
30
|
const hasExplicitPermissionMode = options.extraArgs.some((arg) => ["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
31
|
+
// Runtime sessions must not inherit an operator's personal or project-local
|
|
32
|
+
// Claude hooks. Apart from making execution machine-dependent, those hooks
|
|
33
|
+
// commonly reference helper files through CLAUDE_PROJECT_DIR; that variable
|
|
34
|
+
// points at the temporary session checkout, where a host-only helper does
|
|
35
|
+
// not exist, and a failing SessionEnd hook turns an otherwise successful
|
|
36
|
+
// task into an adapter failure.
|
|
37
|
+
//
|
|
38
|
+
// An empty source list disables user/project/local settings while preserving
|
|
39
|
+
// the explicit --settings file below ("flagSettings" in Claude Code), so the
|
|
40
|
+
// generated worktree-guard hook remains active. Operators can deliberately
|
|
41
|
+
// opt sources back in through NAVARCH_CLAUDE_EXTRA_ARGS.
|
|
42
|
+
if (!hasSettingSources) {
|
|
43
|
+
args.push("--setting-sources", "");
|
|
44
|
+
}
|
|
30
45
|
// Navarch sessions are unattended, so route permission decisions through
|
|
31
46
|
// Claude Code's native auto-mode classifier instead of prompting a human or
|
|
32
47
|
// bypassing checks. Operators can replace this with a different permission
|
|
@@ -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/claim-loop.cjs
CHANGED
|
@@ -23,6 +23,7 @@ class ClaimLoop {
|
|
|
23
23
|
claimInFlight = false;
|
|
24
24
|
consecutiveFailures = 0;
|
|
25
25
|
nextClaimAt = 0;
|
|
26
|
+
capacityCooldownUntil = 0;
|
|
26
27
|
quiescenceWaiters = new Set();
|
|
27
28
|
constructor(api, config, capacity, runSession) {
|
|
28
29
|
this.api = api;
|
|
@@ -60,7 +61,7 @@ class ClaimLoop {
|
|
|
60
61
|
if (this.stopped ||
|
|
61
62
|
this.claimInFlight ||
|
|
62
63
|
!this.capacity.hasCapacity() ||
|
|
63
|
-
Date.now() < this.nextClaimAt)
|
|
64
|
+
Date.now() < Math.max(this.nextClaimAt, this.capacityCooldownUntil))
|
|
64
65
|
return;
|
|
65
66
|
this.claimInFlight = true;
|
|
66
67
|
try {
|
|
@@ -72,6 +73,7 @@ class ClaimLoop {
|
|
|
72
73
|
available_capacity: this.capacity.available(),
|
|
73
74
|
capabilities: this.config.capabilities,
|
|
74
75
|
agent_type: this.config.agentType,
|
|
76
|
+
runtimes: this.config.runtimes,
|
|
75
77
|
session_id: sessionId,
|
|
76
78
|
});
|
|
77
79
|
if (this.consecutiveFailures > 0) {
|
|
@@ -84,6 +86,13 @@ class ClaimLoop {
|
|
|
84
86
|
this.capacity.acquire(claimed.lease_id);
|
|
85
87
|
log.info(`claimed task ${claimed.task.id} (${claimed.task.task_type}) as lease ${claimed.lease_id}, session ${sessionId}`);
|
|
86
88
|
this.runSession(claimed, sessionId)
|
|
89
|
+
.then((outcome) => {
|
|
90
|
+
const cooldownUntil = outcome?.claimCooldownUntil;
|
|
91
|
+
if (!cooldownUntil || cooldownUntil <= Date.now())
|
|
92
|
+
return;
|
|
93
|
+
this.capacityCooldownUntil = Math.max(this.capacityCooldownUntil, cooldownUntil);
|
|
94
|
+
log.warn(`adapter capacity exhausted; pausing new claims until ${new Date(this.capacityCooldownUntil).toISOString()}`);
|
|
95
|
+
})
|
|
87
96
|
.catch((err) => log.error(`session ${sessionId} failed: ${String(err)}`))
|
|
88
97
|
.finally(() => this.capacity.release(claimed.lease_id));
|
|
89
98
|
}
|
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
|
@@ -3,12 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_SANDBOX_IMAGE = void 0;
|
|
6
7
|
exports.isRuntimeAgentType = isRuntimeAgentType;
|
|
7
8
|
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
/** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
|
|
12
|
+
exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
|
|
10
13
|
function isRuntimeAgentType(value) {
|
|
11
|
-
return value === "claude-code" || value === "codex";
|
|
14
|
+
return value === "claude-code" || value === "codex" || value === "gemini";
|
|
12
15
|
}
|
|
13
16
|
function envInt(env, name, fallback) {
|
|
14
17
|
const raw = env[name];
|
|
@@ -38,7 +41,9 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
38
41
|
// default. Docker isolation is an explicit operator opt-in, not a
|
|
39
42
|
// prerequisite for claiming ordinary shell work.
|
|
40
43
|
const sandboxMode = env.NAVARCH_SANDBOX_MODE === "docker" ? "docker" : "host";
|
|
41
|
-
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell"];
|
|
44
|
+
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell", "browser-use"];
|
|
45
|
+
const agentType = isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code";
|
|
46
|
+
const runtimes = envList(env, "NAVARCH_RUNTIMES", [agentType]).filter(isRuntimeAgentType);
|
|
42
47
|
return {
|
|
43
48
|
apiBase: env.NAVARCH_API_BASE ?? "http://localhost:3000",
|
|
44
49
|
workspaceRoot: env.NAVARCH_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
|
|
@@ -52,16 +57,19 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
52
57
|
// interval must stay comfortably under that TTL.
|
|
53
58
|
leaseHeartbeatIntervalMs,
|
|
54
59
|
sessionTimeoutMs: envInt(env, "NAVARCH_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
|
|
55
|
-
agentType
|
|
60
|
+
agentType,
|
|
61
|
+
runtimes: runtimes.length > 0 ? runtimes : [agentType],
|
|
56
62
|
claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
|
|
57
63
|
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
58
64
|
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
59
65
|
codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
|
|
66
|
+
geminiBin: env.NAVARCH_GEMINI_BIN ?? "gemini",
|
|
67
|
+
geminiExtraArgs: envList(env, "NAVARCH_GEMINI_EXTRA_ARGS", []),
|
|
60
68
|
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
61
69
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
62
70
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
63
71
|
sandboxMode,
|
|
64
|
-
dockerImage: env.NAVARCH_DOCKER_IMAGE ??
|
|
72
|
+
dockerImage: env.NAVARCH_DOCKER_IMAGE ?? exports.DEFAULT_SANDBOX_IMAGE,
|
|
65
73
|
// Multiple sessions share one machine; keeping each agent inside its own
|
|
66
74
|
// worktree is the safe default, so disabling is the explicit opt-out.
|
|
67
75
|
worktreeGuard: !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? ""),
|
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
|
@@ -19,10 +19,11 @@ const logger_cjs_1 = require("./logger.cjs");
|
|
|
19
19
|
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
20
20
|
const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
21
21
|
const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
22
|
+
const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
|
|
22
23
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
23
24
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
24
25
|
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
25
|
-
const
|
|
26
|
+
const COMPLETION_REMEDIATION_RETRIES = 2;
|
|
26
27
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
27
28
|
/**
|
|
28
29
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
@@ -30,8 +31,8 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
30
31
|
* 2. fetch secrets from the broker at session start (managed GitHub git
|
|
31
32
|
* credentials are subsequently refreshed by a lease-scoped helper)
|
|
32
33
|
* 3. optionally stand up a Docker sandbox when explicitly configured
|
|
33
|
-
* 4. run the
|
|
34
|
-
*
|
|
34
|
+
* 4. run the adapter selected by the claim (Claude Code, Codex, or Gemini;
|
|
35
|
+
* adapters/index.cts#selectAdapter), heartbeating the
|
|
35
36
|
* lease throughout
|
|
36
37
|
* 5. redact + upload the transcript, map the exit condition, complete the
|
|
37
38
|
* lease (recording which agent_type ran it)
|
|
@@ -46,9 +47,14 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
46
47
|
async function runSession(deps, claimed, sessionId) {
|
|
47
48
|
const { api, config } = deps;
|
|
48
49
|
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
50
|
+
const runtime = claimed.runtime ?? config.agentType;
|
|
49
51
|
const execution = bundle.execution ?? {
|
|
50
52
|
profile: task.execution_profile ?? "standard",
|
|
51
|
-
model:
|
|
53
|
+
model: runtime === "codex"
|
|
54
|
+
? "gpt-5.6-sol"
|
|
55
|
+
: runtime === "gemini"
|
|
56
|
+
? "auto"
|
|
57
|
+
: "best",
|
|
52
58
|
reasoning_effort: "medium",
|
|
53
59
|
};
|
|
54
60
|
const executionReport = {
|
|
@@ -98,7 +104,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
98
104
|
evidence_urls: [],
|
|
99
105
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
100
106
|
exit_status: "crashed",
|
|
101
|
-
agent_type:
|
|
107
|
+
agent_type: runtime,
|
|
102
108
|
...executionReport,
|
|
103
109
|
});
|
|
104
110
|
secrets = {};
|
|
@@ -122,7 +128,9 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
122
128
|
let pendingGuidance = [];
|
|
123
129
|
let activeAbortController = null;
|
|
124
130
|
let leaseLost = false;
|
|
131
|
+
let leaseGone = false;
|
|
125
132
|
let heartbeatInFlight = null;
|
|
133
|
+
const sessionOutcome = {};
|
|
126
134
|
const pollLease = () => {
|
|
127
135
|
if (heartbeatInFlight)
|
|
128
136
|
return heartbeatInFlight;
|
|
@@ -146,6 +154,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
146
154
|
.catch((err) => {
|
|
147
155
|
log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
|
|
148
156
|
leaseLost = true;
|
|
157
|
+
leaseGone = isTerminalLeaseHeartbeatError(err);
|
|
149
158
|
activeAbortController?.abort();
|
|
150
159
|
})
|
|
151
160
|
.finally(() => {
|
|
@@ -167,7 +176,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
167
176
|
evidence_urls: [],
|
|
168
177
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
169
178
|
exit_status: "crashed",
|
|
170
|
-
agent_type:
|
|
179
|
+
agent_type: runtime,
|
|
171
180
|
...executionReport,
|
|
172
181
|
})
|
|
173
182
|
.catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
|
|
@@ -208,11 +217,12 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
208
217
|
// machine. Host-mode Claude gets a generated PreToolUse hook; host-mode
|
|
209
218
|
// Codex gets an OS-enforced native permission profile over the same roots.
|
|
210
219
|
// Docker mode already has a container boundary. NAVARCH_WORKTREE_GUARD=off
|
|
211
|
-
// opts out for
|
|
220
|
+
// opts out for any agent.
|
|
212
221
|
let claudeSettingsPath = null;
|
|
213
222
|
let codexGuardArgs;
|
|
223
|
+
let geminiGuardMounts;
|
|
214
224
|
if (config.sandboxMode === "host" && config.worktreeGuard) {
|
|
215
|
-
if (
|
|
225
|
+
if (runtime === "claude-code") {
|
|
216
226
|
if (config.claudeExtraArgs.includes("--settings")) {
|
|
217
227
|
log.warn("NAVARCH_CLAUDE_EXTRA_ARGS supplies --settings; skipping the generated worktree-guard settings for this session.");
|
|
218
228
|
}
|
|
@@ -227,7 +237,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
227
237
|
claudeSettingsPath = guard.settingsPath;
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
|
-
else {
|
|
240
|
+
else if (runtime === "codex") {
|
|
231
241
|
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
232
242
|
workDir,
|
|
233
243
|
worktreePath: gitWorktree.worktreePath,
|
|
@@ -236,6 +246,15 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
236
246
|
extraRoots: config.guardExtraRoots,
|
|
237
247
|
});
|
|
238
248
|
}
|
|
249
|
+
else {
|
|
250
|
+
geminiGuardMounts = (0, worktree_guard_cjs_1.geminiSandboxMounts)({
|
|
251
|
+
workDir,
|
|
252
|
+
worktreePath: gitWorktree.worktreePath,
|
|
253
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
254
|
+
workspaceRoot: config.workspaceRoot,
|
|
255
|
+
extraRoots: config.guardExtraRoots,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
239
258
|
}
|
|
240
259
|
try {
|
|
241
260
|
await gitWorktree.prepare();
|
|
@@ -243,17 +262,16 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
243
262
|
await sandbox.create();
|
|
244
263
|
await sandbox.injectEnv(toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail));
|
|
245
264
|
}
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
265
|
+
// The control plane resolves the project default/task override before
|
|
266
|
+
// claim and returns one of this worker's advertised runtimes.
|
|
267
|
+
// All adapters implement the same AgentAdapter.run() shape
|
|
249
268
|
// (adapters/types.cts), so nothing else in this function branches on
|
|
250
269
|
// 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;
|
|
270
|
+
const adapter = (0, index_cjs_1.selectAdapter)(runtime);
|
|
271
|
+
const { bin, extraArgs } = adapterCommand(config, runtime);
|
|
254
272
|
const attempts = [];
|
|
255
273
|
let nextPrompt = null;
|
|
256
|
-
let
|
|
274
|
+
let completionRemediationRetries = 0;
|
|
257
275
|
while (true) {
|
|
258
276
|
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
259
277
|
// It is already included in deliveredGuidance, so clear the pending
|
|
@@ -279,16 +297,28 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
279
297
|
env: toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail),
|
|
280
298
|
settingsPath: claudeSettingsPath,
|
|
281
299
|
codexGuardArgs,
|
|
300
|
+
geminiSandboxMounts: geminiGuardMounts,
|
|
282
301
|
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
283
302
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
284
303
|
signal: activeAbortController.signal,
|
|
285
304
|
});
|
|
286
305
|
activeAbortController = null;
|
|
287
306
|
attempts.push(turnResult);
|
|
307
|
+
const capacityLimit = (0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(turnResult);
|
|
308
|
+
if (capacityLimit) {
|
|
309
|
+
sessionOutcome.claimCooldownUntil = Math.max(sessionOutcome.claimCooldownUntil ?? 0, capacityLimit.retryAtMs);
|
|
310
|
+
}
|
|
288
311
|
// Close the small race between a naturally completed turn and the next
|
|
289
312
|
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
290
|
-
// lease can be completed.
|
|
291
|
-
|
|
313
|
+
// lease can be completed. A terminal heartbeat response means the
|
|
314
|
+
// control plane has already disposed of the lease, so neither retry the
|
|
315
|
+
// heartbeat nor attempt completion against that stale lease.
|
|
316
|
+
if (!leaseGone)
|
|
317
|
+
await pollLease();
|
|
318
|
+
if (leaseGone) {
|
|
319
|
+
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
320
|
+
return sessionOutcome;
|
|
321
|
+
}
|
|
292
322
|
if (!leaseLost && pendingGuidance.length > 0)
|
|
293
323
|
continue;
|
|
294
324
|
const result = {
|
|
@@ -349,7 +379,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
349
379
|
},
|
|
350
380
|
transcript_url: transcriptUrl,
|
|
351
381
|
exit_status: mapping.exitStatus,
|
|
352
|
-
agent_type:
|
|
382
|
+
agent_type: runtime,
|
|
353
383
|
...executionReport,
|
|
354
384
|
};
|
|
355
385
|
try {
|
|
@@ -357,17 +387,21 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
357
387
|
break;
|
|
358
388
|
}
|
|
359
389
|
catch (err) {
|
|
360
|
-
|
|
390
|
+
if (isAlreadyReleasedCompletionError(err)) {
|
|
391
|
+
log.warn(`completion skipped for ${leaseId}: the lease was already released.`);
|
|
392
|
+
return sessionOutcome;
|
|
393
|
+
}
|
|
394
|
+
const rejection = mapping.leaseOutcome === "completed" ? completionRemediationMessage(err) : null;
|
|
361
395
|
if (!rejection)
|
|
362
396
|
throw err;
|
|
363
397
|
const redactedRejection = (0, redact_cjs_1.redactText)(rejection, knownSecrets);
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
log.warn(`completion for ${leaseId}
|
|
398
|
+
if (completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
|
|
399
|
+
completionRemediationRetries += 1;
|
|
400
|
+
log.warn(`completion for ${leaseId} needs more work; restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
367
401
|
nextPrompt = redactedRejection;
|
|
368
402
|
continue;
|
|
369
403
|
}
|
|
370
|
-
log.warn(`completion for ${leaseId} still
|
|
404
|
+
log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries; failing with the control-plane rejection.`);
|
|
371
405
|
await api.completeLease(leaseId, {
|
|
372
406
|
...completion,
|
|
373
407
|
status: "failed",
|
|
@@ -390,7 +424,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
390
424
|
evidence_urls: [],
|
|
391
425
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
392
426
|
exit_status: "crashed",
|
|
393
|
-
agent_type:
|
|
427
|
+
agent_type: runtime,
|
|
394
428
|
...executionReport,
|
|
395
429
|
})
|
|
396
430
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
@@ -404,6 +438,17 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
404
438
|
await gitWorktree.cleanup();
|
|
405
439
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
406
440
|
}
|
|
441
|
+
return sessionOutcome;
|
|
442
|
+
}
|
|
443
|
+
function adapterCommand(config, runtime) {
|
|
444
|
+
switch (runtime) {
|
|
445
|
+
case "claude-code":
|
|
446
|
+
return { bin: config.claudeBin, extraArgs: config.claudeExtraArgs };
|
|
447
|
+
case "codex":
|
|
448
|
+
return { bin: config.codexBin, extraArgs: config.codexExtraArgs };
|
|
449
|
+
case "gemini":
|
|
450
|
+
return { bin: config.geminiBin, extraArgs: config.geminiExtraArgs };
|
|
451
|
+
}
|
|
407
452
|
}
|
|
408
453
|
function sumReportedUsage(attempts, key) {
|
|
409
454
|
const reported = attempts.flatMap((attempt) => {
|
|
@@ -412,16 +457,28 @@ function sumReportedUsage(attempts, key) {
|
|
|
412
457
|
});
|
|
413
458
|
return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
|
|
414
459
|
}
|
|
415
|
-
function
|
|
460
|
+
function completionRemediationMessage(err) {
|
|
416
461
|
if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
|
|
417
462
|
return null;
|
|
418
463
|
if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
|
|
419
464
|
return null;
|
|
420
465
|
const body = err.body;
|
|
421
|
-
return body.code === "pr_required"
|
|
466
|
+
return (body.code === "pr_required" || body.code === "pr_not_ready") &&
|
|
467
|
+
typeof body.error === "string"
|
|
422
468
|
? body.error
|
|
423
469
|
: null;
|
|
424
470
|
}
|
|
471
|
+
function isTerminalLeaseHeartbeatError(err) {
|
|
472
|
+
return err instanceof api_cjs_1.NavarchApiError && [403, 404, 410].includes(err.status);
|
|
473
|
+
}
|
|
474
|
+
function isAlreadyReleasedCompletionError(err) {
|
|
475
|
+
if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
|
|
476
|
+
return false;
|
|
477
|
+
if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
|
|
478
|
+
return false;
|
|
479
|
+
const body = err.body;
|
|
480
|
+
return body.error === "lease already released";
|
|
481
|
+
}
|
|
425
482
|
/** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
|
|
426
483
|
function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
|
|
427
484
|
// Keep the existing `(secrets, authorName, authorEmail)` call shape while
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -7,6 +7,7 @@ exports.guardHookScriptPath = guardHookScriptPath;
|
|
|
7
7
|
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
8
8
|
exports.codexToolReadRoots = codexToolReadRoots;
|
|
9
9
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
10
|
+
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
10
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
12
|
const node_fs_1 = require("node:fs");
|
|
12
13
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -76,6 +77,10 @@ function codexToolReadRoots(env = process.env) {
|
|
|
76
77
|
node_path_1.default.join(homeDir, ".agents", "skills"),
|
|
77
78
|
node_path_1.default.join(homeDir, ".local", "bin"),
|
|
78
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",
|
|
79
84
|
node_path_1.default.join(homeDir, "Library", "Caches", "ms-playwright"),
|
|
80
85
|
node_path_1.default.join(homeDir, ".cache", "ms-playwright"),
|
|
81
86
|
];
|
|
@@ -137,6 +142,21 @@ function codexWorktreeGuardArgs(options) {
|
|
|
137
142
|
`permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
|
|
138
143
|
];
|
|
139
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
|
+
}
|
|
140
160
|
function isPathInside(candidate, root) {
|
|
141
161
|
const relative = node_path_1.default.relative(root, candidate);
|
|
142
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.13",
|
|
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,6 +15,7 @@
|
|
|
15
15
|
"ai-agent",
|
|
16
16
|
"claude-code",
|
|
17
17
|
"codex",
|
|
18
|
+
"gemini-cli",
|
|
18
19
|
"task-runner"
|
|
19
20
|
],
|
|
20
21
|
"bin": {
|