@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.
@@ -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
- /** Parses stdout for `codex exec --json` usage/final-message events and folds them onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd/reportText unset when nothing parses — see exit-conditions.cts#parseCodexJsonEvents). */
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 response = await this.fetchImpl(`${this.baseUrl}${pathname}`, {
48
- method,
49
- headers,
50
- body: body === undefined ? undefined : JSON.stringify(body),
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
+ }
@@ -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 || this.claimInFlight || !this.capacity.hasCapacity())
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: ${err.message}${detail ? ` ${detail}` : ""}`);
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(err instanceof Error ? err.message : String(err));
321
+ log.error(describeCliError(err));
309
322
  process.exitCode = 1;
310
323
  }
311
324
  }
@@ -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 = 0;
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
- tokensIn = (event.msg.input_tokens ?? 0) + (event.msg.cached_input_tokens ?? 0);
108
- tokensOut = event.msg.output_tokens ?? 0;
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 result;
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 = deliveredGuidance.length > (bundle.guidance?.length ?? 0)
263
- ? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
264
- : promptText;
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.reduce((sum, attempt) => sum + (attempt.tokensIn ?? 0), 0),
292
- tokensOut: attempts.reduce((sum, attempt) => sum + (attempt.tokensOut ?? 0), 0),
293
- costUsd: attempts.reduce((sum, attempt) => sum + (attempt.costUsd ?? 0), 0),
296
+ tokensIn: sumReportedUsage(attempts, "tokensIn"),
297
+ tokensOut: sumReportedUsage(attempts, "tokensOut"),
298
+ costUsd: sumReportedUsage(attempts, "costUsd"),
294
299
  };
295
- break;
296
- }
297
- const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({ ...result, killedByLeaseLoss: leaseLost || result.killedByLeaseLoss });
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
- if (prUrl)
305
- mapping.evidenceUrls.push(prUrl);
306
- }
307
- catch (err) {
308
- // Evidence discovery is best-effort: a GitHub outage or token scope
309
- // mismatch must not turn an otherwise valid completion into a crash.
310
- log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
311
- }
312
- const knownSecrets = registry.list();
313
- if (mapping.leaseOutcome === "failed") {
314
- log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
315
- }
316
- const transcript = attempts
317
- .flatMap((attempt, index) => [
318
- `# agent turn ${index + 1} stdout`,
319
- (0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
320
- "",
321
- `# agent turn ${index + 1} stderr`,
322
- (0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
323
- "",
324
- ])
325
- .join("\n");
326
- let transcriptUrl;
327
- try {
328
- const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
329
- await (0, upload_cjs_1.uploadTranscript)(upload_url, transcript);
330
- transcriptUrl = public_url;
331
- }
332
- catch (err) {
333
- log.warn(`transcript upload failed for ${leaseId}: ${String(err)}`);
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
@@ -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.9",
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": "./bin/navarch.cjs"
21
+ "navarch-runtime": "bin/navarch.cjs"
22
22
  },
23
23
  "files": [
24
24
  "dist",