@sagentlab/navarch-runtime 0.1.8 → 0.1.10
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 +29 -3
- package/bin/git-credential-navarch.cjs +46 -0
- package/dist/adapters/codex.cjs +6 -2
- package/dist/cli.cjs +55 -15
- package/dist/config.cjs +2 -0
- package/dist/exit-conditions.cjs +17 -3
- package/dist/git-worktree.cjs +91 -30
- package/dist/session.cjs +170 -73
- package/dist/supervisor.cjs +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,8 +60,9 @@ project only (it will never be dispatched work from any other project).
|
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
62
|
npx @sagentlab/navarch-runtime connect --token flmt_<...> --project <project-id> \
|
|
63
|
-
--name my-agent-1 --agent codex --api-base https://navarch.example.com
|
|
64
|
-
|
|
63
|
+
--name my-agent-1 --agent codex --api-base https://navarch.example.com \
|
|
64
|
+
--config-dir ~/.navarch/my-agent-1
|
|
65
|
+
npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/my-agent-1
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
(`@sagentlab/navarch-runtime` is published to npm, so `npx
|
|
@@ -79,6 +80,30 @@ The from-source flow — `git clone` + `./install.sh` + `node bin/navarch.cjs
|
|
|
79
80
|
| `supervise [--agent claude-code\|codex]` | Runs the daemon under the update supervisor, enabling drain-safe automatic updates and rollback. |
|
|
80
81
|
| `doctor` | Prints resolved config + Docker/registration status; no side effects. |
|
|
81
82
|
|
|
83
|
+
### Running multiple agents on one machine
|
|
84
|
+
|
|
85
|
+
Every agent instance must have its own config directory. The directory contains
|
|
86
|
+
the machine identity, update state, managed runtime versions, and (by default)
|
|
87
|
+
session workspaces. Reusing it lets a later `connect` replace `machine.json` and
|
|
88
|
+
causes both processes to race over update and workspace state.
|
|
89
|
+
|
|
90
|
+
Pass the same `--config-dir` to `connect` and `supervise` for each instance:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npx @sagentlab/navarch-runtime connect <pebble enrollment options> \
|
|
94
|
+
--name pebble-agent-2816 --agent codex --config-dir ~/.navarch/pebble
|
|
95
|
+
npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/pebble
|
|
96
|
+
|
|
97
|
+
npx @sagentlab/navarch-runtime connect <tobi enrollment options> \
|
|
98
|
+
--name tobi-agent-1521 --agent codex --config-dir ~/.navarch/tobi
|
|
99
|
+
npx @sagentlab/navarch-runtime supervise --config-dir ~/.navarch/tobi
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`NAVARCH_CONFIG_DIR` remains equivalent for service managers and environment
|
|
103
|
+
files. A supervisor also pins its starting identity into replacement workers,
|
|
104
|
+
so an accidental later edit to `machine.json` cannot change that running
|
|
105
|
+
instance during an automatic-update handoff.
|
|
106
|
+
|
|
82
107
|
## Active-task guidance
|
|
83
108
|
|
|
84
109
|
Guidance added to a task that is already running is delivered on that
|
|
@@ -183,7 +208,7 @@ unchanged across the deployment.
|
|
|
183
208
|
| `NAVARCH_MACHINE_NAME` | — | Used by `register`. |
|
|
184
209
|
| `NAVARCH_ENROLLMENT_TOKEN` | — | Alternative to `register --token` / `connect --token`. |
|
|
185
210
|
| `NAVARCH_PROJECT_ID` | — | Alternative to `connect --project`. |
|
|
186
|
-
| `NAVARCH_CONFIG_DIR` | `~/.navarch` |
|
|
211
|
+
| `NAVARCH_CONFIG_DIR` | `~/.navarch` | Per-instance state root. Equivalent to `--config-dir`; use a different directory for every agent on the same host. |
|
|
187
212
|
| `NAVARCH_WORKSPACE_ROOT` | `<config dir>/sandboxes` | Persistent bare repo caches plus isolated per-session worktrees. |
|
|
188
213
|
| `NAVARCH_MAX_SESSIONS` | `5` | Local concurrent-session capacity cap — see `src/capacity.cts`. |
|
|
189
214
|
| `NAVARCH_CAPABILITIES` | `shell` (`docker-sandbox,shell` in Docker mode) | Comma list reported at heartbeat/claim time. |
|
|
@@ -192,6 +217,7 @@ unchanged across the deployment.
|
|
|
192
217
|
| `NAVARCH_HEARTBEAT_INTERVAL_MS` | `60000` | Machine-level heartbeat interval. |
|
|
193
218
|
| `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 15-minute lease TTL (schema-design.md §4). |
|
|
194
219
|
| `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
|
|
220
|
+
| `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
|
|
195
221
|
| `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
|
|
196
222
|
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
|
|
197
223
|
| `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. |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Git credential helper for managed Navarch repositories. Git invokes this
|
|
4
|
+
// for every remote operation, so each lookup gets a fresh installation token
|
|
5
|
+
// from the lease-scoped broker instead of relying on the session-start token.
|
|
6
|
+
|
|
7
|
+
const operation = process.argv[2];
|
|
8
|
+
if (operation !== "get") process.exit(0);
|
|
9
|
+
|
|
10
|
+
const apiBase = process.env.NAVARCH_GIT_CREDENTIAL_API_BASE?.replace(/\/+$/, "");
|
|
11
|
+
const machineToken = process.env.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN;
|
|
12
|
+
const leaseId = process.env.NAVARCH_GIT_CREDENTIAL_LEASE_ID;
|
|
13
|
+
|
|
14
|
+
if (!apiBase || !machineToken || !leaseId) {
|
|
15
|
+
console.error("navarch git credential helper is missing broker configuration");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`${apiBase}/api/broker/issue`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${machineToken}`,
|
|
25
|
+
"content-type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ lease_id: leaseId, secret_names: ["github-pat"] }),
|
|
28
|
+
signal: AbortSignal.timeout(15_000),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`broker returned HTTP ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
const result = await response.json();
|
|
34
|
+
const token = result?.secrets?.["github-pat"];
|
|
35
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
36
|
+
throw new Error("broker did not issue github-pat");
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(`username=x-access-token\npassword=${token}\n`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
console.error(`navarch git credential refresh failed: ${message}`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
void main();
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -136,7 +136,11 @@ function tomlKey(value) {
|
|
|
136
136
|
function safeEnvSegment(value) {
|
|
137
137
|
return value.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Parses stdout for `codex exec --json` usage/final-message events and folds
|
|
141
|
+
* them onto the raw result. Codex currently reports token counts but no USD
|
|
142
|
+
* cost, so costUsd remains absent unless the stream explicitly provides one.
|
|
143
|
+
*/
|
|
140
144
|
function attachUsage(result) {
|
|
141
145
|
const events = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout);
|
|
142
146
|
if (events.length === 0)
|
|
@@ -147,7 +151,7 @@ function attachUsage(result) {
|
|
|
147
151
|
...result,
|
|
148
152
|
tokensIn: usage.tokensIn,
|
|
149
153
|
tokensOut: usage.tokensOut,
|
|
150
|
-
costUsd: usage.costUsd,
|
|
154
|
+
...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),
|
|
151
155
|
...(reportText !== undefined ? { reportText } : {}),
|
|
152
156
|
};
|
|
153
157
|
}
|
package/dist/cli.cjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.main = main;
|
|
4
7
|
const config_cjs_1 = require("./config.cjs");
|
|
5
8
|
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
6
10
|
const machine_store_cjs_1 = require("./machine-store.cjs");
|
|
7
11
|
const api_cjs_1 = require("./api.cjs");
|
|
8
12
|
const capacity_cjs_1 = require("./capacity.cjs");
|
|
@@ -15,6 +19,19 @@ const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
|
|
|
15
19
|
const supervisor_cjs_1 = require("./supervisor.cjs");
|
|
16
20
|
const log = (0, logger_cjs_1.createLogger)("cli");
|
|
17
21
|
const PACKAGE_NAME = "@sagentlab/navarch-runtime";
|
|
22
|
+
/** Include a control-plane response's safe error detail in top-level CLI failures. */
|
|
23
|
+
function describeCliError(err) {
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
+
if (!err || typeof err !== "object" || !("body" in err))
|
|
26
|
+
return message;
|
|
27
|
+
const body = err.body;
|
|
28
|
+
const detail = body && typeof body === "object" && "error" in body && typeof body.error === "string"
|
|
29
|
+
? body.error
|
|
30
|
+
: typeof body === "string"
|
|
31
|
+
? body
|
|
32
|
+
: undefined;
|
|
33
|
+
return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
|
|
34
|
+
}
|
|
18
35
|
/**
|
|
19
36
|
* How to tell the user to re-invoke this CLI, matching however THEY launched
|
|
20
37
|
* it. The bare `navarch-runtime` bin only exists on PATH after a global install
|
|
@@ -32,6 +49,17 @@ function invocation(subcommand) {
|
|
|
32
49
|
const prefix = viaNpx ? `npx ${PACKAGE_NAME}` : "navarch-runtime";
|
|
33
50
|
return `${prefix} ${subcommand}`;
|
|
34
51
|
}
|
|
52
|
+
/** Keep the post-enrollment next step pinned to the identity we just saved. */
|
|
53
|
+
function superviseInvocation(configDir) {
|
|
54
|
+
return `${invocation("supervise")} --config-dir ${configDir}`;
|
|
55
|
+
}
|
|
56
|
+
/** Resolve CLI-local configuration without mutating the parent shell environment. */
|
|
57
|
+
function configFromFlags(flags) {
|
|
58
|
+
const configDir = flags["config-dir"];
|
|
59
|
+
return (0, config_cjs_1.loadRuntimeConfig)(configDir
|
|
60
|
+
? { ...process.env, NAVARCH_CONFIG_DIR: node_path_1.default.resolve(configDir) }
|
|
61
|
+
: process.env);
|
|
62
|
+
}
|
|
35
63
|
function agentFromFlag(flags) {
|
|
36
64
|
const value = flags.agent;
|
|
37
65
|
if (value === undefined)
|
|
@@ -66,7 +94,7 @@ function parseArgs(argv) {
|
|
|
66
94
|
* RegisterMachineRequest for the (assumed, to-confirm) endpoint contract.
|
|
67
95
|
*/
|
|
68
96
|
async function registerCommand(flags) {
|
|
69
|
-
const config = (
|
|
97
|
+
const config = configFromFlags(flags);
|
|
70
98
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
71
99
|
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
72
100
|
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
@@ -102,7 +130,7 @@ async function registerCommand(flags) {
|
|
|
102
130
|
console.log("Machine registered.");
|
|
103
131
|
console.log(` machine_id: ${result.machine_id}`);
|
|
104
132
|
console.log(` token: ${result.token}`);
|
|
105
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${
|
|
133
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${superviseInvocation(config.configDir)}\` to begin serving tasks.`);
|
|
106
134
|
}
|
|
107
135
|
/**
|
|
108
136
|
* `navarch-runtime connect` — "Connect an agent to a project"
|
|
@@ -114,7 +142,7 @@ async function registerCommand(flags) {
|
|
|
114
142
|
* machine token exactly once, same discipline as `register`.
|
|
115
143
|
*/
|
|
116
144
|
async function connectCommand(flags) {
|
|
117
|
-
const config = (
|
|
145
|
+
const config = configFromFlags(flags);
|
|
118
146
|
const apiBase = flags["api-base"] ?? config.apiBase;
|
|
119
147
|
const enrollmentToken = flags.token ?? process.env.NAVARCH_ENROLLMENT_TOKEN;
|
|
120
148
|
const name = flags.name ?? process.env.NAVARCH_MACHINE_NAME;
|
|
@@ -150,10 +178,10 @@ async function connectCommand(flags) {
|
|
|
150
178
|
console.log("Machine connected.");
|
|
151
179
|
console.log(` machine_id: ${result.machine_id}`);
|
|
152
180
|
console.log(` token: ${result.token}`);
|
|
153
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${
|
|
181
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${superviseInvocation(config.configDir)}\` to begin serving tasks.`);
|
|
154
182
|
}
|
|
155
183
|
async function startCommand(flags) {
|
|
156
|
-
const baseConfig = (
|
|
184
|
+
const baseConfig = configFromFlags(flags);
|
|
157
185
|
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(baseConfig.configDir, baseConfig.apiBase);
|
|
158
186
|
// Adapter selection belongs to the local machine: an explicit start flag
|
|
159
187
|
// wins, followed by NAVARCH_AGENT, the choice saved at connect/register
|
|
@@ -203,14 +231,25 @@ async function startCommand(flags) {
|
|
|
203
231
|
process.on("SIGTERM", shutdown);
|
|
204
232
|
}
|
|
205
233
|
async function superviseCommand(flags) {
|
|
206
|
-
const config = (
|
|
234
|
+
const config = configFromFlags(flags);
|
|
235
|
+
const identity = await (0, machine_store_cjs_1.resolveMachineIdentity)(config.configDir, config.apiBase);
|
|
207
236
|
const agentType = agentFromFlag(flags);
|
|
208
237
|
const workerArgs = agentType ? ["--agent", agentType] : [];
|
|
209
|
-
|
|
238
|
+
// Pin the identity for this supervisor's lifetime. Without this snapshot, a
|
|
239
|
+
// replacement worker rereads machine.json after an automatic update and can
|
|
240
|
+
// silently become a different machine if another terminal reused the same
|
|
241
|
+
// config directory in the meantime.
|
|
242
|
+
const exitCode = await (0, supervisor_cjs_1.superviseRuntime)(config.configDir, process.argv[1] ?? "", workerArgs, 120_000, {
|
|
243
|
+
NAVARCH_CONFIG_DIR: config.configDir,
|
|
244
|
+
NAVARCH_MACHINE_ID: identity.machine_id,
|
|
245
|
+
NAVARCH_MACHINE_TOKEN: identity.token,
|
|
246
|
+
NAVARCH_MACHINE_NAME: identity.name,
|
|
247
|
+
NAVARCH_API_BASE: identity.api_base,
|
|
248
|
+
});
|
|
210
249
|
process.exitCode = exitCode;
|
|
211
250
|
}
|
|
212
251
|
async function doctorCommand(flags) {
|
|
213
|
-
const config = (
|
|
252
|
+
const config = configFromFlags(flags);
|
|
214
253
|
const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
|
|
215
254
|
console.log(`api_base: ${config.apiBase}`);
|
|
216
255
|
console.log(`config_dir: ${config.configDir}`);
|
|
@@ -237,14 +276,15 @@ function helpText() {
|
|
|
237
276
|
|
|
238
277
|
Usage:
|
|
239
278
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
240
|
-
[--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
279
|
+
[--config-dir <path>] [--agent claude-code|codex] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
241
280
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
242
|
-
[--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
243
|
-
navarch-runtime start [--agent claude-code|codex]
|
|
244
|
-
navarch-runtime supervise [--agent claude-code|codex]
|
|
245
|
-
navarch-runtime doctor
|
|
281
|
+
[--config-dir <path>] [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
282
|
+
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex]
|
|
283
|
+
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex]
|
|
284
|
+
navarch-runtime doctor [--config-dir <path>]
|
|
246
285
|
|
|
247
|
-
|
|
286
|
+
Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
|
|
287
|
+
Other configuration is via NAVARCH_* environment variables; see runtime/README.md.
|
|
248
288
|
`;
|
|
249
289
|
}
|
|
250
290
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -278,7 +318,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
278
318
|
}
|
|
279
319
|
}
|
|
280
320
|
catch (err) {
|
|
281
|
-
log.error(
|
|
321
|
+
log.error(describeCliError(err));
|
|
282
322
|
process.exitCode = 1;
|
|
283
323
|
}
|
|
284
324
|
}
|
package/dist/config.cjs
CHANGED
|
@@ -57,6 +57,8 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
57
57
|
claudeExtraArgs: envList(env, "NAVARCH_CLAUDE_EXTRA_ARGS", []),
|
|
58
58
|
codexBin: env.NAVARCH_CODEX_BIN ?? "codex",
|
|
59
59
|
codexExtraArgs: envList(env, "NAVARCH_CODEX_EXTRA_ARGS", []),
|
|
60
|
+
gitAuthorName: env.NAVARCH_GIT_AUTHOR_NAME ?? "sagentlab",
|
|
61
|
+
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
60
62
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
61
63
|
sandboxMode,
|
|
62
64
|
dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -95,7 +95,7 @@ function parseCodexJsonEvents(stdout) {
|
|
|
95
95
|
function extractUsageFromCodexEvents(events) {
|
|
96
96
|
let tokensIn = 0;
|
|
97
97
|
let tokensOut = 0;
|
|
98
|
-
let costUsd
|
|
98
|
+
let costUsd;
|
|
99
99
|
for (const event of events) {
|
|
100
100
|
if (event.type === "turn.completed" && event.usage) {
|
|
101
101
|
tokensIn = event.usage.input_tokens ?? 0;
|
|
@@ -104,8 +104,22 @@ function extractUsageFromCodexEvents(events) {
|
|
|
104
104
|
costUsd = event.usage.total_cost_usd;
|
|
105
105
|
}
|
|
106
106
|
if (event.msg?.type === "token_count") {
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const nested = event.msg.info?.total_token_usage;
|
|
108
|
+
tokensIn = nested
|
|
109
|
+
? (nested.input_tokens ?? 0)
|
|
110
|
+
: (event.msg.input_tokens ?? 0) + (event.msg.cached_input_tokens ?? 0);
|
|
111
|
+
tokensOut = nested?.output_tokens ?? event.msg.output_tokens ?? 0;
|
|
112
|
+
if (typeof nested?.total_cost_usd === "number")
|
|
113
|
+
costUsd = nested.total_cost_usd;
|
|
114
|
+
}
|
|
115
|
+
if (event.type === "event_msg" && event.payload?.type === "token_count") {
|
|
116
|
+
const total = event.payload.info?.total_token_usage;
|
|
117
|
+
if (total) {
|
|
118
|
+
tokensIn = total.input_tokens ?? 0;
|
|
119
|
+
tokensOut = total.output_tokens ?? 0;
|
|
120
|
+
if (typeof total.total_cost_usd === "number")
|
|
121
|
+
costUsd = total.total_cost_usd;
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
124
|
if (typeof event.msg?.total_cost_usd === "number") {
|
|
111
125
|
costUsd = event.msg.total_cost_usd;
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -38,40 +38,95 @@ class GitWorktree {
|
|
|
38
38
|
await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.repositoryPath), { recursive: true });
|
|
39
39
|
await node_fs_1.promises.mkdir(this.sessionRoot, { recursive: true });
|
|
40
40
|
await withRepositoryLock(this.repositoryPath, async () => {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
await this.ensureRepositoryCache();
|
|
42
|
+
try {
|
|
43
|
+
await this.addSessionWorktree();
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!isInvalidReferenceError(error))
|
|
47
|
+
throw error;
|
|
48
|
+
// This bare cache owns the metadata for every active session worktree.
|
|
49
|
+
// Replacing it here would invalidate those worktrees, so leave it intact
|
|
50
|
+
// and fail with enough context for a coordinated repair.
|
|
51
|
+
throw new Error(`Failed to create a session worktree because the cached repository at ${this.repositoryPath} ` +
|
|
52
|
+
`could not resolve the fetched start ref. The shared cache was preserved to avoid invalidating ` +
|
|
53
|
+
`active worktrees; retry after active sessions finish or repair the cache in place: ${errorMessage(error)}`);
|
|
49
54
|
}
|
|
50
|
-
const remoteHead = await this.runGit(["--git-dir", this.repositoryPath, "ls-remote", "--symref", "origin", "HEAD"], true);
|
|
51
|
-
const defaultBranch = parseRemoteHead(remoteHead.stdout);
|
|
52
|
-
const startRef = defaultBranch
|
|
53
|
-
? defaultBranch.replace(/^refs\/heads\//, "refs/remotes/origin/")
|
|
54
|
-
: "HEAD";
|
|
55
|
-
await this.runGit([
|
|
56
|
-
"--git-dir",
|
|
57
|
-
this.repositoryPath,
|
|
58
|
-
"fetch",
|
|
59
|
-
"--prune",
|
|
60
|
-
"origin",
|
|
61
|
-
"+refs/heads/*:refs/remotes/origin/*",
|
|
62
|
-
], true);
|
|
63
|
-
await this.runGit([
|
|
64
|
-
"--git-dir",
|
|
65
|
-
this.repositoryPath,
|
|
66
|
-
"worktree",
|
|
67
|
-
"add",
|
|
68
|
-
"-b",
|
|
69
|
-
this.branch,
|
|
70
|
-
this.worktreePath,
|
|
71
|
-
startRef,
|
|
72
|
-
], false);
|
|
73
55
|
});
|
|
74
56
|
}
|
|
57
|
+
/** Clones the bare cache if missing, repoints origin if needed, and fetches all branches. */
|
|
58
|
+
async ensureRepositoryCache() {
|
|
59
|
+
if (!(await pathExists(node_path_1.default.join(this.repositoryPath, "HEAD")))) {
|
|
60
|
+
await this.runGit(["clone", "--bare", this.cloneUrl, this.repositoryPath], true);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const origin = await this.runGit(["--git-dir", this.repositoryPath, "remote", "get-url", "origin"], false);
|
|
64
|
+
if (normalizeCloneUrl(origin.stdout) !== normalizeCloneUrl(this.cloneUrl)) {
|
|
65
|
+
await this.runGit(["--git-dir", this.repositoryPath, "remote", "set-url", "origin", this.cloneUrl], false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await this.runGit([
|
|
69
|
+
"--git-dir",
|
|
70
|
+
this.repositoryPath,
|
|
71
|
+
"fetch",
|
|
72
|
+
"--prune",
|
|
73
|
+
"origin",
|
|
74
|
+
"+refs/heads/*:refs/remotes/origin/*",
|
|
75
|
+
], true);
|
|
76
|
+
}
|
|
77
|
+
async addSessionWorktree() {
|
|
78
|
+
const startRef = await this.resolveStartRef();
|
|
79
|
+
if (!startRef) {
|
|
80
|
+
// Empty remote (no commits yet): there is nothing to base the session on,
|
|
81
|
+
// so bootstrap an orphan branch the session can push as the first commit.
|
|
82
|
+
await this.addOrphanWorktree();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await this.runGit([
|
|
86
|
+
"--git-dir",
|
|
87
|
+
this.repositoryPath,
|
|
88
|
+
"worktree",
|
|
89
|
+
"add",
|
|
90
|
+
"-b",
|
|
91
|
+
this.branch,
|
|
92
|
+
this.worktreePath,
|
|
93
|
+
startRef,
|
|
94
|
+
], false);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolves the remote-tracking ref new session branches start from, or null
|
|
98
|
+
* when the remote has no branches at all (a freshly provisioned empty repo).
|
|
99
|
+
*/
|
|
100
|
+
async resolveStartRef() {
|
|
101
|
+
const remoteHead = await this.runGit(["--git-dir", this.repositoryPath, "ls-remote", "--symref", "origin", "HEAD"], true);
|
|
102
|
+
const defaultBranch = parseRemoteHead(remoteHead.stdout);
|
|
103
|
+
if (defaultBranch)
|
|
104
|
+
return defaultBranch.replace(/^refs\/heads\//, "refs/remotes/origin/");
|
|
105
|
+
// No symref (e.g. an unborn remote HEAD): fall back to whatever branches
|
|
106
|
+
// the fetch brought in, preferring the conventional default names.
|
|
107
|
+
const refs = await this.runGit(["--git-dir", this.repositoryPath, "for-each-ref", "--format=%(refname)", "refs/remotes/origin"], false);
|
|
108
|
+
const branches = refs.stdout
|
|
109
|
+
.split("\n")
|
|
110
|
+
.map((line) => line.trim())
|
|
111
|
+
.filter((line) => line && line !== "refs/remotes/origin/HEAD");
|
|
112
|
+
for (const preferred of ["refs/remotes/origin/main", "refs/remotes/origin/master"]) {
|
|
113
|
+
if (branches.includes(preferred))
|
|
114
|
+
return preferred;
|
|
115
|
+
}
|
|
116
|
+
return branches[0] ?? null;
|
|
117
|
+
}
|
|
118
|
+
async addOrphanWorktree() {
|
|
119
|
+
try {
|
|
120
|
+
await this.runGit(["--git-dir", this.repositoryPath, "worktree", "add", "--orphan", "-b", this.branch, this.worktreePath], false);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// `worktree add --orphan` needs git >= 2.42; older gits reject the flag.
|
|
124
|
+
if (!/unknown option|usage: git worktree add/i.test(errorMessage(error)))
|
|
125
|
+
throw error;
|
|
126
|
+
await this.runGit(["init", "--initial-branch", this.branch, this.worktreePath], false);
|
|
127
|
+
await this.runGit(["-C", this.worktreePath, "remote", "add", "origin", this.cloneUrl], false);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
75
130
|
async cleanup() {
|
|
76
131
|
await withRepositoryLock(this.repositoryPath, async () => {
|
|
77
132
|
await this.runner
|
|
@@ -172,3 +227,9 @@ function parseRemoteHead(output) {
|
|
|
172
227
|
const match = output.match(/^ref:\s+(refs\/heads\/[A-Za-z0-9._/-]+)\s+HEAD$/m);
|
|
173
228
|
return match?.[1] ?? null;
|
|
174
229
|
}
|
|
230
|
+
function errorMessage(error) {
|
|
231
|
+
return error instanceof Error ? error.message : String(error);
|
|
232
|
+
}
|
|
233
|
+
function isInvalidReferenceError(error) {
|
|
234
|
+
return /invalid reference/i.test(errorMessage(error));
|
|
235
|
+
}
|
package/dist/session.cjs
CHANGED
|
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runSession = runSession;
|
|
7
|
+
exports.toEnvMap = toEnvMap;
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
10
|
+
const api_cjs_1 = require("./api.cjs");
|
|
9
11
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
10
12
|
const index_cjs_1 = require("./adapters/index.cjs");
|
|
11
13
|
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
@@ -19,11 +21,14 @@ const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
|
19
21
|
const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
20
22
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
21
23
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
24
|
+
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
25
|
+
const PR_REQUIRED_COMPLETION_RETRIES = 2;
|
|
22
26
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
23
27
|
/**
|
|
24
28
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
25
29
|
* 1. write the prompt file
|
|
26
|
-
* 2. fetch secrets from the broker
|
|
30
|
+
* 2. fetch secrets from the broker at session start (managed GitHub git
|
|
31
|
+
* credentials are subsequently refreshed by a lease-scoped helper)
|
|
27
32
|
* 3. optionally stand up a Docker sandbox when explicitly configured
|
|
28
33
|
* 4. run the configured agent adapter (Claude Code or Codex, per
|
|
29
34
|
* NAVARCH_AGENT — adapters/index.cts#selectAdapter), heartbeating the
|
|
@@ -58,17 +63,30 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
58
63
|
await node_fs_1.promises.mkdir(workDir, { recursive: true });
|
|
59
64
|
const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
60
65
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
61
|
-
// Secrets: fetched once, held only in memory (registry + env map below),
|
|
66
|
+
// Secrets: initially fetched once, held only in memory (registry + env map below),
|
|
62
67
|
// never written to disk on the host. They only ever reach disk inside the
|
|
63
68
|
// sandbox's tmpfs (sandbox.cts injectEnv), which is wiped with the container.
|
|
64
69
|
const registry = new redact_cjs_1.SecretRegistry();
|
|
65
70
|
let secrets = {};
|
|
71
|
+
let managedGithubCredential = false;
|
|
66
72
|
const secretNames = bundle.secret_manifest.map((s) => s.name);
|
|
67
73
|
if (secretNames.length > 0) {
|
|
68
74
|
const issued = await api.issueSecrets({ lease_id: leaseId, secret_names: secretNames });
|
|
69
75
|
secrets = issued.secrets;
|
|
76
|
+
managedGithubCredential = issued.github_app_issued === true;
|
|
70
77
|
registry.registerAll(secrets);
|
|
71
78
|
}
|
|
79
|
+
let gitCredentialRefresh;
|
|
80
|
+
if (managedGithubCredential && secrets["github-pat"]) {
|
|
81
|
+
const helperPath = node_path_1.default.join(workDir, GIT_CREDENTIAL_HELPER_FILENAME);
|
|
82
|
+
await node_fs_1.promises.copyFile(node_path_1.default.resolve(__dirname, "../bin/git-credential-navarch.cjs"), helperPath);
|
|
83
|
+
gitCredentialRefresh = {
|
|
84
|
+
helperPath,
|
|
85
|
+
apiBase: api.getBaseUrl(),
|
|
86
|
+
machineToken: api.getToken() ?? "",
|
|
87
|
+
leaseId,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
72
90
|
const cloneUrl = bundle.repository?.clone_url ??
|
|
73
91
|
(task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
|
|
74
92
|
if (!cloneUrl) {
|
|
@@ -223,7 +241,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
223
241
|
await gitWorktree.prepare();
|
|
224
242
|
if (sandbox) {
|
|
225
243
|
await sandbox.create();
|
|
226
|
-
await sandbox.injectEnv(toEnvMap(secrets));
|
|
244
|
+
await sandbox.injectEnv(toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail));
|
|
227
245
|
}
|
|
228
246
|
// Picks the Claude Code or Codex adapter per NAVARCH_AGENT
|
|
229
247
|
// (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
|
|
@@ -234,7 +252,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
234
252
|
const bin = config.agentType === "codex" ? config.codexBin : config.claudeBin;
|
|
235
253
|
const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
|
|
236
254
|
const attempts = [];
|
|
237
|
-
let
|
|
255
|
+
let nextPrompt = null;
|
|
256
|
+
let prRequiredCompletionRetries = 0;
|
|
238
257
|
while (true) {
|
|
239
258
|
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
240
259
|
// It is already included in deliveredGuidance, so clear the pending
|
|
@@ -243,9 +262,11 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
243
262
|
activeAbortController = new AbortController();
|
|
244
263
|
if (leaseLost)
|
|
245
264
|
activeAbortController.abort();
|
|
246
|
-
const runPrompt =
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
const runPrompt = nextPrompt ??
|
|
266
|
+
(deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
267
|
+
? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
|
|
268
|
+
: promptText);
|
|
269
|
+
nextPrompt = null;
|
|
249
270
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
250
271
|
const turnResult = await adapter.run({
|
|
251
272
|
prompt: runPrompt,
|
|
@@ -255,7 +276,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
255
276
|
model: execution.model,
|
|
256
277
|
reasoningEffort: execution.reasoning_effort,
|
|
257
278
|
timeoutMs: config.sessionTimeoutMs,
|
|
258
|
-
env: toEnvMap(secrets),
|
|
279
|
+
env: toEnvMap(secrets, gitCredentialRefresh, config.gitAuthorName, config.gitAuthorEmail),
|
|
259
280
|
settingsPath: claudeSettingsPath,
|
|
260
281
|
codexGuardArgs,
|
|
261
282
|
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
@@ -270,66 +291,93 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
270
291
|
await pollLease();
|
|
271
292
|
if (!leaseLost && pendingGuidance.length > 0)
|
|
272
293
|
continue;
|
|
273
|
-
result = {
|
|
294
|
+
const result = {
|
|
274
295
|
...turnResult,
|
|
275
|
-
tokensIn: attempts
|
|
276
|
-
tokensOut: attempts
|
|
277
|
-
costUsd: attempts
|
|
296
|
+
tokensIn: sumReportedUsage(attempts, "tokensIn"),
|
|
297
|
+
tokensOut: sumReportedUsage(attempts, "tokensOut"),
|
|
298
|
+
costUsd: sumReportedUsage(attempts, "costUsd"),
|
|
278
299
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
|
|
284
|
-
repository: bundle.repository?.full_name ?? task.repo,
|
|
285
|
-
headBranch: gitWorktree.branch,
|
|
286
|
-
githubToken,
|
|
300
|
+
const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({
|
|
301
|
+
...result,
|
|
302
|
+
killedByLeaseLoss: leaseLost || result.killedByLeaseLoss,
|
|
287
303
|
});
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
304
|
+
try {
|
|
305
|
+
const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
|
|
306
|
+
repository: bundle.repository?.full_name ?? task.repo,
|
|
307
|
+
headBranch: gitWorktree.branch,
|
|
308
|
+
githubToken,
|
|
309
|
+
});
|
|
310
|
+
if (prUrl)
|
|
311
|
+
mapping.evidenceUrls.push(prUrl);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// Evidence discovery is best-effort: a GitHub outage or token scope
|
|
315
|
+
// mismatch must not turn an otherwise valid completion into a crash.
|
|
316
|
+
log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
|
|
317
|
+
}
|
|
318
|
+
const knownSecrets = registry.list();
|
|
319
|
+
if (mapping.leaseOutcome === "failed") {
|
|
320
|
+
log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
|
|
321
|
+
}
|
|
322
|
+
const transcript = attempts
|
|
323
|
+
.flatMap((attempt, index) => [
|
|
324
|
+
`# agent turn ${index + 1} stdout`,
|
|
325
|
+
(0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
|
|
326
|
+
"",
|
|
327
|
+
`# agent turn ${index + 1} stderr`,
|
|
328
|
+
(0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
|
|
329
|
+
"",
|
|
330
|
+
])
|
|
331
|
+
.join("\n");
|
|
332
|
+
let transcriptUrl;
|
|
333
|
+
try {
|
|
334
|
+
const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
|
|
335
|
+
await (0, upload_cjs_1.uploadTranscript)(upload_url, transcript);
|
|
336
|
+
transcriptUrl = public_url;
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
log.warn(`transcript upload failed for ${leaseId}: ${String(err)}`);
|
|
340
|
+
}
|
|
341
|
+
const completion = {
|
|
342
|
+
status: mapping.leaseOutcome,
|
|
343
|
+
report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
|
|
344
|
+
evidence_urls: mapping.evidenceUrls,
|
|
345
|
+
cost: {
|
|
346
|
+
...(result.tokensIn !== undefined ? { tokens_in: result.tokensIn } : {}),
|
|
347
|
+
...(result.tokensOut !== undefined ? { tokens_out: result.tokensOut } : {}),
|
|
348
|
+
...(result.costUsd !== undefined ? { cost_usd: result.costUsd } : {}),
|
|
349
|
+
},
|
|
350
|
+
transcript_url: transcriptUrl,
|
|
351
|
+
exit_status: mapping.exitStatus,
|
|
352
|
+
agent_type: config.agentType,
|
|
353
|
+
...executionReport,
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
await api.completeLease(leaseId, completion);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
const rejection = mapping.leaseOutcome === "completed" ? prRequiredRejectionMessage(err) : null;
|
|
361
|
+
if (!rejection)
|
|
362
|
+
throw err;
|
|
363
|
+
const redactedRejection = (0, redact_cjs_1.redactText)(rejection, knownSecrets);
|
|
364
|
+
if (prRequiredCompletionRetries < PR_REQUIRED_COMPLETION_RETRIES) {
|
|
365
|
+
prRequiredCompletionRetries += 1;
|
|
366
|
+
log.warn(`completion for ${leaseId} requires a pull request; restarting agent turn ${prRequiredCompletionRetries}/${PR_REQUIRED_COMPLETION_RETRIES} in the same worktree.`);
|
|
367
|
+
nextPrompt = redactedRejection;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
log.warn(`completion for ${leaseId} still requires a pull request after ${PR_REQUIRED_COMPLETION_RETRIES} retries; failing with the control-plane rejection.`);
|
|
371
|
+
await api.completeLease(leaseId, {
|
|
372
|
+
...completion,
|
|
373
|
+
status: "failed",
|
|
374
|
+
report: redactedRejection,
|
|
375
|
+
failure_summary: redactedRejection,
|
|
376
|
+
exit_status: "failed",
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
318
380
|
}
|
|
319
|
-
await api.completeLease(leaseId, {
|
|
320
|
-
status: mapping.leaseOutcome,
|
|
321
|
-
report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
|
|
322
|
-
evidence_urls: mapping.evidenceUrls,
|
|
323
|
-
cost: {
|
|
324
|
-
tokens_in: result.tokensIn ?? 0,
|
|
325
|
-
tokens_out: result.tokensOut ?? 0,
|
|
326
|
-
cost_usd: result.costUsd ?? 0,
|
|
327
|
-
},
|
|
328
|
-
transcript_url: transcriptUrl,
|
|
329
|
-
exit_status: mapping.exitStatus,
|
|
330
|
-
agent_type: config.agentType,
|
|
331
|
-
...executionReport,
|
|
332
|
-
});
|
|
333
381
|
}
|
|
334
382
|
catch (err) {
|
|
335
383
|
log.error(`session ${leaseId} threw before completing: ${String(err)}`);
|
|
@@ -357,22 +405,71 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
357
405
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
358
406
|
}
|
|
359
407
|
}
|
|
408
|
+
function sumReportedUsage(attempts, key) {
|
|
409
|
+
const reported = attempts.flatMap((attempt) => {
|
|
410
|
+
const value = attempt[key];
|
|
411
|
+
return value === undefined ? [] : [value];
|
|
412
|
+
});
|
|
413
|
+
return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
|
|
414
|
+
}
|
|
415
|
+
function prRequiredRejectionMessage(err) {
|
|
416
|
+
if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
|
|
417
|
+
return null;
|
|
418
|
+
if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
|
|
419
|
+
return null;
|
|
420
|
+
const body = err.body;
|
|
421
|
+
return body.code === "pr_required" && typeof body.error === "string"
|
|
422
|
+
? body.error
|
|
423
|
+
: null;
|
|
424
|
+
}
|
|
360
425
|
/** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
|
|
361
|
-
function toEnvMap(secrets) {
|
|
426
|
+
function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
|
|
427
|
+
// Keep the existing `(secrets, authorName, authorEmail)` call shape while
|
|
428
|
+
// accepting a refresh config before the author identity for managed repos.
|
|
429
|
+
const gitCredentialRefresh = typeof credentialRefreshOrAuthorName === "string"
|
|
430
|
+
? undefined
|
|
431
|
+
: credentialRefreshOrAuthorName;
|
|
432
|
+
const gitAuthorName = typeof credentialRefreshOrAuthorName === "string"
|
|
433
|
+
? credentialRefreshOrAuthorName
|
|
434
|
+
: gitAuthorNameOrEmail;
|
|
435
|
+
const resolvedGitAuthorEmail = typeof credentialRefreshOrAuthorName === "string"
|
|
436
|
+
? gitAuthorNameOrEmail
|
|
437
|
+
: gitAuthorEmail;
|
|
362
438
|
const out = {};
|
|
363
439
|
for (const [name, value] of Object.entries(secrets)) {
|
|
364
440
|
out[name.toUpperCase().replace(/[^A-Z0-9_]/g, "_")] = value;
|
|
365
441
|
}
|
|
442
|
+
const gitConfig = [
|
|
443
|
+
["user.name", gitAuthorName],
|
|
444
|
+
["user.email", resolvedGitAuthorEmail],
|
|
445
|
+
];
|
|
366
446
|
if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
|
|
367
447
|
out.GITHUB_TOKEN = secrets["github-pat"];
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
448
|
+
if (gitCredentialRefresh) {
|
|
449
|
+
out.NAVARCH_GIT_CREDENTIAL_API_BASE = gitCredentialRefresh.apiBase;
|
|
450
|
+
out.NAVARCH_GIT_CREDENTIAL_MACHINE_TOKEN = gitCredentialRefresh.machineToken;
|
|
451
|
+
out.NAVARCH_GIT_CREDENTIAL_LEASE_ID = gitCredentialRefresh.leaseId;
|
|
452
|
+
gitConfig.unshift(["credential.helper", ""], ["credential.helper", `!node ${shellQuote(gitCredentialRefresh.helperPath)}`]);
|
|
453
|
+
out.GIT_TERMINAL_PROMPT = "0";
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Make ordinary `git push` calls from the agent use the in-memory token.
|
|
457
|
+
// The helper contains only an env-var reference; the token itself never
|
|
458
|
+
// lands in argv, git config, or the worktree.
|
|
459
|
+
gitConfig.unshift([
|
|
460
|
+
"credential.helper",
|
|
461
|
+
'!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f',
|
|
462
|
+
]);
|
|
463
|
+
out.GIT_TERMINAL_PROMPT = "0";
|
|
464
|
+
}
|
|
376
465
|
}
|
|
466
|
+
out.GIT_CONFIG_COUNT = String(gitConfig.length);
|
|
467
|
+
gitConfig.forEach(([key, value], index) => {
|
|
468
|
+
out[`GIT_CONFIG_KEY_${index}`] = key;
|
|
469
|
+
out[`GIT_CONFIG_VALUE_${index}`] = value;
|
|
470
|
+
});
|
|
377
471
|
return out;
|
|
378
472
|
}
|
|
473
|
+
function shellQuote(value) {
|
|
474
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
475
|
+
}
|
package/dist/supervisor.cjs
CHANGED
|
@@ -6,10 +6,10 @@ const logger_cjs_1 = require("./logger.cjs");
|
|
|
6
6
|
const update_installer_cjs_1 = require("./update-installer.cjs");
|
|
7
7
|
const log = (0, logger_cjs_1.createLogger)("supervisor");
|
|
8
8
|
const UPDATE_RESTART_EXIT_CODE = 75;
|
|
9
|
-
function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
|
|
9
|
+
function runWorker(binPath, args, healthTimeoutMs, workerEnv, setCurrentChild, onHealthy) {
|
|
10
10
|
return new Promise((resolve) => {
|
|
11
11
|
const child = (0, node_child_process_1.spawn)(process.execPath, [binPath, "start", ...args], {
|
|
12
|
-
env: { ...process.env, NAVARCH_SUPERVISED: "1" },
|
|
12
|
+
env: { ...process.env, ...workerEnv, NAVARCH_SUPERVISED: "1" },
|
|
13
13
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
14
14
|
});
|
|
15
15
|
setCurrentChild(child);
|
|
@@ -70,7 +70,7 @@ function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
/** Runs the worker and owns update activation, health checking, and rollback. */
|
|
73
|
-
async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000) {
|
|
73
|
+
async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000, workerEnv = {}) {
|
|
74
74
|
let currentBin = initialBinPath;
|
|
75
75
|
let rollbackBin = null;
|
|
76
76
|
let candidatePending = null;
|
|
@@ -99,7 +99,7 @@ async function superviseRuntime(configDir, initialBinPath, workerArgs = [], heal
|
|
|
99
99
|
}
|
|
100
100
|
while (!stopping) {
|
|
101
101
|
const candidateThisRun = awaitingCandidateHealth;
|
|
102
|
-
const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, (child) => {
|
|
102
|
+
const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, workerEnv, (child) => {
|
|
103
103
|
currentChild = child;
|
|
104
104
|
}, async () => {
|
|
105
105
|
if (candidateThisRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
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",
|