@sagentlab/navarch-runtime 0.1.10 → 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/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 {
@@ -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.10",
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",