@sagentlab/navarch-runtime 0.1.1 → 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 +26 -17
- package/dist/claim-loop.cjs +1 -0
- package/dist/cli.cjs +38 -14
- package/dist/config.cjs +13 -4
- package/dist/git-worktree.cjs +142 -0
- package/dist/machine-store.cjs +3 -0
- package/dist/prompt.cjs +7 -1
- package/dist/sandbox.cjs +20 -6
- package/dist/session.cjs +63 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ 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)
|
|
@@ -20,7 +21,7 @@ the API contract.
|
|
|
20
21
|
|
|
21
22
|
```sh
|
|
22
23
|
git clone <this repo> && cd sagentlab/runtime
|
|
23
|
-
./install.sh # checks node
|
|
24
|
+
./install.sh # checks node, npm install, npm run build
|
|
24
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
|
|
@@ -48,8 +49,8 @@ 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
56
|
command. Unlike `register`, no `NAVARCH_ENROLLMENT_SECRET` or
|
|
@@ -59,7 +60,7 @@ 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://navarch.example.com
|
|
63
|
+
--name my-agent-1 --agent codex --api-base https://navarch.example.com
|
|
63
64
|
node bin/navarch.cjs start
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -72,9 +73,9 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
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
81
|
## Configuration (`NAVARCH_*` env vars)
|
|
@@ -87,17 +88,17 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
87
88
|
| `NAVARCH_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
|
|
88
89
|
| `NAVARCH_PROJECT_ID` | — | Alternative to `connect --project`. |
|
|
89
90
|
| `NAVARCH_CONFIG_DIR` | `~/.navarch` | Where `machine.json` lives. |
|
|
90
|
-
| `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` |
|
|
91
|
-
| `NAVARCH_MAX_SESSIONS` | `
|
|
92
|
-
| `NAVARCH_CAPABILITIES` | `docker-sandbox,shell` | Comma list reported at heartbeat/claim time. |
|
|
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. |
|
|
93
94
|
| `NAVARCH_OWNER_ZONE` | `sagentlab` | `sagentlab` or `customer-<slug>-premises` (project-plan.md §3.11). |
|
|
94
95
|
| `NAVARCH_POLL_INTERVAL_MS` | `5000` | Claim-loop poll interval. |
|
|
95
96
|
| `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
|
|
96
97
|
| `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
|
|
97
98
|
| `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
|
|
98
|
-
| `NAVARCH_SANDBOX_MODE` | `
|
|
99
|
+
| `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
|
|
99
100
|
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
|
|
100
|
-
| `NAVARCH_AGENT` | `claude-code` |
|
|
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. |
|
|
101
102
|
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
102
103
|
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). |
|
|
103
104
|
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
@@ -106,8 +107,10 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
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
|
|
@@ -120,6 +123,9 @@ export NAVARCH_AGENT=claude-code
|
|
|
120
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
|
|
@@ -149,7 +155,9 @@ cli.cts
|
|
|
149
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
162
|
(adapters/types.cts) by NAVARCH_AGENT, then .run(...):
|
|
155
163
|
- claudeCodeAdapter (adapters/claude.cts) — `claude -p <prompt> --mcp-config <path>`
|
|
@@ -158,7 +166,8 @@ cli.cts
|
|
|
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
|
package/dist/claim-loop.cjs
CHANGED
package/dist/cli.cjs
CHANGED
|
@@ -29,6 +29,15 @@ function invocation(subcommand) {
|
|
|
29
29
|
const prefix = viaNpx ? `npx ${PACKAGE_NAME}` : "navarch-runtime";
|
|
30
30
|
return `${prefix} ${subcommand}`;
|
|
31
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
|
+
}
|
|
32
41
|
function parseArgs(argv) {
|
|
33
42
|
const [command, ...rest] = argv;
|
|
34
43
|
const flags = {};
|
|
@@ -70,6 +79,7 @@ async function registerCommand(flags) {
|
|
|
70
79
|
.map((s) => s.trim())
|
|
71
80
|
.filter(Boolean);
|
|
72
81
|
const ownerZone = flags["owner-zone"] ?? config.ownerZone;
|
|
82
|
+
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
73
83
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
74
84
|
const result = await client.registerMachine({
|
|
75
85
|
enrollment_token: enrollmentToken,
|
|
@@ -83,6 +93,7 @@ async function registerCommand(flags) {
|
|
|
83
93
|
token: result.token,
|
|
84
94
|
name,
|
|
85
95
|
api_base: apiBase,
|
|
96
|
+
agent_type: agentType,
|
|
86
97
|
});
|
|
87
98
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
88
99
|
console.log("Machine registered.");
|
|
@@ -93,8 +104,8 @@ async function registerCommand(flags) {
|
|
|
93
104
|
/**
|
|
94
105
|
* `navarch-runtime connect` — "Connect an agent to a project"
|
|
95
106
|
* (docs/navarch/schema-design.md §7 "Agent connect"; self-hosted-runner
|
|
96
|
-
* style). The project-scoped sibling of `register`: redeems a
|
|
97
|
-
*
|
|
107
|
+
* style). The project-scoped sibling of `register`: redeems a single-use
|
|
108
|
+
* enrollment token a project owner minted from the platform UI
|
|
98
109
|
* (ConnectAgentPanel → POST /api/projects/:id/enrollment-tokens) instead of
|
|
99
110
|
* the global NAVARCH_ENROLLMENT_SECRET `register` needs. Prints the
|
|
100
111
|
* machine token exactly once, same discipline as `register`.
|
|
@@ -116,6 +127,7 @@ async function connectCommand(flags) {
|
|
|
116
127
|
.split(",")
|
|
117
128
|
.map((s) => s.trim())
|
|
118
129
|
.filter(Boolean);
|
|
130
|
+
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
119
131
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
120
132
|
const result = await client.connectMachine({
|
|
121
133
|
enrollment_token: enrollmentToken,
|
|
@@ -129,6 +141,7 @@ async function connectCommand(flags) {
|
|
|
129
141
|
token: result.token,
|
|
130
142
|
name,
|
|
131
143
|
api_base: apiBase,
|
|
144
|
+
agent_type: agentType,
|
|
132
145
|
});
|
|
133
146
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
134
147
|
console.log("Machine connected.");
|
|
@@ -136,16 +149,22 @@ async function connectCommand(flags) {
|
|
|
136
149
|
console.log(` token: ${result.token}`);
|
|
137
150
|
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
|
|
138
151
|
}
|
|
139
|
-
async function startCommand() {
|
|
140
|
-
const
|
|
141
|
-
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 };
|
|
142
161
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
143
162
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
144
163
|
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity);
|
|
145
164
|
const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId));
|
|
146
165
|
heartbeat.start();
|
|
147
166
|
claimLoop.start();
|
|
148
|
-
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}`);
|
|
149
168
|
const shutdown = () => {
|
|
150
169
|
log.info("shutting down...");
|
|
151
170
|
heartbeat.stop();
|
|
@@ -155,7 +174,7 @@ async function startCommand() {
|
|
|
155
174
|
process.on("SIGINT", shutdown);
|
|
156
175
|
process.on("SIGTERM", shutdown);
|
|
157
176
|
}
|
|
158
|
-
async function doctorCommand() {
|
|
177
|
+
async function doctorCommand(flags) {
|
|
159
178
|
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
160
179
|
const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
|
|
161
180
|
console.log(`api_base: ${config.apiBase}`);
|
|
@@ -165,23 +184,28 @@ async function doctorCommand() {
|
|
|
165
184
|
console.log(`capabilities: ${config.capabilities.join(", ")}`);
|
|
166
185
|
console.log(`sandbox_mode: ${config.sandboxMode}`);
|
|
167
186
|
console.log(`docker: ${dockerOk ? "available" : "NOT AVAILABLE (docker-backed sessions will fail)"}`);
|
|
187
|
+
let identity;
|
|
168
188
|
try {
|
|
169
|
-
|
|
170
|
-
console.log(`machine: ${identity.name} (${identity.machine_id})`);
|
|
189
|
+
identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
|
|
171
190
|
}
|
|
172
191
|
catch {
|
|
173
192
|
console.log(`machine: not registered — run \`${invocation("register")}\``);
|
|
193
|
+
return;
|
|
174
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}`);
|
|
175
199
|
}
|
|
176
200
|
function helpText() {
|
|
177
201
|
return `navarch-runtime — Navarch machine-side session manager
|
|
178
202
|
|
|
179
203
|
Usage:
|
|
180
204
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
181
|
-
[--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]
|
|
182
206
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
183
|
-
[--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
184
|
-
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]
|
|
185
209
|
navarch-runtime doctor
|
|
186
210
|
|
|
187
211
|
Configuration is via NAVARCH_* environment variables; see runtime/README.md.
|
|
@@ -198,10 +222,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
198
222
|
await connectCommand(flags);
|
|
199
223
|
break;
|
|
200
224
|
case "start":
|
|
201
|
-
await startCommand();
|
|
225
|
+
await startCommand(flags);
|
|
202
226
|
break;
|
|
203
227
|
case "doctor":
|
|
204
|
-
await doctorCommand();
|
|
228
|
+
await doctorCommand(flags);
|
|
205
229
|
break;
|
|
206
230
|
case "help":
|
|
207
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)
|
|
@@ -30,12 +34,17 @@ function envList(env, name, fallback) {
|
|
|
30
34
|
function loadRuntimeConfig(env = process.env) {
|
|
31
35
|
const configDir = env.NAVARCH_CONFIG_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), ".navarch");
|
|
32
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
43
|
apiBase: env.NAVARCH_API_BASE ?? "http://localhost:3000",
|
|
35
44
|
workspaceRoot: env.NAVARCH_WORKSPACE_ROOT ?? node_path_1.default.join(configDir, "sandboxes"),
|
|
36
45
|
configDir,
|
|
37
|
-
maxSessions: envInt(env, "NAVARCH_MAX_SESSIONS",
|
|
38
|
-
capabilities: envList(env, "NAVARCH_CAPABILITIES",
|
|
46
|
+
maxSessions: envInt(env, "NAVARCH_MAX_SESSIONS", 5),
|
|
47
|
+
capabilities: envList(env, "NAVARCH_CAPABILITIES", defaultCapabilities),
|
|
39
48
|
ownerZone: env.NAVARCH_OWNER_ZONE ?? "sagentlab",
|
|
40
49
|
pollIntervalMs: envInt(env, "NAVARCH_POLL_INTERVAL_MS", 5000),
|
|
41
50
|
machineHeartbeatIntervalMs: envInt(env, "NAVARCH_HEARTBEAT_INTERVAL_MS", 60_000),
|
|
@@ -43,13 +52,13 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
43
52
|
// interval must stay comfortably under that TTL.
|
|
44
53
|
leaseHeartbeatIntervalMs,
|
|
45
54
|
sessionTimeoutMs: envInt(env, "NAVARCH_SESSION_TIMEOUT_MS", 45 * 60 * 1000),
|
|
46
|
-
agentType: env.NAVARCH_AGENT
|
|
55
|
+
agentType: isRuntimeAgentType(env.NAVARCH_AGENT) ? env.NAVARCH_AGENT : "claude-code",
|
|
47
56
|
claudeBin: env.NAVARCH_CLAUDE_BIN ?? "claude",
|
|
48
57
|
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
49
58
|
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
50
59
|
codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
|
|
51
60
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
52
|
-
sandboxMode
|
|
61
|
+
sandboxMode,
|
|
53
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
|
@@ -48,6 +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" || process.env.NAVARCH_AGENT === "claude-code"
|
|
52
|
+
? process.env.NAVARCH_AGENT
|
|
53
|
+
: undefined,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
const stored = await loadMachineIdentity(configDir);
|
package/dist/prompt.cjs
CHANGED
|
@@ -10,13 +10,19 @@ exports.renderPrompt = renderPrompt;
|
|
|
10
10
|
*/
|
|
11
11
|
function renderPrompt(task, bundle) {
|
|
12
12
|
if (bundle.task_context) {
|
|
13
|
-
|
|
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()]
|
|
14
17
|
.filter((section) => Boolean(section))
|
|
15
18
|
.join("\n\n") + "\n";
|
|
16
19
|
}
|
|
17
20
|
const sections = [];
|
|
18
21
|
sections.push(`# Task: ${task.summary}`);
|
|
19
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
|
+
}
|
|
20
26
|
if (task.github_issue_url) {
|
|
21
27
|
sections.push(`GitHub issue: ${task.github_issue_url}`);
|
|
22
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;
|
|
@@ -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,14 +14,15 @@ 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
|
|
@@ -29,11 +30,11 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
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,16 +99,25 @@ 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,
|
|
@@ -90,8 +125,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
90
125
|
// auth app/api/mcp/route.ts requires -- see lib/navarch/mcp/context.ts),
|
|
91
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,13 +137,13 @@ 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
148
|
// Picks the Claude Code or Codex adapter per NAVARCH_AGENT
|
|
114
149
|
// (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
|
|
@@ -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/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": {
|