@sagentlab/navarch-runtime 0.1.9 → 0.1.11
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/dist/adapters/codex.cjs +6 -2
- package/dist/api.cjs +49 -6
- package/dist/claim-loop.cjs +20 -3
- package/dist/cli.cjs +14 -1
- package/dist/exit-conditions.cjs +17 -3
- package/dist/session.cjs +109 -60
- package/dist/worktree-guard.cjs +31 -0
- package/package.json +2 -2
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/api.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NavarchApiClient = exports.NavarchApiError = void 0;
|
|
3
|
+
exports.NavarchApiClient = exports.NavarchTransportError = exports.NavarchApiError = void 0;
|
|
4
4
|
class NavarchApiError extends Error {
|
|
5
5
|
status;
|
|
6
6
|
body;
|
|
@@ -12,6 +12,18 @@ class NavarchApiError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
exports.NavarchApiError = NavarchApiError;
|
|
15
|
+
/** A request that failed before the control plane returned an HTTP response. */
|
|
16
|
+
class NavarchTransportError extends Error {
|
|
17
|
+
method;
|
|
18
|
+
endpoint;
|
|
19
|
+
constructor(method, endpoint, cause) {
|
|
20
|
+
super(`Navarch API ${method} ${endpoint} transport failed: ${describeError(cause)}`, { cause });
|
|
21
|
+
this.method = method;
|
|
22
|
+
this.endpoint = endpoint;
|
|
23
|
+
this.name = "NavarchTransportError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.NavarchTransportError = NavarchTransportError;
|
|
15
27
|
/**
|
|
16
28
|
* Typed client for the Navarch control-plane API surface WP-07 depends on:
|
|
17
29
|
* dispatch/claim, per-lease heartbeat, complete, and broker/issue
|
|
@@ -44,11 +56,20 @@ class NavarchApiClient {
|
|
|
44
56
|
}
|
|
45
57
|
headers.authorization = `Bearer ${this.token}`;
|
|
46
58
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
const requestUrl = `${this.baseUrl}${pathname}`;
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await this.fetchImpl(requestUrl, {
|
|
63
|
+
method,
|
|
64
|
+
headers,
|
|
65
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
// Include enough request/cause context to diagnose DNS, connection, and
|
|
70
|
+
// TLS failures. Deliberately omit headers and URL credentials/query data.
|
|
71
|
+
throw new NavarchTransportError(method, safeEndpoint(requestUrl, pathname), err);
|
|
72
|
+
}
|
|
52
73
|
if (response.status === 204)
|
|
53
74
|
return null;
|
|
54
75
|
const text = await response.text();
|
|
@@ -133,3 +154,25 @@ function safeJsonParse(text) {
|
|
|
133
154
|
return null;
|
|
134
155
|
}
|
|
135
156
|
}
|
|
157
|
+
function safeEndpoint(requestUrl, pathname) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = new URL(requestUrl);
|
|
160
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return pathname;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function describeError(err) {
|
|
167
|
+
if (!(err instanceof Error))
|
|
168
|
+
return String(err);
|
|
169
|
+
let detail = `${err.name}: ${err.message}`;
|
|
170
|
+
const cause = err.cause;
|
|
171
|
+
if (cause instanceof Error) {
|
|
172
|
+
detail += `; cause: ${cause.name}: ${cause.message}`;
|
|
173
|
+
}
|
|
174
|
+
else if (cause && typeof cause === "object" && "code" in cause) {
|
|
175
|
+
detail += `; cause code: ${String(cause.code)}`;
|
|
176
|
+
}
|
|
177
|
+
return detail;
|
|
178
|
+
}
|
package/dist/claim-loop.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
5
5
|
const api_cjs_1 = require("./api.cjs");
|
|
6
6
|
const logger_cjs_1 = require("./logger.cjs");
|
|
7
7
|
const log = (0, logger_cjs_1.createLogger)("claim");
|
|
8
|
+
const MAX_CLAIM_BACKOFF_MS = 60_000;
|
|
8
9
|
/**
|
|
9
10
|
* Polls dispatch/claim on an interval, gated by available capacity
|
|
10
11
|
* (implementation-plan.md WP-07 "Claim loop: poll dispatch, receive context
|
|
@@ -20,6 +21,8 @@ class ClaimLoop {
|
|
|
20
21
|
timer = null;
|
|
21
22
|
stopped = false;
|
|
22
23
|
claimInFlight = false;
|
|
24
|
+
consecutiveFailures = 0;
|
|
25
|
+
nextClaimAt = 0;
|
|
23
26
|
quiescenceWaiters = new Set();
|
|
24
27
|
constructor(api, config, capacity, runSession) {
|
|
25
28
|
this.api = api;
|
|
@@ -31,6 +34,8 @@ class ClaimLoop {
|
|
|
31
34
|
if (this.timer)
|
|
32
35
|
return;
|
|
33
36
|
this.stopped = false;
|
|
37
|
+
this.consecutiveFailures = 0;
|
|
38
|
+
this.nextClaimAt = 0;
|
|
34
39
|
this.timer = setInterval(() => void this.tick(), this.config.pollIntervalMs);
|
|
35
40
|
}
|
|
36
41
|
stop() {
|
|
@@ -52,7 +57,10 @@ class ClaimLoop {
|
|
|
52
57
|
await new Promise((resolve) => this.quiescenceWaiters.add(resolve));
|
|
53
58
|
}
|
|
54
59
|
async tick() {
|
|
55
|
-
if (this.stopped ||
|
|
60
|
+
if (this.stopped ||
|
|
61
|
+
this.claimInFlight ||
|
|
62
|
+
!this.capacity.hasCapacity() ||
|
|
63
|
+
Date.now() < this.nextClaimAt)
|
|
56
64
|
return;
|
|
57
65
|
this.claimInFlight = true;
|
|
58
66
|
try {
|
|
@@ -66,6 +74,11 @@ class ClaimLoop {
|
|
|
66
74
|
agent_type: this.config.agentType,
|
|
67
75
|
session_id: sessionId,
|
|
68
76
|
});
|
|
77
|
+
if (this.consecutiveFailures > 0) {
|
|
78
|
+
log.info(`claim polling recovered after ${this.consecutiveFailures} failed attempt(s)`);
|
|
79
|
+
}
|
|
80
|
+
this.consecutiveFailures = 0;
|
|
81
|
+
this.nextClaimAt = 0;
|
|
69
82
|
if (!claimed)
|
|
70
83
|
return;
|
|
71
84
|
this.capacity.acquire(claimed.lease_id);
|
|
@@ -75,16 +88,20 @@ class ClaimLoop {
|
|
|
75
88
|
.finally(() => this.capacity.release(claimed.lease_id));
|
|
76
89
|
}
|
|
77
90
|
catch (err) {
|
|
91
|
+
this.consecutiveFailures += 1;
|
|
92
|
+
const retryDelayMs = Math.min(MAX_CLAIM_BACKOFF_MS, this.config.pollIntervalMs * 2 ** this.consecutiveFailures);
|
|
93
|
+
this.nextClaimAt = Date.now() + retryDelayMs;
|
|
78
94
|
// NavarchApiError's message is only the status line ("… failed with
|
|
79
95
|
// 500"); the control plane's actual error text lives in `.body`. Log it
|
|
80
96
|
// so a server-side claim failure is diagnosable from the runtime alone
|
|
81
97
|
// instead of an opaque bare status.
|
|
82
98
|
if (err instanceof api_cjs_1.NavarchApiError) {
|
|
83
99
|
const detail = typeof err.body === "string" ? err.body : JSON.stringify(err.body);
|
|
84
|
-
log.warn(`claim failed
|
|
100
|
+
log.warn(`claim failed (attempt ${this.consecutiveFailures}; retrying in ${retryDelayMs}ms): ` +
|
|
101
|
+
`${err.message}${detail ? ` — ${detail}` : ""}`);
|
|
85
102
|
}
|
|
86
103
|
else {
|
|
87
|
-
log.warn(`claim failed: ${String(err)}`);
|
|
104
|
+
log.warn(`claim failed (attempt ${this.consecutiveFailures}; retrying in ${retryDelayMs}ms): ${String(err)}`);
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
finally {
|
package/dist/cli.cjs
CHANGED
|
@@ -19,6 +19,19 @@ const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
|
|
|
19
19
|
const supervisor_cjs_1 = require("./supervisor.cjs");
|
|
20
20
|
const log = (0, logger_cjs_1.createLogger)("cli");
|
|
21
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
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* How to tell the user to re-invoke this CLI, matching however THEY launched
|
|
24
37
|
* it. The bare `navarch-runtime` bin only exists on PATH after a global install
|
|
@@ -305,7 +318,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
305
318
|
}
|
|
306
319
|
}
|
|
307
320
|
catch (err) {
|
|
308
|
-
log.error(
|
|
321
|
+
log.error(describeCliError(err));
|
|
309
322
|
process.exitCode = 1;
|
|
310
323
|
}
|
|
311
324
|
}
|
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/session.cjs
CHANGED
|
@@ -7,6 +7,7 @@ exports.runSession = runSession;
|
|
|
7
7
|
exports.toEnvMap = toEnvMap;
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_fs_1 = require("node:fs");
|
|
10
|
+
const api_cjs_1 = require("./api.cjs");
|
|
10
11
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
11
12
|
const index_cjs_1 = require("./adapters/index.cjs");
|
|
12
13
|
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
@@ -21,6 +22,7 @@ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
|
21
22
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
22
23
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
23
24
|
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
25
|
+
const PR_REQUIRED_COMPLETION_RETRIES = 2;
|
|
24
26
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
25
27
|
/**
|
|
26
28
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
@@ -250,7 +252,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
250
252
|
const bin = config.agentType === "codex" ? config.codexBin : config.claudeBin;
|
|
251
253
|
const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
|
|
252
254
|
const attempts = [];
|
|
253
|
-
let
|
|
255
|
+
let nextPrompt = null;
|
|
256
|
+
let prRequiredCompletionRetries = 0;
|
|
254
257
|
while (true) {
|
|
255
258
|
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
256
259
|
// It is already included in deliveredGuidance, so clear the pending
|
|
@@ -259,9 +262,11 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
259
262
|
activeAbortController = new AbortController();
|
|
260
263
|
if (leaseLost)
|
|
261
264
|
activeAbortController.abort();
|
|
262
|
-
const runPrompt =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
const runPrompt = nextPrompt ??
|
|
266
|
+
(deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
267
|
+
? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
|
|
268
|
+
: promptText);
|
|
269
|
+
nextPrompt = null;
|
|
265
270
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
266
271
|
const turnResult = await adapter.run({
|
|
267
272
|
prompt: runPrompt,
|
|
@@ -286,66 +291,93 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
286
291
|
await pollLease();
|
|
287
292
|
if (!leaseLost && pendingGuidance.length > 0)
|
|
288
293
|
continue;
|
|
289
|
-
result = {
|
|
294
|
+
const result = {
|
|
290
295
|
...turnResult,
|
|
291
|
-
tokensIn: attempts
|
|
292
|
-
tokensOut: attempts
|
|
293
|
-
costUsd: attempts
|
|
296
|
+
tokensIn: sumReportedUsage(attempts, "tokensIn"),
|
|
297
|
+
tokensOut: sumReportedUsage(attempts, "tokensOut"),
|
|
298
|
+
costUsd: sumReportedUsage(attempts, "costUsd"),
|
|
294
299
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
|
|
300
|
-
repository: bundle.repository?.full_name ?? task.repo,
|
|
301
|
-
headBranch: gitWorktree.branch,
|
|
302
|
-
githubToken,
|
|
300
|
+
const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({
|
|
301
|
+
...result,
|
|
302
|
+
killedByLeaseLoss: leaseLost || result.killedByLeaseLoss,
|
|
303
303
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
}
|
|
334
380
|
}
|
|
335
|
-
await api.completeLease(leaseId, {
|
|
336
|
-
status: mapping.leaseOutcome,
|
|
337
|
-
report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
|
|
338
|
-
evidence_urls: mapping.evidenceUrls,
|
|
339
|
-
cost: {
|
|
340
|
-
tokens_in: result.tokensIn ?? 0,
|
|
341
|
-
tokens_out: result.tokensOut ?? 0,
|
|
342
|
-
cost_usd: result.costUsd ?? 0,
|
|
343
|
-
},
|
|
344
|
-
transcript_url: transcriptUrl,
|
|
345
|
-
exit_status: mapping.exitStatus,
|
|
346
|
-
agent_type: config.agentType,
|
|
347
|
-
...executionReport,
|
|
348
|
-
});
|
|
349
381
|
}
|
|
350
382
|
catch (err) {
|
|
351
383
|
log.error(`session ${leaseId} threw before completing: ${String(err)}`);
|
|
@@ -373,6 +405,23 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
373
405
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
374
406
|
}
|
|
375
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
|
+
}
|
|
376
425
|
/** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
|
|
377
426
|
function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
|
|
378
427
|
// Keep the existing `(secrets, authorName, authorEmail)` call shape while
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -5,9 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.guardHookScriptPath = guardHookScriptPath;
|
|
7
7
|
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
8
|
+
exports.codexToolReadRoots = codexToolReadRoots;
|
|
8
9
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
13
|
const CODEX_GUARD_PROFILE = "navarch-worktree";
|
|
12
14
|
/**
|
|
13
15
|
* Tools the hook screens. Everything else — the lease-scoped Navarch MCP
|
|
@@ -55,6 +57,29 @@ async function prepareWorktreeGuard(options) {
|
|
|
55
57
|
await node_fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
56
58
|
return { settingsPath, configPath, hookScriptPath };
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Read-only roots required by Codex's own tools and installed skills.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately exclude the CODEX_HOME root itself: it may contain auth.json
|
|
64
|
+
* and user configuration. Package executables, installed skill instructions,
|
|
65
|
+
* plugin code, the operator's executable directory, and browser/toolchain
|
|
66
|
+
* installations are sufficient for helpers such as apply_patch, npm, gh, and
|
|
67
|
+
* browser evidence tooling.
|
|
68
|
+
*/
|
|
69
|
+
function codexToolReadRoots(env = process.env) {
|
|
70
|
+
const homeDir = env.HOME?.trim() || node_os_1.default.homedir();
|
|
71
|
+
const codexHome = env.CODEX_HOME?.trim() || node_path_1.default.join(homeDir, ".codex");
|
|
72
|
+
return [
|
|
73
|
+
node_path_1.default.join(codexHome, "packages"),
|
|
74
|
+
node_path_1.default.join(codexHome, "skills"),
|
|
75
|
+
node_path_1.default.join(codexHome, "plugins"),
|
|
76
|
+
node_path_1.default.join(homeDir, ".agents", "skills"),
|
|
77
|
+
node_path_1.default.join(homeDir, ".local", "bin"),
|
|
78
|
+
"/opt/homebrew",
|
|
79
|
+
node_path_1.default.join(homeDir, "Library", "Caches", "ms-playwright"),
|
|
80
|
+
node_path_1.default.join(homeDir, ".cache", "ms-playwright"),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
58
83
|
/**
|
|
59
84
|
* Builds one-off Codex permission-profile arguments for a host session.
|
|
60
85
|
*
|
|
@@ -78,6 +103,12 @@ function codexWorktreeGuardArgs(options) {
|
|
|
78
103
|
[node_path_1.default.resolve(options.worktreePath)]: "write",
|
|
79
104
|
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
80
105
|
};
|
|
106
|
+
for (const root of codexToolReadRoots()) {
|
|
107
|
+
const resolved = node_path_1.default.resolve(root);
|
|
108
|
+
if (isPathInside(resolved, node_path_1.default.resolve(options.workspaceRoot)))
|
|
109
|
+
continue;
|
|
110
|
+
filesystem[resolved] = "read";
|
|
111
|
+
}
|
|
81
112
|
for (const root of options.extraRoots ?? []) {
|
|
82
113
|
const resolved = node_path_1.default.resolve(root);
|
|
83
114
|
// Match the Claude hook's denied-root precedence: an extra root cannot
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"task-runner"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"navarch-runtime": "
|
|
21
|
+
"navarch-runtime": "bin/navarch.cjs"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|