@sagentlab/navarch-runtime 0.1.0 → 0.1.2
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 +74 -64
- package/dist/adapter.cjs +1 -1
- package/dist/adapters/claude.cjs +2 -2
- package/dist/adapters/codex.cjs +5 -5
- package/dist/adapters/index.cjs +1 -1
- package/dist/api.cjs +2 -2
- package/dist/capacity.cjs +1 -1
- package/dist/claim-loop.cjs +13 -1
- package/dist/cli.cjs +71 -29
- package/dist/config.cjs +28 -19
- package/dist/git-worktree.cjs +142 -0
- package/dist/machine-store.cjs +8 -5
- package/dist/mcp-config.cjs +7 -7
- package/dist/prompt.cjs +11 -0
- package/dist/sandbox.cjs +21 -7
- package/dist/session.cjs +67 -21
- package/dist/types.cjs +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,21 +7,22 @@ it. Plain Node/TypeScript, zero production dependencies, no Next.js coupling
|
|
|
7
7
|
machine.
|
|
8
8
|
|
|
9
9
|
The agent CLI a session runs is either **Claude Code** or **OpenAI Codex**,
|
|
10
|
-
selected
|
|
10
|
+
selected by the local machine via `--agent` or `NAVARCH_AGENT` (default
|
|
11
|
+
`claude-code`) — see
|
|
11
12
|
"Choosing an agent (Claude Code vs. Codex)" below.
|
|
12
13
|
|
|
13
14
|
See [`docs/agent-platform-project-plan.md`](../docs/agent-platform-project-plan.md)
|
|
14
|
-
§3.8/§3.9/§3.11 and [`docs/
|
|
15
|
+
§3.8/§3.9/§3.11 and [`docs/navarch/implementation-plan.md`](../docs/navarch/implementation-plan.md)
|
|
15
16
|
WP-07 for the design this implements, and
|
|
16
|
-
[`docs/
|
|
17
|
+
[`docs/navarch/schema-design.md`](../docs/navarch/schema-design.md) §7 for
|
|
17
18
|
the API contract.
|
|
18
19
|
|
|
19
20
|
## Quick start on a fresh machine
|
|
20
21
|
|
|
21
22
|
```sh
|
|
22
23
|
git clone <this repo> && cd sagentlab/runtime
|
|
23
|
-
./install.sh # checks node
|
|
24
|
-
export
|
|
24
|
+
./install.sh # checks node, npm install, npm run build
|
|
25
|
+
export NAVARCH_API_BASE=https://navarch.example.com
|
|
25
26
|
node bin/navarch.cjs register --token <enrollment-token> --name my-machine-1
|
|
26
27
|
node bin/navarch.cjs start
|
|
27
28
|
```
|
|
@@ -30,15 +31,15 @@ Or, once dependencies are installed:
|
|
|
30
31
|
|
|
31
32
|
```sh
|
|
32
33
|
npm run build
|
|
33
|
-
|
|
34
|
+
NAVARCH_API_BASE=https://navarch.example.com npm run register -- --token <enrollment-token> --name my-machine-1
|
|
34
35
|
npm start
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
`register` is the one command that prints the machine's auth token — exactly
|
|
38
|
-
once, to stdout. It is stored at `$
|
|
39
|
-
`~/.
|
|
39
|
+
once, to stdout. It is stored at `$NAVARCH_CONFIG_DIR/machine.json` (default
|
|
40
|
+
`~/.navarch/machine.json`, mode `0600`) and never echoed again. For
|
|
40
41
|
twelve-factor deployments (systemd `EnvironmentFile`, container secrets),
|
|
41
|
-
skip `register` and set `
|
|
42
|
+
skip `register` and set `NAVARCH_MACHINE_TOKEN` + `NAVARCH_MACHINE_ID`
|
|
42
43
|
directly.
|
|
43
44
|
|
|
44
45
|
Run `node bin/navarch.cjs doctor` any time to print resolved config, Docker
|
|
@@ -48,78 +49,83 @@ availability, and registration status without starting the daemon.
|
|
|
48
49
|
|
|
49
50
|
`connect` is the project-scoped sibling of `register` — "Connect an agent to
|
|
50
51
|
a project" (self-hosted-runner style, like a GitHub Actions self-hosted
|
|
51
|
-
runner token): a project **owner** mints a
|
|
52
|
-
|
|
52
|
+
runner token): a project **owner** mints a single-use token from the platform
|
|
53
|
+
UI (the project settings page's "Connect
|
|
53
54
|
an agent" panel, or the onboarding wizard's Agent step),
|
|
54
55
|
`POST /api/projects/:id/enrollment-tokens`, and pastes you the ready-to-run
|
|
55
|
-
command. Unlike `register`, no `
|
|
56
|
+
command. Unlike `register`, no `NAVARCH_ENROLLMENT_SECRET` or
|
|
56
57
|
`--owner-zone` is needed — the token is already scoped to exactly one
|
|
57
58
|
project, and the resulting machine's `project_bindings` is set to that
|
|
58
59
|
project only (it will never be dispatched work from any other project).
|
|
59
60
|
|
|
60
61
|
```sh
|
|
61
62
|
npx @sagentlab/navarch-runtime connect --token flmt_<...> --project <project-id> \
|
|
62
|
-
--name my-agent-1 --api-base https://
|
|
63
|
+
--name my-agent-1 --agent codex --api-base https://navarch.example.com
|
|
63
64
|
node bin/navarch.cjs start
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
(`@sagentlab/navarch-runtime` is
|
|
67
|
-
|
|
68
|
-
`git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
69
|
-
|
|
67
|
+
(`@sagentlab/navarch-runtime` is published to npm, so `npx
|
|
68
|
+
@sagentlab/navarch-runtime <cmd>` works on a fresh machine with no clone.
|
|
69
|
+
The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
70
|
+
<cmd>` — remains available for local development.)
|
|
70
71
|
|
|
71
72
|
## Commands
|
|
72
73
|
|
|
73
74
|
| Command | Purpose |
|
|
74
75
|
|---|---|
|
|
75
|
-
| `register --token <t> --name <n> [--
|
|
76
|
-
| `connect --token <t> --name <n> [--
|
|
77
|
-
| `start` | Runs the daemon
|
|
76
|
+
| `register --token <t> --name <n> [--agent claude-code\|codex] […]` | Registers this machine, saves its local agent choice, and prints the token once. |
|
|
77
|
+
| `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. |
|
|
78
|
+
| `start [--agent claude-code\|codex]` | Runs the daemon. A start-time agent choice overrides the saved choice. |
|
|
78
79
|
| `doctor` | Prints resolved config + Docker/registration status; no side effects. |
|
|
79
80
|
|
|
80
|
-
## Configuration (`
|
|
81
|
+
## Configuration (`NAVARCH_*` env vars)
|
|
81
82
|
|
|
82
83
|
| Var | Default | Meaning |
|
|
83
84
|
|---|---|---|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
85
|
+
| `NAVARCH_API_BASE` | `http://localhost:3000` | Control-plane base URL. |
|
|
86
|
+
| `NAVARCH_MACHINE_TOKEN` / `NAVARCH_MACHINE_ID` | — | Skip `register`/the config file; twelve-factor auth. |
|
|
87
|
+
| `NAVARCH_MACHINE_NAME` | — | Used by `register`. |
|
|
88
|
+
| `NAVARCH_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
|
|
89
|
+
| `NAVARCH_PROJECT_ID` | — | Alternative to `connect --project`. |
|
|
90
|
+
| `NAVARCH_CONFIG_DIR` | `~/.navarch` | Where `machine.json` lives. |
|
|
91
|
+
| `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Persistent bare repo caches plus isolated per-session worktrees. |
|
|
92
|
+
| `NAVARCH_MAX_SESSIONS` | `5` | Local concurrent-session capacity cap — see `src/capacity.cts`. |
|
|
93
|
+
| `NAVARCH_CAPABILITIES` | `shell` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. |
|
|
94
|
+
| `NAVARCH_OWNER_ZONE` | `sagentlab` | `sagentlab` or `customer-<slug>-premises` (project-plan.md §3.11). |
|
|
95
|
+
| `NAVARCH_POLL_INTERVAL_MS` | `5000` | Claim-loop poll interval. |
|
|
96
|
+
| `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
|
|
97
|
+
| `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
|
|
98
|
+
| `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
|
|
99
|
+
| `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
|
|
100
|
+
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
|
|
101
|
+
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `codex`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
102
|
+
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
103
|
+
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). |
|
|
104
|
+
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
105
|
+
| `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config`/`--json` (Codex). |
|
|
106
|
+
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
106
107
|
|
|
107
108
|
## Choosing an agent (Claude Code vs. Codex)
|
|
108
109
|
|
|
109
|
-
Each machine
|
|
110
|
-
`
|
|
110
|
+
Each machine chooses its own agent CLI. Pass `--agent` while connecting to
|
|
111
|
+
persist the choice in local `machine.json`, override it for one daemon start
|
|
112
|
+
with `start --agent`, or set `NAVARCH_AGENT` in the machine's service
|
|
113
|
+
environment:
|
|
111
114
|
|
|
112
115
|
```sh
|
|
113
116
|
# Claude Code (default) — requires the `claude` CLI installed and
|
|
114
|
-
# authenticated on this machine (or
|
|
115
|
-
export
|
|
117
|
+
# authenticated on this machine (or NAVARCH_CLAUDE_BIN pointing at it).
|
|
118
|
+
export NAVARCH_AGENT=claude-code
|
|
116
119
|
|
|
117
120
|
# OpenAI Codex — requires the `codex` CLI installed and authenticated on
|
|
118
|
-
# this machine (or
|
|
121
|
+
# this machine (or NAVARCH_CODEX_BIN pointing at it), analogous to the
|
|
119
122
|
# Claude Code prerequisite above.
|
|
120
|
-
export
|
|
123
|
+
export NAVARCH_AGENT=codex
|
|
121
124
|
```
|
|
122
125
|
|
|
126
|
+
Priority is `start --agent` → `NAVARCH_AGENT` → the locally saved choice →
|
|
127
|
+
`claude-code`. The control plane does not choose the adapter.
|
|
128
|
+
|
|
123
129
|
Both adapters implement the same `AgentAdapter` interface
|
|
124
130
|
(`src/adapters/types.cts`) and run either directly on the host or via
|
|
125
131
|
`docker exec` in the session's sandbox container, exactly like the Claude
|
|
@@ -144,21 +150,24 @@ cli.cts
|
|
|
144
150
|
├─ register → api.registerMachine() → machine-store.cts (writes machine.json once)
|
|
145
151
|
├─ connect → api.connectMachine() → machine-store.cts (writes machine.json once)
|
|
146
152
|
└─ start
|
|
147
|
-
├─ MachineHeartbeatLoop (heartbeat-loop.cts) → api.machineHeartbeat() [every
|
|
148
|
-
└─ ClaimLoop (claim-loop.cts) → api.claim() [every
|
|
149
|
-
└─ runSession (session.cts), one per claimed lease, run concurrently up to
|
|
153
|
+
├─ MachineHeartbeatLoop (heartbeat-loop.cts) → api.machineHeartbeat() [every NAVARCH_HEARTBEAT_INTERVAL_MS]
|
|
154
|
+
└─ ClaimLoop (claim-loop.cts) → api.claim() [every NAVARCH_POLL_INTERVAL_MS, gated by CapacityTracker]
|
|
155
|
+
└─ runSession (session.cts), one per claimed lease, run concurrently up to NAVARCH_MAX_SESSIONS:
|
|
150
156
|
1. write prompt.md (prompt.cts renders the 4-layer context bundle)
|
|
151
157
|
2. api.issueSecrets() → held in memory only
|
|
152
|
-
3.
|
|
158
|
+
3. fetch the project's bare repository cache and create a
|
|
159
|
+
unique git worktree for this session; optionally mount it
|
|
160
|
+
into Docker when NAVARCH_SANDBOX_MODE=docker
|
|
153
161
|
4. selectAdapter(config.agentType) (adapters/index.cts) picks one AgentAdapter
|
|
154
|
-
(adapters/types.cts) by
|
|
162
|
+
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
155
163
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
|
|
156
164
|
- codexAdapter (adapters/codex.cts) — `codex exec <prompt> --json --mcp-config <path>` (ASSUMED)
|
|
157
|
-
heartbeating the lease every
|
|
165
|
+
heartbeating the lease every NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS throughout either;
|
|
158
166
|
a failed heartbeat aborts the run (kills the process) and marks the outcome as lease-lost
|
|
159
167
|
5. mapExitCondition (exit-conditions.cts) → redact.cts scrubs the transcript → upload.cts PUTs it
|
|
160
168
|
6. api.completeLease(), reporting agent_type: config.agentType
|
|
161
|
-
7. sandbox.wipe()
|
|
169
|
+
7. sandbox.wipe() when present; remove the session workspace
|
|
170
|
+
unconditionally (finally block)
|
|
162
171
|
```
|
|
163
172
|
|
|
164
173
|
`adapter.cts` (top-level) is now a backward-compat re-export of
|
|
@@ -229,13 +238,13 @@ everything that can be verified without those:
|
|
|
229
238
|
plus Claude's and Codex's usage/report parsing (`tests/exit-conditions.test.cts`).
|
|
230
239
|
- `redact.cts` — exact-value and pattern-based redaction (`tests/redact.test.cts`).
|
|
231
240
|
- `capacity.cts` — capacity math and acquire/release bookkeeping (`tests/capacity.test.cts`).
|
|
232
|
-
- `config.cts` — env var parsing and defaults, including `
|
|
233
|
-
`
|
|
241
|
+
- `config.cts` — env var parsing and defaults, including `NAVARCH_AGENT`/
|
|
242
|
+
`NAVARCH_CODEX_BIN`/`NAVARCH_CODEX_EXTRA_ARGS` (`tests/config.test.cts`).
|
|
234
243
|
- `sandbox.cts` — command construction (flags, env-via-stdin, credential-helper
|
|
235
244
|
argv hygiene) against an injected fake `CommandRunner`, plus `isDockerAvailable()`
|
|
236
245
|
degrading to `false` instead of throwing when Docker is absent (`tests/sandbox.test.cts`).
|
|
237
246
|
- `adapters/index.cts#selectAdapter` — picks the right `AgentAdapter` for every
|
|
238
|
-
`
|
|
247
|
+
`NAVARCH_AGENT` value, including the claude-code fallback for an unrecognized
|
|
239
248
|
one (`tests/adapters/index.test.cts`).
|
|
240
249
|
- `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
|
|
241
250
|
and the docker-exec path (fake `CommandRunner`), and usage/report-text
|
|
@@ -287,7 +296,7 @@ secrets absent from disk after exit"):
|
|
|
287
296
|
`--dangerously-bypass-approvals-and-sandbox` per published Codex CLI
|
|
288
297
|
docs) to avoid blocking on an approval prompt inside the already-isolated
|
|
289
298
|
Docker sandbox — deliberately **not** hardcoded, left to
|
|
290
|
-
`
|
|
299
|
+
`NAVARCH_CODEX_EXTRA_ARGS` until confirmed, since guessing the wrong
|
|
291
300
|
flag here could silently disable sandboxing rather than just fail loudly.
|
|
292
301
|
- Whether the Codex CLI even authenticates/runs non-interactively the same
|
|
293
302
|
way `claude` does (API key vs. ChatGPT-account OAuth device flow) — this
|
|
@@ -315,13 +324,14 @@ secrets absent from disk after exit"):
|
|
|
315
324
|
that one project. Unit-tested here: `connectMachine()`'s request shape
|
|
316
325
|
(`tests/api.test.cts`) and the `connect` command's flag/env parsing
|
|
317
326
|
(`tests/cli.test.cts`); not tested here: the real Postgres round trip
|
|
318
|
-
(`lib/
|
|
327
|
+
(`lib/navarch/__tests__/enrollment.test.ts` and the
|
|
319
328
|
`app/api/machines/connect` / `app/api/projects/[id]/enrollment-tokens`
|
|
320
329
|
route tests cover that with a mocked admin client, not a live database).
|
|
321
|
-
- `@sagentlab/navarch-runtime` is
|
|
330
|
+
- `@sagentlab/navarch-runtime` is published to npm — every `npx
|
|
322
331
|
@sagentlab/navarch-runtime ...` command shown above (and in
|
|
323
|
-
`ConnectAgentPanel`)
|
|
324
|
-
(`git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
332
|
+
`ConnectAgentPanel`) resolves the published package directly; the
|
|
333
|
+
from-source flow (`git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
334
|
+
connect ...`) is an equivalent local-development alternative.
|
|
325
335
|
|
|
326
336
|
## A note on `.cts` instead of `.ts`
|
|
327
337
|
|
package/dist/adapter.cjs
CHANGED
|
@@ -7,7 +7,7 @@ exports.claudeCodeAdapter = exports.runClaudeCodeAdapter = void 0;
|
|
|
7
7
|
* The Claude Code adapter that used to live entirely in this file now lives
|
|
8
8
|
* in adapters/claude.cts, implementing the generalized `AgentAdapter`
|
|
9
9
|
* interface (adapters/types.cts) alongside adapters/codex.cts's Codex CLI
|
|
10
|
-
* sibling — session.cts picks between the two via
|
|
10
|
+
* sibling — session.cts picks between the two via NAVARCH_AGENT
|
|
11
11
|
* (config.cts's `agentType`) through adapters/index.cts#selectAdapter.
|
|
12
12
|
*
|
|
13
13
|
* This file is kept, unchanged in its exported names, so any existing
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -14,7 +14,7 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
14
14
|
*
|
|
15
15
|
* This is one of two implementations of the AgentAdapter interface
|
|
16
16
|
* (adapters/types.cts) — see adapters/codex.cts for the Codex CLI sibling
|
|
17
|
-
* session.cts picks between via
|
|
17
|
+
* session.cts picks between via NAVARCH_AGENT (config.cts's `agentType`).
|
|
18
18
|
*
|
|
19
19
|
* NEEDS LIVE VERIFICATION (WP-13): `--output-format json` is assumed to make
|
|
20
20
|
* `claude -p` print a single JSON result object with a `usage` block, per
|
|
@@ -128,7 +128,7 @@ async function runViaDocker(options, args) {
|
|
|
128
128
|
function shellQuote(value) {
|
|
129
129
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
130
130
|
}
|
|
131
|
-
/** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via
|
|
131
|
+
/** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=claude-code (the default). */
|
|
132
132
|
exports.claudeCodeAdapter = {
|
|
133
133
|
agentType: "claude-code",
|
|
134
134
|
run: runClaudeCodeAdapter,
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -8,7 +8,7 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
8
8
|
* Headless OpenAI Codex CLI adapter — the Codex sibling of claude.cts's
|
|
9
9
|
* `runClaudeCodeAdapter`, implementing the same AgentAdapter interface
|
|
10
10
|
* (adapters/types.cts) so session.cts can pick either one at runtime via
|
|
11
|
-
*
|
|
11
|
+
* NAVARCH_AGENT (config.cts's `agentType`). Structurally this mirrors
|
|
12
12
|
* claude.cts exactly: same host-vs-docker-exec split, same
|
|
13
13
|
* timeout/AbortSignal handling, same "attach best-effort usage onto the raw
|
|
14
14
|
* result" shape — only the CLI invocation and output parsing differ.
|
|
@@ -41,14 +41,14 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
41
41
|
* - Sandboxing/approvals: a real `codex exec` may prompt for
|
|
42
42
|
* approval/sandbox-escalation on some actions by default; because this
|
|
43
43
|
* runtime already isolates the session in its own Docker container (or,
|
|
44
|
-
* in `
|
|
44
|
+
* in `NAVARCH_SANDBOX_MODE=host`, trusts the host), the intent is to pass
|
|
45
45
|
* whatever flag disables Codex's own approval gate for a fully
|
|
46
46
|
* non-interactive run (something like `--full-auto` or
|
|
47
47
|
* `--dangerously-bypass-approvals-and-sandbox` in published Codex CLI
|
|
48
48
|
* documentation) — deliberately NOT hardcoded here since getting an
|
|
49
49
|
* unverified flag wrong could silently change sandboxing behavior; left to
|
|
50
|
-
* be supplied via
|
|
51
|
-
* - `
|
|
50
|
+
* be supplied via NAVARCH_CODEX_EXTRA_ARGS until confirmed.
|
|
51
|
+
* - `NAVARCH_CODEX_EXTRA_ARGS` (`extraArgs`) wins over the default `--json`
|
|
52
52
|
* exactly like the Claude adapter's `--output-format` opt-out, so an
|
|
53
53
|
* operator can fall back to plain-text output (or add the real
|
|
54
54
|
* approval-bypass flag) without an adapter code change.
|
|
@@ -160,7 +160,7 @@ async function runViaDocker(options, args) {
|
|
|
160
160
|
function shellQuote(value) {
|
|
161
161
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
162
162
|
}
|
|
163
|
-
/** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via
|
|
163
|
+
/** The AgentAdapter (adapters/types.cts) wrapper session.cts selects via NAVARCH_AGENT=codex. */
|
|
164
164
|
exports.codexAdapter = {
|
|
165
165
|
agentType: "codex",
|
|
166
166
|
run: runCodexAdapter,
|
package/dist/adapters/index.cjs
CHANGED
|
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "codexAdapter", { enumerable: true, get: function
|
|
|
10
10
|
Object.defineProperty(exports, "runCodexAdapter", { enumerable: true, get: function () { return codex_cjs_1.runCodexAdapter; } });
|
|
11
11
|
/**
|
|
12
12
|
* Picks the AgentAdapter (adapters/types.cts) session.cts should run a
|
|
13
|
-
* session with, keyed off config.cts's `agentType` (
|
|
13
|
+
* session with, keyed off config.cts's `agentType` (NAVARCH_AGENT). This is
|
|
14
14
|
* the one place agent-type branching happens outside config loading itself —
|
|
15
15
|
* session.cts calls the returned adapter's `run()` uniformly regardless of
|
|
16
16
|
* which one it got.
|
package/dist/api.cjs
CHANGED
|
@@ -74,11 +74,11 @@ class NavarchApiClient {
|
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* `POST /api/machines/connect` — "Connect an agent to a project"
|
|
77
|
-
* (docs/
|
|
77
|
+
* (docs/navarch/schema-design.md §7 "Agent connect"). CONFIRMED endpoint,
|
|
78
78
|
* the project-scoped sibling of {@link registerMachine}: redeems a
|
|
79
79
|
* single-use, project-scoped enrollment token a project owner minted
|
|
80
80
|
* (POST /api/projects/:id/enrollment-tokens) instead of the global
|
|
81
|
-
*
|
|
81
|
+
* NAVARCH_ENROLLMENT_SECRET. Unauthenticated like registerMachine — the
|
|
82
82
|
* enrollment token in the body is the auth.
|
|
83
83
|
*/
|
|
84
84
|
async connectMachine(req) {
|
package/dist/capacity.cjs
CHANGED
|
@@ -9,7 +9,7 @@ function computeAvailableCapacity(maxSessions, activeSessions) {
|
|
|
9
9
|
/**
|
|
10
10
|
* Tracks which session (lease) ids currently occupy a slot on this machine so
|
|
11
11
|
* the heartbeat loop reports accurate available_capacity and the claim loop
|
|
12
|
-
* never over-claims beyond
|
|
12
|
+
* never over-claims beyond NAVARCH_MAX_SESSIONS.
|
|
13
13
|
*/
|
|
14
14
|
class CapacityTracker {
|
|
15
15
|
maxSessions;
|
package/dist/claim-loop.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClaimLoop = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const api_cjs_1 = require("./api.cjs");
|
|
5
6
|
const logger_cjs_1 = require("./logger.cjs");
|
|
6
7
|
const log = (0, logger_cjs_1.createLogger)("claim");
|
|
7
8
|
/**
|
|
@@ -46,6 +47,7 @@ class ClaimLoop {
|
|
|
46
47
|
const claimed = await this.api.claim({
|
|
47
48
|
available_capacity: this.capacity.available(),
|
|
48
49
|
capabilities: this.config.capabilities,
|
|
50
|
+
agent_type: this.config.agentType,
|
|
49
51
|
session_id: sessionId,
|
|
50
52
|
});
|
|
51
53
|
if (!claimed)
|
|
@@ -57,7 +59,17 @@ class ClaimLoop {
|
|
|
57
59
|
.finally(() => this.capacity.release(claimed.lease_id));
|
|
58
60
|
}
|
|
59
61
|
catch (err) {
|
|
60
|
-
|
|
62
|
+
// NavarchApiError's message is only the status line ("… failed with
|
|
63
|
+
// 500"); the control plane's actual error text lives in `.body`. Log it
|
|
64
|
+
// so a server-side claim failure is diagnosable from the runtime alone
|
|
65
|
+
// instead of an opaque bare status.
|
|
66
|
+
if (err instanceof api_cjs_1.NavarchApiError) {
|
|
67
|
+
const detail = typeof err.body === "string" ? err.body : JSON.stringify(err.body);
|
|
68
|
+
log.warn(`claim failed: ${err.message}${detail ? ` — ${detail}` : ""}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
log.warn(`claim failed: ${String(err)}`);
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -11,6 +11,33 @@ const session_cjs_1 = require("./session.cjs");
|
|
|
11
11
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
12
12
|
const logger_cjs_1 = require("./logger.cjs");
|
|
13
13
|
const log = (0, logger_cjs_1.createLogger)("cli");
|
|
14
|
+
const PACKAGE_NAME = "@sagentlab/navarch-runtime";
|
|
15
|
+
/**
|
|
16
|
+
* How to tell the user to re-invoke this CLI, matching however THEY launched
|
|
17
|
+
* it. The bare `navarch-runtime` bin only exists on PATH after a global install
|
|
18
|
+
* (`npm i -g`) or `npm link`; the documented onboarding path is `npx
|
|
19
|
+
* @sagentlab/navarch-runtime …`, where the bare name is NOT on PATH. Printing
|
|
20
|
+
* `navarch-runtime start` to someone who ran us via npx sends them straight
|
|
21
|
+
* into `command not found`, so detect that case and echo the form that works.
|
|
22
|
+
*/
|
|
23
|
+
function invocation(subcommand) {
|
|
24
|
+
const scriptPath = process.argv[1] ?? "";
|
|
25
|
+
const viaNpx = scriptPath.includes("/_npx/") ||
|
|
26
|
+
scriptPath.includes("\\_npx\\") ||
|
|
27
|
+
process.env.npm_command === "exec" ||
|
|
28
|
+
process.env.npm_lifecycle_event === "npx";
|
|
29
|
+
const prefix = viaNpx ? `npx ${PACKAGE_NAME}` : "navarch-runtime";
|
|
30
|
+
return `${prefix} ${subcommand}`;
|
|
31
|
+
}
|
|
32
|
+
function agentFromFlag(flags) {
|
|
33
|
+
const value = flags.agent;
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!(0, config_cjs_1.isRuntimeAgentType)(value)) {
|
|
37
|
+
throw new Error("--agent must be either 'claude-code' or 'codex'.");
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
14
41
|
function parseArgs(argv) {
|
|
15
42
|
const [command, ...rest] = argv;
|
|
16
43
|
const flags = {};
|
|
@@ -38,13 +65,13 @@ function parseArgs(argv) {
|
|
|
38
65
|
async function registerCommand(flags) {
|
|
39
66
|
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
40
67
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
41
|
-
const enrollmentToken = flags.token ?? process.env.
|
|
42
|
-
const name = flags.name ?? process.env.
|
|
68
|
+
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
69
|
+
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
43
70
|
if (!enrollmentToken) {
|
|
44
|
-
throw new Error("Missing --token (or
|
|
71
|
+
throw new Error("Missing --token (or NAVARCH_ENROLLMENT_TOKEN) — get one from the admin console.");
|
|
45
72
|
}
|
|
46
73
|
if (!name) {
|
|
47
|
-
throw new Error("Missing --name (or
|
|
74
|
+
throw new Error("Missing --name (or NAVARCH_MACHINE_NAME) for this machine.");
|
|
48
75
|
}
|
|
49
76
|
const maxSessions = Number(flags["max-sessions"] ?? config.maxSessions);
|
|
50
77
|
const capabilities = (flags.capabilities ?? config.capabilities.join(","))
|
|
@@ -52,6 +79,7 @@ async function registerCommand(flags) {
|
|
|
52
79
|
.map((s) => s.trim())
|
|
53
80
|
.filter(Boolean);
|
|
54
81
|
const ownerZone = flags["owner-zone"] ?? config.ownerZone;
|
|
82
|
+
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
55
83
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
56
84
|
const result = await client.registerMachine({
|
|
57
85
|
enrollment_token: enrollmentToken,
|
|
@@ -65,39 +93,41 @@ async function registerCommand(flags) {
|
|
|
65
93
|
token: result.token,
|
|
66
94
|
name,
|
|
67
95
|
api_base: apiBase,
|
|
96
|
+
agent_type: agentType,
|
|
68
97
|
});
|
|
69
98
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
70
99
|
console.log("Machine registered.");
|
|
71
100
|
console.log(` machine_id: ${result.machine_id}`);
|
|
72
101
|
console.log(` token: ${result.token}`);
|
|
73
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run
|
|
102
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
|
|
74
103
|
}
|
|
75
104
|
/**
|
|
76
105
|
* `navarch-runtime connect` — "Connect an agent to a project"
|
|
77
|
-
* (docs/
|
|
78
|
-
* style). The project-scoped sibling of `register`: redeems a
|
|
79
|
-
*
|
|
106
|
+
* (docs/navarch/schema-design.md §7 "Agent connect"; self-hosted-runner
|
|
107
|
+
* style). The project-scoped sibling of `register`: redeems a single-use
|
|
108
|
+
* enrollment token a project owner minted from the platform UI
|
|
80
109
|
* (ConnectAgentPanel → POST /api/projects/:id/enrollment-tokens) instead of
|
|
81
|
-
* the global
|
|
110
|
+
* the global NAVARCH_ENROLLMENT_SECRET `register` needs. Prints the
|
|
82
111
|
* machine token exactly once, same discipline as `register`.
|
|
83
112
|
*/
|
|
84
113
|
async function connectCommand(flags) {
|
|
85
114
|
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
86
115
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
87
|
-
const enrollmentToken = flags.token ?? process.env.
|
|
88
|
-
const name = flags.name ?? process.env.
|
|
89
|
-
const projectId = flags.project ?? process.env.
|
|
116
|
+
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
117
|
+
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
118
|
+
const projectId = flags.project ?? process.env.NAVARCH_PROJECT_ID;
|
|
90
119
|
if (!enrollmentToken) {
|
|
91
|
-
throw new Error("Missing --token (or
|
|
120
|
+
throw new Error("Missing --token (or NAVARCH_ENROLLMENT_TOKEN) — get one from a project owner's \"Connect an agent\" panel.");
|
|
92
121
|
}
|
|
93
122
|
if (!name) {
|
|
94
|
-
throw new Error("Missing --name (or
|
|
123
|
+
throw new Error("Missing --name (or NAVARCH_MACHINE_NAME) for this machine.");
|
|
95
124
|
}
|
|
96
125
|
const maxSessions = Number(flags["max-sessions"] ?? config.maxSessions);
|
|
97
126
|
const capabilities = (flags.capabilities ?? config.capabilities.join(","))
|
|
98
127
|
.split(",")
|
|
99
128
|
.map((s) => s.trim())
|
|
100
129
|
.filter(Boolean);
|
|
130
|
+
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
101
131
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
102
132
|
const result = await client.connectMachine({
|
|
103
133
|
enrollment_token: enrollmentToken,
|
|
@@ -111,23 +141,30 @@ async function connectCommand(flags) {
|
|
|
111
141
|
token: result.token,
|
|
112
142
|
name,
|
|
113
143
|
api_base: apiBase,
|
|
144
|
+
agent_type: agentType,
|
|
114
145
|
});
|
|
115
146
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
116
147
|
console.log("Machine connected.");
|
|
117
148
|
console.log(` machine_id: ${result.machine_id}`);
|
|
118
149
|
console.log(` token: ${result.token}`);
|
|
119
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run
|
|
150
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
|
|
120
151
|
}
|
|
121
|
-
async function startCommand() {
|
|
122
|
-
const
|
|
123
|
-
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(
|
|
152
|
+
async function startCommand(flags) {
|
|
153
|
+
const baseConfig = (0, config_cjs_1.loadRuntimeConfig)();
|
|
154
|
+
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
155
|
+
// Adapter selection belongs to the local machine: an explicit start flag
|
|
156
|
+
// wins, followed by NAVARCH_AGENT, the choice saved at connect/register
|
|
157
|
+
// time, and finally the backwards-compatible Claude Code default.
|
|
158
|
+
const agentType = agentFromFlag(flags) ??
|
|
159
|
+
(process.env.NAVARCH_AGENT ? baseConfig.agentType : identity.agent_type ?? baseConfig.agentType);
|
|
160
|
+
const config = { ...baseConfig, agentType };
|
|
124
161
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
125
162
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
126
163
|
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity);
|
|
127
164
|
const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId));
|
|
128
165
|
heartbeat.start();
|
|
129
166
|
claimLoop.start();
|
|
130
|
-
log.info(`navarch-runtime started: machine=${identity.name} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
|
|
167
|
+
log.info(`navarch-runtime started: machine=${identity.name} agent=${config.agentType} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
|
|
131
168
|
const shutdown = () => {
|
|
132
169
|
log.info("shutting down...");
|
|
133
170
|
heartbeat.stop();
|
|
@@ -137,7 +174,7 @@ async function startCommand() {
|
|
|
137
174
|
process.on("SIGINT", shutdown);
|
|
138
175
|
process.on("SIGTERM", shutdown);
|
|
139
176
|
}
|
|
140
|
-
async function doctorCommand() {
|
|
177
|
+
async function doctorCommand(flags) {
|
|
141
178
|
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
142
179
|
const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
|
|
143
180
|
console.log(`api_base: ${config.apiBase}`);
|
|
@@ -147,26 +184,31 @@ async function doctorCommand() {
|
|
|
147
184
|
console.log(`capabilities: ${config.capabilities.join(", ")}`);
|
|
148
185
|
console.log(`sandbox_mode: ${config.sandboxMode}`);
|
|
149
186
|
console.log(`docker: ${dockerOk ? "available" : "NOT AVAILABLE (docker-backed sessions will fail)"}`);
|
|
187
|
+
let identity;
|
|
150
188
|
try {
|
|
151
|
-
|
|
152
|
-
console.log(`machine: ${identity.name} (${identity.machine_id})`);
|
|
189
|
+
identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
|
|
153
190
|
}
|
|
154
191
|
catch {
|
|
155
|
-
console.log(
|
|
192
|
+
console.log(`machine: not registered — run \`${invocation("register")}\``);
|
|
193
|
+
return;
|
|
156
194
|
}
|
|
195
|
+
const agentType = agentFromFlag(flags) ??
|
|
196
|
+
(process.env.NAVARCH_AGENT ? config.agentType : identity.agent_type ?? config.agentType);
|
|
197
|
+
console.log(`machine: ${identity.name} (${identity.machine_id})`);
|
|
198
|
+
console.log(`agent: ${agentType}`);
|
|
157
199
|
}
|
|
158
200
|
function helpText() {
|
|
159
201
|
return `navarch-runtime — Navarch machine-side session manager
|
|
160
202
|
|
|
161
203
|
Usage:
|
|
162
204
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
163
|
-
[--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
205
|
+
[--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
164
206
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
165
|
-
[--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
166
|
-
navarch-runtime start
|
|
207
|
+
[--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
208
|
+
navarch-runtime start [--agent claude-code|codex]
|
|
167
209
|
navarch-runtime doctor
|
|
168
210
|
|
|
169
|
-
Configuration is via
|
|
211
|
+
Configuration is via NAVARCH_* environment variables; see runtime/README.md.
|
|
170
212
|
`;
|
|
171
213
|
}
|
|
172
214
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -180,10 +222,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
180
222
|
await connectCommand(flags);
|
|
181
223
|
break;
|
|
182
224
|
case "start":
|
|
183
|
-
await startCommand();
|
|
225
|
+
await startCommand(flags);
|
|
184
226
|
break;
|
|
185
227
|
case "doctor":
|
|
186
|
-
await doctorCommand();
|
|
228
|
+
await doctorCommand(flags);
|
|
187
229
|
break;
|
|
188
230
|
case "help":
|
|
189
231
|
case "--help":
|
package/dist/config.cjs
CHANGED
|
@@ -3,9 +3,13 @@ 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.isRuntimeAgentType = isRuntimeAgentType;
|
|
6
7
|
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
function isRuntimeAgentType(value) {
|
|
11
|
+
return value === "claude-code" || value === "codex";
|
|
12
|
+
}
|
|
9
13
|
function envInt(env, name, fallback) {
|
|
10
14
|
const raw = env[name];
|
|
11
15
|
if (!raw)
|
|
@@ -23,33 +27,38 @@ function envList(env, name, fallback) {
|
|
|
23
27
|
.filter(Boolean);
|
|
24
28
|
}
|
|
25
29
|
/**
|
|
26
|
-
* Loads runtime config from
|
|
30
|
+
* Loads runtime config from NAVARCH_* env vars, with sane defaults for a
|
|
27
31
|
* fresh machine. Accepts an explicit env map (defaulting to process.env) so
|
|
28
32
|
* it is trivially unit-testable without mutating global state.
|
|
29
33
|
*/
|
|
30
34
|
function loadRuntimeConfig(env = process.env) {
|
|
31
|
-
const configDir = env.
|
|
32
|
-
const leaseHeartbeatIntervalMs = envInt(env, "
|
|
35
|
+
const configDir = env.NAVARCH_CONFIG_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".navarch");
|
|
36
|
+
const leaseHeartbeatIntervalMs = envInt(env, "NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS", 5 * 60 * 1000);
|
|
37
|
+
// Run the agent with the resources already available on its machine by
|
|
38
|
+
// default. Docker isolation is an explicit operator opt-in, not a
|
|
39
|
+
// prerequisite for claiming ordinary shell work.
|
|
40
|
+
const sandboxMode = env.NAVARCH_SANDBOX_MODE === "docker" ? "docker" : "host";
|
|
41
|
+
const defaultCapabilities = sandboxMode === "docker" ? ["docker-sandbox", "shell"] : ["shell"];
|
|
33
42
|
return {
|
|
34
|
-
apiBase: env.
|
|
35
|
-
workspaceRoot: env.
|
|
43
|
+
apiBase: env.NAVARCH_API_BASE ?? "http://localhost:3000",
|
|
44
|
+
workspaceRoot: env.NAVARCH_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
|
|
36
45
|
configDir,
|
|
37
|
-
maxSessions: envInt(env, "
|
|
38
|
-
capabilities: envList(env, "
|
|
39
|
-
ownerZone: env.
|
|
40
|
-
pollIntervalMs: envInt(env, "
|
|
41
|
-
machineHeartbeatIntervalMs: envInt(env, "
|
|
46
|
+
maxSessions: envInt(env, "NAVARCH_MAX_SESSIONS", 5),
|
|
47
|
+
capabilities: envList(env, "NAVARCH_CAPABILITIES", defaultCapabilities),
|
|
48
|
+
ownerZone: env.NAVARCH_OWNER_ZONE ?? "sagentlab",
|
|
49
|
+
pollIntervalMs: envInt(env, "NAVARCH_POLL_INTERVAL_MS", 5000),
|
|
50
|
+
machineHeartbeatIntervalMs: envInt(env, "NAVARCH_HEARTBEAT_INTERVAL_MS", 60_000),
|
|
42
51
|
// leases.expires_at = claimed_at + 15 min (schema-design.md §4) — default renewal
|
|
43
52
|
// interval must stay comfortably under that TTL.
|
|
44
53
|
leaseHeartbeatIntervalMs,
|
|
45
|
-
sessionTimeoutMs: envInt(env, "
|
|
46
|
-
agentType: env.
|
|
47
|
-
claudeBin: env.
|
|
48
|
-
claudeExtraArgs: envList(env, "
|
|
49
|
-
codexBin: env.
|
|
50
|
-
codexExtraArgs: envList(env, "
|
|
51
|
-
mcpConfigPath: env.
|
|
52
|
-
sandboxMode
|
|
53
|
-
dockerImage: env.
|
|
54
|
+
sessionTimeoutMs: envInt(env, "NAVARCH_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
|
|
55
|
+
agentType: isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code",
|
|
56
|
+
claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
|
|
57
|
+
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
58
|
+
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
59
|
+
codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
|
|
60
|
+
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
61
|
+
sandboxMode,
|
|
62
|
+
dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
|
|
54
63
|
};
|
|
55
64
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GitWorktree = void 0;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
10
|
+
const repositoryLocks = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Maintains one bare repository cache per project and checks out each session
|
|
13
|
+
* into its own uniquely named worktree. The cache avoids N full clones while
|
|
14
|
+
* git's worktree metadata keeps concurrent agents from sharing an index or
|
|
15
|
+
* working directory.
|
|
16
|
+
*/
|
|
17
|
+
class GitWorktree {
|
|
18
|
+
sessionRoot;
|
|
19
|
+
worktreePath;
|
|
20
|
+
repositoryPath;
|
|
21
|
+
branch;
|
|
22
|
+
runner;
|
|
23
|
+
cloneUrl;
|
|
24
|
+
githubToken;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
const projectKey = safePathSegment(options.projectId);
|
|
27
|
+
const sessionKey = safePathSegment(options.sessionId);
|
|
28
|
+
this.sessionRoot = node_path_1.default.join(options.workspaceRoot, "sessions", sessionKey);
|
|
29
|
+
this.worktreePath = node_path_1.default.join(this.sessionRoot, "repo");
|
|
30
|
+
this.repositoryPath = node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
|
|
31
|
+
this.branch = `navarch/${safePathSegment(options.taskId).slice(0, 32)}-${sessionKey.slice(0, 12)}`;
|
|
32
|
+
this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
33
|
+
this.cloneUrl = options.cloneUrl;
|
|
34
|
+
this.githubToken = options.githubToken;
|
|
35
|
+
}
|
|
36
|
+
async prepare() {
|
|
37
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
|
|
38
|
+
await node_fs_1.promises.mkdir(this.sessionRoot, { recursive: true });
|
|
39
|
+
await withRepositoryLock(this.repositoryPath, async () => {
|
|
40
|
+
if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
|
|
41
|
+
await this.runGit(["clone", "--bare", this.cloneUrl, this.repositoryPath], true);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
|
|
45
|
+
if (normalizeCloneUrl(origin.stdout) !== normalizeCloneUrl(this.cloneUrl)) {
|
|
46
|
+
await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const remoteHead = await this.runGit(["ls-remote", "--symref", "origin", "HEAD"], true);
|
|
50
|
+
const startRef = parseRemoteHead(remoteHead.stdout) ?? "HEAD";
|
|
51
|
+
await this.runGit([
|
|
52
|
+
"--git-dir",
|
|
53
|
+
this.repositoryPath,
|
|
54
|
+
"fetch",
|
|
55
|
+
"--prune",
|
|
56
|
+
"origin",
|
|
57
|
+
"+refs/heads/*:refs/heads/*",
|
|
58
|
+
], true);
|
|
59
|
+
await this.runGit([
|
|
60
|
+
"--git-dir",
|
|
61
|
+
this.repositoryPath,
|
|
62
|
+
"worktree",
|
|
63
|
+
"add",
|
|
64
|
+
"-b",
|
|
65
|
+
this.branch,
|
|
66
|
+
this.worktreePath,
|
|
67
|
+
startRef,
|
|
68
|
+
], false);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async cleanup() {
|
|
72
|
+
await withRepositoryLock(this.repositoryPath, async () => {
|
|
73
|
+
await this.runner
|
|
74
|
+
.run("git", ["--git-dir", this.repositoryPath, "worktree", "remove", "--force", this.worktreePath])
|
|
75
|
+
.catch(() => undefined);
|
|
76
|
+
await this.runner
|
|
77
|
+
.run("git", ["--git-dir", this.repositoryPath, "branch", "-D", this.branch])
|
|
78
|
+
.catch(() => undefined);
|
|
79
|
+
await this.runner
|
|
80
|
+
.run("git", ["--git-dir", this.repositoryPath, "worktree", "prune"])
|
|
81
|
+
.catch(() => undefined);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async runGit(args, authenticated) {
|
|
85
|
+
const credentialArgs = authenticated && this.githubToken
|
|
86
|
+
? [
|
|
87
|
+
"-c",
|
|
88
|
+
'credential.helper=!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f',
|
|
89
|
+
]
|
|
90
|
+
: [];
|
|
91
|
+
const result = await this.runner.run("git", [...credentialArgs, ...args], {
|
|
92
|
+
env: this.githubToken
|
|
93
|
+
? { ...process.env, GITHUB_TOKEN: this.githubToken, GIT_TERMINAL_PROMPT: "0" }
|
|
94
|
+
: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
95
|
+
});
|
|
96
|
+
if (result.code !== 0) {
|
|
97
|
+
throw new Error(`git ${args[0] ?? "command"} failed: ${result.stderr || result.stdout}`);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.GitWorktree = GitWorktree;
|
|
103
|
+
async function withRepositoryLock(key, work) {
|
|
104
|
+
const previous = repositoryLocks.get(key) ?? Promise.resolve();
|
|
105
|
+
let release;
|
|
106
|
+
const current = new Promise((resolve) => {
|
|
107
|
+
release = resolve;
|
|
108
|
+
});
|
|
109
|
+
const queued = previous.then(() => current);
|
|
110
|
+
repositoryLocks.set(key, queued);
|
|
111
|
+
await previous;
|
|
112
|
+
try {
|
|
113
|
+
return await work();
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
release();
|
|
117
|
+
if (repositoryLocks.get(key) === queued)
|
|
118
|
+
repositoryLocks.delete(key);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function pathExists(value) {
|
|
122
|
+
try {
|
|
123
|
+
await node_fs_1.promises.access(value);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function safePathSegment(value) {
|
|
131
|
+
const safe = value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
|
|
132
|
+
if (!safe)
|
|
133
|
+
throw new Error("Cannot create a git worktree without a valid project/session identifier.");
|
|
134
|
+
return safe;
|
|
135
|
+
}
|
|
136
|
+
function normalizeCloneUrl(value) {
|
|
137
|
+
return value.trim().replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
function parseRemoteHead(output) {
|
|
140
|
+
const match = output.match(/^ref:\s+(refs\/heads\/[A-Za-z0-9._/-]+)\s+HEAD$/m);
|
|
141
|
+
return match?.[1] ?? null;
|
|
142
|
+
}
|
package/dist/machine-store.cjs
CHANGED
|
@@ -40,19 +40,22 @@ async function loadMachineIdentity(configDir) {
|
|
|
40
40
|
* per-task broker-issued secrets, handled entirely in session.cts/sandbox.cts.
|
|
41
41
|
*/
|
|
42
42
|
async function resolveMachineIdentity(configDir, apiBaseFallback) {
|
|
43
|
-
const envToken = process.env.
|
|
44
|
-
const envId = process.env.
|
|
43
|
+
const envToken = process.env.NAVARCH_MACHINE_TOKEN;
|
|
44
|
+
const envId = process.env.NAVARCH_MACHINE_ID;
|
|
45
45
|
if (envToken && envId) {
|
|
46
46
|
return {
|
|
47
47
|
machine_id: envId,
|
|
48
48
|
token: envToken,
|
|
49
|
-
name: process.env.
|
|
50
|
-
api_base: process.env.
|
|
49
|
+
name: process.env.NAVARCH_MACHINE_NAME ?? envId,
|
|
50
|
+
api_base: process.env.NAVARCH_API_BASE ?? apiBaseFallback,
|
|
51
|
+
agent_type: process.env.NAVARCH_AGENT === "codex" || process.env.NAVARCH_AGENT === "claude-code"
|
|
52
|
+
? process.env.NAVARCH_AGENT
|
|
53
|
+
: undefined,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
const stored = await loadMachineIdentity(configDir);
|
|
54
57
|
if (stored)
|
|
55
58
|
return stored;
|
|
56
59
|
throw new Error("No machine identity found. Run `navarch-runtime register` first, or set " +
|
|
57
|
-
"
|
|
60
|
+
"NAVARCH_MACHINE_TOKEN + NAVARCH_MACHINE_ID for a twelve-factor deployment.");
|
|
58
61
|
}
|
package/dist/mcp-config.cjs
CHANGED
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
// platform MCP server".
|
|
6
6
|
//
|
|
7
7
|
// Previously `mcpConfigPath` was just a static, operator-supplied path
|
|
8
|
-
// (config.cts's `
|
|
8
|
+
// (config.cts's `NAVARCH_MCP_CONFIG_PATH`) with nothing that ever wrote a
|
|
9
9
|
// real config file -- there was no way for a session to actually reach the
|
|
10
10
|
// platform MCP server with the auth it needs. This module is the fix: one
|
|
11
11
|
// config, generated fresh per session, carrying the machine's bearer token
|
|
12
12
|
// and this session's lease id (the same two-layer auth app/api/mcp/route.ts
|
|
13
|
-
// requires -- see lib/
|
|
13
|
+
// requires -- see lib/navarch/mcp/context.ts).
|
|
14
14
|
//
|
|
15
15
|
// Pure and side-effect free (no filesystem access) so it's trivially unit
|
|
16
16
|
// testable; session.cts is the only caller that writes the result to disk.
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.
|
|
18
|
+
exports.NAVARCH_LEASE_HEADER = void 0;
|
|
19
19
|
exports.buildNavarchMcpConfig = buildNavarchMcpConfig;
|
|
20
|
-
/** The header app/api/mcp/route.ts's lib/
|
|
21
|
-
exports.
|
|
20
|
+
/** The header app/api/mcp/route.ts's lib/navarch/mcp/context.ts reads to identify which lease is calling. */
|
|
21
|
+
exports.NAVARCH_LEASE_HEADER = "X-Navarch-Lease-Id";
|
|
22
22
|
/**
|
|
23
23
|
* Builds the `.mcp.json`-shaped config object Claude Code's `--mcp-config`
|
|
24
24
|
* flag expects: a remote "http" (streamable HTTP) server entry with the
|
|
25
25
|
* bearer token and lease id as request headers.
|
|
26
26
|
*/
|
|
27
27
|
function buildNavarchMcpConfig(opts) {
|
|
28
|
-
const serverName = opts.serverName ?? "
|
|
28
|
+
const serverName = opts.serverName ?? "navarch";
|
|
29
29
|
return {
|
|
30
30
|
mcpServers: {
|
|
31
31
|
[serverName]: {
|
|
@@ -33,7 +33,7 @@ function buildNavarchMcpConfig(opts) {
|
|
|
33
33
|
url: `${opts.apiBase.replace(/\/+$/, "")}/api/mcp`,
|
|
34
34
|
headers: {
|
|
35
35
|
Authorization: `Bearer ${opts.machineToken}`,
|
|
36
|
-
[exports.
|
|
36
|
+
[exports.NAVARCH_LEASE_HEADER]: opts.leaseId,
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
},
|
package/dist/prompt.cjs
CHANGED
|
@@ -9,9 +9,20 @@ exports.renderPrompt = renderPrompt;
|
|
|
9
9
|
* WP-07 "write prompt file".
|
|
10
10
|
*/
|
|
11
11
|
function renderPrompt(task, bundle) {
|
|
12
|
+
if (bundle.task_context) {
|
|
13
|
+
const repositoryContext = bundle.repository
|
|
14
|
+
? `Repository: ${bundle.repository.full_name} (${bundle.repository.url})\nLocal checkout: this session's isolated git worktree (current working directory).`
|
|
15
|
+
: null;
|
|
16
|
+
return [repositoryContext, bundle.task_context.trim(), bundle.retry_context?.trim()]
|
|
17
|
+
.filter((section) => Boolean(section))
|
|
18
|
+
.join("\n\n") + "\n";
|
|
19
|
+
}
|
|
12
20
|
const sections = [];
|
|
13
21
|
sections.push(`# Task: ${task.summary}`);
|
|
14
22
|
sections.push(`Type: ${task.task_type} | Repo: ${task.repo} | Environment: ${task.environment_scope}`);
|
|
23
|
+
if (bundle.repository) {
|
|
24
|
+
sections.push(`Repository URL: ${bundle.repository.url}\nLocal checkout: this session's isolated git worktree (current working directory).`);
|
|
25
|
+
}
|
|
15
26
|
if (task.github_issue_url) {
|
|
16
27
|
sections.push(`GitHub issue: ${task.github_issue_url}`);
|
|
17
28
|
}
|
package/dist/sandbox.cjs
CHANGED
|
@@ -15,7 +15,7 @@ exports.SandboxUnavailableError = SandboxUnavailableError;
|
|
|
15
15
|
exports.nodeCommandRunner = {
|
|
16
16
|
run(cmd, args, opts = {}) {
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
|
-
const child = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts.cwd });
|
|
18
|
+
const child = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts.cwd, env: opts.env });
|
|
19
19
|
let stdout = "";
|
|
20
20
|
let stderr = "";
|
|
21
21
|
let settled = false;
|
|
@@ -69,7 +69,7 @@ async function isDockerAvailable(runner = exports.nodeCommandRunner) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
function containerName(sessionId) {
|
|
72
|
-
return `
|
|
72
|
+
return `navarch-${sessionId}`;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* One Docker container per session (implementation-plan.md WP-07 "Sandbox:
|
|
@@ -91,14 +91,27 @@ class DockerSandbox {
|
|
|
91
91
|
runner;
|
|
92
92
|
workDir;
|
|
93
93
|
image;
|
|
94
|
+
containerWorkDir;
|
|
95
|
+
sharedGitDir;
|
|
94
96
|
constructor(opts) {
|
|
95
97
|
this.name = containerName(opts.sessionId);
|
|
96
98
|
this.runner = opts.runner ?? exports.nodeCommandRunner;
|
|
97
99
|
this.workDir = node_path_1.default.join(opts.workspaceRoot, opts.sessionId);
|
|
98
100
|
this.image = opts.image;
|
|
101
|
+
this.containerWorkDir = opts.containerWorkDir ?? null;
|
|
102
|
+
this.sharedGitDir = opts.sharedGitDir ?? null;
|
|
99
103
|
}
|
|
100
104
|
async create() {
|
|
101
105
|
await node_fs_1.promises.mkdir(this.workDir, { recursive: true });
|
|
106
|
+
const mounts = this.containerWorkDir
|
|
107
|
+
? [
|
|
108
|
+
"-v",
|
|
109
|
+
`${this.workDir}:${this.workDir}`,
|
|
110
|
+
...(this.sharedGitDir ? ["-v", `${this.sharedGitDir}:${this.sharedGitDir}`] : []),
|
|
111
|
+
"-w",
|
|
112
|
+
this.containerWorkDir,
|
|
113
|
+
]
|
|
114
|
+
: ["-v", `${this.workDir}:/workspace`, "-w", "/workspace"];
|
|
102
115
|
const result = await this.runner.run("docker", [
|
|
103
116
|
"run",
|
|
104
117
|
"-d",
|
|
@@ -110,10 +123,7 @@ class DockerSandbox {
|
|
|
110
123
|
"/tmp",
|
|
111
124
|
"--tmpfs",
|
|
112
125
|
"/run",
|
|
113
|
-
|
|
114
|
-
`${this.workDir}:/workspace`,
|
|
115
|
-
"-w",
|
|
116
|
-
"/workspace",
|
|
126
|
+
...mounts,
|
|
117
127
|
this.image,
|
|
118
128
|
"tail",
|
|
119
129
|
"-f",
|
|
@@ -156,9 +166,13 @@ class DockerSandbox {
|
|
|
156
166
|
}
|
|
157
167
|
/** Force-removes the container and the host-side workspace mount. Best-effort: never throws. */
|
|
158
168
|
async wipe() {
|
|
159
|
-
await this.
|
|
169
|
+
await this.stop();
|
|
160
170
|
await node_fs_1.promises.rm(this.workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
161
171
|
}
|
|
172
|
+
/** Stops/removes only the container; the session orchestrator owns worktree cleanup. */
|
|
173
|
+
async stop() {
|
|
174
|
+
await this.runner.run("docker", ["rm", "-f", this.name]).catch(() => undefined);
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
exports.DockerSandbox = DockerSandbox;
|
|
164
178
|
function shellQuote(value) {
|
package/dist/session.cjs
CHANGED
|
@@ -14,26 +14,27 @@ const upload_cjs_1 = require("./upload.cjs");
|
|
|
14
14
|
const prompt_cjs_1 = require("./prompt.cjs");
|
|
15
15
|
const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
16
16
|
const logger_cjs_1 = require("./logger.cjs");
|
|
17
|
-
|
|
17
|
+
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
18
|
+
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
18
19
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
19
20
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
20
21
|
/**
|
|
21
22
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
22
23
|
* 1. write the prompt file
|
|
23
24
|
* 2. fetch secrets from the broker once, at session start
|
|
24
|
-
* 3. stand up a Docker sandbox
|
|
25
|
+
* 3. optionally stand up a Docker sandbox when explicitly configured
|
|
25
26
|
* 4. run the configured agent adapter (Claude Code or Codex, per
|
|
26
|
-
*
|
|
27
|
+
* NAVARCH_AGENT — adapters/index.cts#selectAdapter), heartbeating the
|
|
27
28
|
* lease throughout
|
|
28
29
|
* 5. redact + upload the transcript, map the exit condition, complete the
|
|
29
30
|
* lease (recording which agent_type ran it)
|
|
30
31
|
* 6. wipe the sandbox unconditionally
|
|
31
32
|
*
|
|
32
|
-
* NEEDS LIVE VERIFICATION: the full path requires a real
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* NEEDS LIVE VERIFICATION: the full path requires a real `claude` (or
|
|
34
|
+
* `codex`) binary and a live control-plane API; Docker-backed execution also
|
|
35
|
+
* requires a real Docker daemon. Unit tests exercise each collaborator
|
|
36
|
+
* (api.cts, sandbox.cts, exit-conditions.cts, redact.cts, adapters/*.cts) in
|
|
37
|
+
* isolation instead; see runtime/README.md.
|
|
37
38
|
*/
|
|
38
39
|
async function runSession(deps, claimed, sessionId) {
|
|
39
40
|
const { api, config } = deps;
|
|
@@ -41,7 +42,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
41
42
|
// The session's identity is the pre-allocated session id sent at claim time
|
|
42
43
|
// (recorded on the lease by the dispatcher). Lease-scoped API calls
|
|
43
44
|
// (heartbeat/complete/issue/transcript) still key on leaseId.
|
|
44
|
-
const workDir = node_path_1.default.join(config.workspaceRoot, sessionId);
|
|
45
|
+
const workDir = node_path_1.default.join(config.workspaceRoot, "sessions", sessionId);
|
|
45
46
|
await node_fs_1.promises.mkdir(workDir, { recursive: true });
|
|
46
47
|
const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
47
48
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
@@ -56,6 +57,31 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
56
57
|
secrets = issued.secrets;
|
|
57
58
|
registry.registerAll(secrets);
|
|
58
59
|
}
|
|
60
|
+
const cloneUrl = bundle.repository?.clone_url ??
|
|
61
|
+
(task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
|
|
62
|
+
if (!cloneUrl) {
|
|
63
|
+
const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
|
|
64
|
+
await api.completeLease(leaseId, {
|
|
65
|
+
status: "failed",
|
|
66
|
+
report: failureSummary,
|
|
67
|
+
failure_summary: failureSummary,
|
|
68
|
+
evidence_urls: [],
|
|
69
|
+
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
70
|
+
exit_status: "crashed",
|
|
71
|
+
agent_type: config.agentType,
|
|
72
|
+
});
|
|
73
|
+
secrets = {};
|
|
74
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const gitWorktree = new git_worktree_cjs_1.GitWorktree({
|
|
78
|
+
workspaceRoot: config.workspaceRoot,
|
|
79
|
+
projectId: task.project_id,
|
|
80
|
+
taskId: task.id,
|
|
81
|
+
sessionId,
|
|
82
|
+
cloneUrl,
|
|
83
|
+
githubToken: secrets[bundle.repository?.credential_secret_name ?? "github-pat"],
|
|
84
|
+
});
|
|
59
85
|
const abortController = new AbortController();
|
|
60
86
|
let leaseLost = false;
|
|
61
87
|
const heartbeatTimer = setInterval(() => {
|
|
@@ -73,25 +99,34 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
73
99
|
.completeLease(leaseId, {
|
|
74
100
|
status: "failed",
|
|
75
101
|
report: "Docker sandbox unavailable on this machine.",
|
|
102
|
+
failure_summary: "Docker sandbox unavailable on this machine.",
|
|
76
103
|
evidence_urls: [],
|
|
77
104
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
78
105
|
exit_status: "crashed",
|
|
79
106
|
agent_type: config.agentType,
|
|
80
107
|
})
|
|
81
108
|
.catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
|
|
109
|
+
secrets = {};
|
|
110
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
82
111
|
return;
|
|
83
112
|
}
|
|
84
113
|
const sandbox = dockerAvailable
|
|
85
|
-
? new sandbox_cjs_1.DockerSandbox({
|
|
114
|
+
? new sandbox_cjs_1.DockerSandbox({
|
|
115
|
+
sessionId,
|
|
116
|
+
workspaceRoot: node_path_1.default.join(config.workspaceRoot, "sessions"),
|
|
117
|
+
image: config.dockerImage,
|
|
118
|
+
containerWorkDir: gitWorktree.worktreePath,
|
|
119
|
+
sharedGitDir: gitWorktree.repositoryPath,
|
|
120
|
+
})
|
|
86
121
|
: null;
|
|
87
122
|
// Platform MCP config (implementation-plan.md WP-07: "--mcp-config
|
|
88
123
|
// pointing at the platform MCP server"): generated fresh per session,
|
|
89
124
|
// carrying this machine's bearer token and this session's lease id (the
|
|
90
|
-
// auth app/api/mcp/route.ts requires -- see lib/
|
|
91
|
-
// unless the operator pinned a static override via
|
|
125
|
+
// auth app/api/mcp/route.ts requires -- see lib/navarch/mcp/context.ts),
|
|
126
|
+
// unless the operator pinned a static override via NAVARCH_MCP_CONFIG_PATH
|
|
92
127
|
// (e.g. pointing at a fake MCP server in local testing). Written to
|
|
93
|
-
// workDir (host path)
|
|
94
|
-
//
|
|
128
|
+
// workDir (host path), which Docker mode mounts at that same absolute path
|
|
129
|
+
// so worktree .git pointers and this config path remain valid --
|
|
95
130
|
// the selected adapter (adapters/claude.cts or adapters/codex.cts) needs a
|
|
96
131
|
// path valid in whichever environment it actually runs.
|
|
97
132
|
let mcpConfigPath = config.mcpConfigPath;
|
|
@@ -102,15 +137,15 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
102
137
|
leaseId,
|
|
103
138
|
});
|
|
104
139
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
105
|
-
mcpConfigPath =
|
|
140
|
+
mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
|
|
106
141
|
}
|
|
107
142
|
try {
|
|
143
|
+
await gitWorktree.prepare();
|
|
108
144
|
if (sandbox) {
|
|
109
145
|
await sandbox.create();
|
|
110
146
|
await sandbox.injectEnv(toEnvMap(secrets));
|
|
111
|
-
await sandbox.cloneRepo(task.repo, Boolean(secrets["github-pat"]));
|
|
112
147
|
}
|
|
113
|
-
// Picks the Claude Code or Codex adapter per
|
|
148
|
+
// Picks the Claude Code or Codex adapter per NAVARCH_AGENT
|
|
114
149
|
// (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
|
|
115
150
|
// Both adapters implement the same AgentAdapter.run() shape
|
|
116
151
|
// (adapters/types.cts), so nothing else in this function branches on
|
|
@@ -124,8 +159,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
124
159
|
bin,
|
|
125
160
|
extraArgs,
|
|
126
161
|
timeoutMs: config.sessionTimeoutMs,
|
|
127
|
-
env: secrets,
|
|
128
|
-
cwd: sandbox ? undefined :
|
|
162
|
+
env: toEnvMap(secrets),
|
|
163
|
+
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
129
164
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
130
165
|
signal: abortController.signal,
|
|
131
166
|
});
|
|
@@ -164,10 +199,12 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
164
199
|
}
|
|
165
200
|
catch (err) {
|
|
166
201
|
log.error(`session ${leaseId} threw before completing: ${String(err)}`);
|
|
202
|
+
const failureSummary = (0, redact_cjs_1.redactText)(`Session crashed: ${String(err)}`, registry.list());
|
|
167
203
|
await api
|
|
168
204
|
.completeLease(leaseId, {
|
|
169
205
|
status: "failed",
|
|
170
|
-
report:
|
|
206
|
+
report: failureSummary,
|
|
207
|
+
failure_summary: failureSummary,
|
|
171
208
|
evidence_urls: [],
|
|
172
209
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
173
210
|
exit_status: "crashed",
|
|
@@ -179,7 +216,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
179
216
|
clearInterval(heartbeatTimer);
|
|
180
217
|
secrets = {};
|
|
181
218
|
if (sandbox)
|
|
182
|
-
await sandbox.
|
|
219
|
+
await sandbox.stop();
|
|
220
|
+
await gitWorktree.cleanup();
|
|
183
221
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
184
222
|
}
|
|
185
223
|
}
|
|
@@ -191,6 +229,14 @@ function toEnvMap(secrets) {
|
|
|
191
229
|
}
|
|
192
230
|
if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
|
|
193
231
|
out.GITHUB_TOKEN = secrets["github-pat"];
|
|
232
|
+
// Make ordinary `git push` calls from the agent use the in-memory token.
|
|
233
|
+
// The helper contains only an env-var reference; the token itself never
|
|
234
|
+
// lands in argv, git config, or the worktree.
|
|
235
|
+
out.GIT_CONFIG_COUNT = "1";
|
|
236
|
+
out.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
237
|
+
out.GIT_CONFIG_VALUE_0 =
|
|
238
|
+
'!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f';
|
|
239
|
+
out.GIT_TERMINAL_PROMPT = "0";
|
|
194
240
|
}
|
|
195
241
|
return out;
|
|
196
242
|
}
|
package/dist/types.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Navarch runtime — shared wire types.
|
|
3
3
|
//
|
|
4
4
|
// These interfaces mirror the JSON shapes described in:
|
|
5
|
-
// - docs/
|
|
6
|
-
// - docs/
|
|
5
|
+
// - docs/navarch/schema-design.md (table columns => API field names, esp. §4, §5, §7)
|
|
6
|
+
// - docs/navarch/implementation-plan.md (WP-07 behavioral contract)
|
|
7
7
|
// - docs/agent-platform-project-plan.md (§3.8 dispatch/§3.9 adapter contract)
|
|
8
8
|
//
|
|
9
9
|
// The control-plane API is being built in parallel (WP-01/WP-04/WP-05) in
|
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: registers a machine, claims tasks from the control-plane dispatcher, runs them
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^20.14.0",
|
|
47
47
|
"typescript": "^5.7.2",
|
|
48
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^4.1.10"
|
|
49
49
|
}
|
|
50
50
|
}
|