@sagentlab/navarch-runtime 0.1.12 → 0.1.13
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 +5 -2
- package/dist/adapter-capacity.cjs +75 -0
- package/dist/adapters/claude.cjs +15 -0
- package/dist/claim-loop.cjs +9 -1
- package/dist/config.cjs +4 -1
- package/dist/session.cjs +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -254,13 +254,13 @@ unchanged across the deployment.
|
|
|
254
254
|
| `NAVARCH_SESSION_TIMEOUT_MS` | `2700000` (45 min) | Hard kill timeout for a single session. |
|
|
255
255
|
| `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. |
|
|
256
256
|
| `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
|
|
257
|
-
| `NAVARCH_DOCKER_IMAGE` | `
|
|
257
|
+
| `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
|
|
258
258
|
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code`, `codex`, or `gemini`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
259
259
|
| `NAVARCH_RUNTIMES` | selected `NAVARCH_AGENT` | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
|
|
260
260
|
| `NAVARCH_UPDATE_CHANNEL` | `stable` | Release channel advertised by the worker (`stable` or `canary`); the server-managed machine channel remains authoritative. |
|
|
261
261
|
| `NAVARCH_AUTO_UPDATE` | on under `supervise` | Set `off`, `false`, or `0` to report releases without staging or activating them. Automatic activation is always off under plain `start`. |
|
|
262
262
|
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
263
|
-
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). `--allowedTools`/`--disallowedTools` layer rules onto auto mode; an explicit `--permission-mode`, `--permission-prompt-tool`, or bypass flag replaces the unattended default `--permission-mode auto`. |
|
|
263
|
+
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). `--allowedTools`/`--disallowedTools` layer rules onto auto mode; an explicit `--permission-mode`, `--permission-prompt-tool`, or bypass flag replaces the unattended default `--permission-mode auto`. Runtime sessions default to an empty `--setting-sources` list so machine/user/project hooks cannot leak into temporary checkouts; supply `--setting-sources=<sources>` here to opt in deliberately. |
|
|
264
264
|
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
265
265
|
| `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP `-c` overrides and `--json` (Codex). |
|
|
266
266
|
| `NAVARCH_GEMINI_BIN` | `gemini` | Path/name of the Google Gemini CLI binary. |
|
|
@@ -280,6 +280,9 @@ supported coding agents, using each CLI's native enforcement point:
|
|
|
280
280
|
does not blanket-preapprove the lease-scoped Navarch MCP tools. A generated
|
|
281
281
|
settings file (`src/worktree-guard.cts`, passed as `--settings`) also installs
|
|
282
282
|
`bin/worktree-guard-hook.cjs` as a fail-closed `PreToolUse` boundary hook.
|
|
283
|
+
User, project, and local settings sources are disabled by default, preventing
|
|
284
|
+
host-only hooks and plugins from leaking into unattended sessions; the
|
|
285
|
+
explicit generated settings file remains active.
|
|
283
286
|
- **Codex:** the runtime passes a one-off native permission profile with
|
|
284
287
|
`approval_policy="on-request"` and `approvals_reviewer="auto_review"`.
|
|
285
288
|
Codex's OS sandbox grants read/write access only to the allowed roots and
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectAdapterCapacityLimit = detectAdapterCapacityLimit;
|
|
4
|
+
const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
|
|
5
|
+
const DEFAULT_CAPACITY_COOLDOWN_MS = 15 * 60 * 1000;
|
|
6
|
+
const RESET_GRACE_MS = 30 * 1000;
|
|
7
|
+
const MAX_RESET_SEARCH_MINUTES = 48 * 60;
|
|
8
|
+
/**
|
|
9
|
+
* Detects a provider-side capacity response that applies beyond one task
|
|
10
|
+
* lease. Claude Code reports account session exhaustion as a successful CLI
|
|
11
|
+
* process containing a structured 429 result, so process exit status alone
|
|
12
|
+
* cannot distinguish it from an ordinary failed task.
|
|
13
|
+
*/
|
|
14
|
+
function detectAdapterCapacityLimit(result, nowMs = Date.now()) {
|
|
15
|
+
const parsed = (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout) ??
|
|
16
|
+
(0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stderr);
|
|
17
|
+
if (parsed?.api_error_status !== 429)
|
|
18
|
+
return null;
|
|
19
|
+
const detail = parsed.result ?? "";
|
|
20
|
+
if (!/\b(?:session|usage|rate)\s+limit\b|\btoo many requests\b/i.test(detail)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
retryAtMs: parseResetTime(detail, nowMs) ??
|
|
25
|
+
nowMs + DEFAULT_CAPACITY_COOLDOWN_MS,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolves messages such as "resets 9:50pm (America/New_York)" without
|
|
30
|
+
* assuming the runtime host uses the provider's timezone. Searching minute
|
|
31
|
+
* boundaries also handles UTC offsets and daylight-saving transitions using
|
|
32
|
+
* the platform's IANA timezone database.
|
|
33
|
+
*/
|
|
34
|
+
function parseResetTime(detail, nowMs) {
|
|
35
|
+
const match = detail.match(/\bresets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i);
|
|
36
|
+
if (!match)
|
|
37
|
+
return null;
|
|
38
|
+
const hour12 = Number(match[1]);
|
|
39
|
+
const minute = Number(match[2] ?? "0");
|
|
40
|
+
const meridiem = match[3]?.toLowerCase();
|
|
41
|
+
const timeZone = match[4]?.trim();
|
|
42
|
+
if (!Number.isInteger(hour12) ||
|
|
43
|
+
hour12 < 1 ||
|
|
44
|
+
hour12 > 12 ||
|
|
45
|
+
!Number.isInteger(minute) ||
|
|
46
|
+
minute < 0 ||
|
|
47
|
+
minute > 59 ||
|
|
48
|
+
!timeZone) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const targetHour = hour12 % 12 + (meridiem === "pm" ? 12 : 0);
|
|
52
|
+
let formatter;
|
|
53
|
+
try {
|
|
54
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
55
|
+
timeZone,
|
|
56
|
+
hour: "2-digit",
|
|
57
|
+
minute: "2-digit",
|
|
58
|
+
hourCycle: "h23",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const firstMinuteMs = Math.ceil((nowMs + 1) / 60_000) * 60_000;
|
|
65
|
+
for (let offset = 0; offset < MAX_RESET_SEARCH_MINUTES; offset += 1) {
|
|
66
|
+
const candidateMs = firstMinuteMs + offset * 60_000;
|
|
67
|
+
const parts = formatter.formatToParts(new Date(candidateMs));
|
|
68
|
+
const hour = Number(parts.find((part) => part.type === "hour")?.value);
|
|
69
|
+
const candidateMinute = Number(parts.find((part) => part.type === "minute")?.value);
|
|
70
|
+
if (hour === targetHour && candidateMinute === minute) {
|
|
71
|
+
return candidateMs + RESET_GRACE_MS;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -26,7 +26,22 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
26
26
|
*/
|
|
27
27
|
async function runClaudeCodeAdapter(options) {
|
|
28
28
|
const args = ["-p", options.prompt];
|
|
29
|
+
const hasSettingSources = options.extraArgs.some((arg) => arg === "--setting-sources" || arg.startsWith("--setting-sources="));
|
|
29
30
|
const hasExplicitPermissionMode = options.extraArgs.some((arg) => ["--permission-mode", "--permission-prompt-tool", "--dangerously-skip-permissions"].some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
31
|
+
// Runtime sessions must not inherit an operator's personal or project-local
|
|
32
|
+
// Claude hooks. Apart from making execution machine-dependent, those hooks
|
|
33
|
+
// commonly reference helper files through CLAUDE_PROJECT_DIR; that variable
|
|
34
|
+
// points at the temporary session checkout, where a host-only helper does
|
|
35
|
+
// not exist, and a failing SessionEnd hook turns an otherwise successful
|
|
36
|
+
// task into an adapter failure.
|
|
37
|
+
//
|
|
38
|
+
// An empty source list disables user/project/local settings while preserving
|
|
39
|
+
// the explicit --settings file below ("flagSettings" in Claude Code), so the
|
|
40
|
+
// generated worktree-guard hook remains active. Operators can deliberately
|
|
41
|
+
// opt sources back in through NAVARCH_CLAUDE_EXTRA_ARGS.
|
|
42
|
+
if (!hasSettingSources) {
|
|
43
|
+
args.push("--setting-sources", "");
|
|
44
|
+
}
|
|
30
45
|
// Navarch sessions are unattended, so route permission decisions through
|
|
31
46
|
// Claude Code's native auto-mode classifier instead of prompting a human or
|
|
32
47
|
// bypassing checks. Operators can replace this with a different permission
|
package/dist/claim-loop.cjs
CHANGED
|
@@ -23,6 +23,7 @@ class ClaimLoop {
|
|
|
23
23
|
claimInFlight = false;
|
|
24
24
|
consecutiveFailures = 0;
|
|
25
25
|
nextClaimAt = 0;
|
|
26
|
+
capacityCooldownUntil = 0;
|
|
26
27
|
quiescenceWaiters = new Set();
|
|
27
28
|
constructor(api, config, capacity, runSession) {
|
|
28
29
|
this.api = api;
|
|
@@ -60,7 +61,7 @@ class ClaimLoop {
|
|
|
60
61
|
if (this.stopped ||
|
|
61
62
|
this.claimInFlight ||
|
|
62
63
|
!this.capacity.hasCapacity() ||
|
|
63
|
-
Date.now() < this.nextClaimAt)
|
|
64
|
+
Date.now() < Math.max(this.nextClaimAt, this.capacityCooldownUntil))
|
|
64
65
|
return;
|
|
65
66
|
this.claimInFlight = true;
|
|
66
67
|
try {
|
|
@@ -85,6 +86,13 @@ class ClaimLoop {
|
|
|
85
86
|
this.capacity.acquire(claimed.lease_id);
|
|
86
87
|
log.info(`claimed task ${claimed.task.id} (${claimed.task.task_type}) as lease ${claimed.lease_id}, session ${sessionId}`);
|
|
87
88
|
this.runSession(claimed, sessionId)
|
|
89
|
+
.then((outcome) => {
|
|
90
|
+
const cooldownUntil = outcome?.claimCooldownUntil;
|
|
91
|
+
if (!cooldownUntil || cooldownUntil <= Date.now())
|
|
92
|
+
return;
|
|
93
|
+
this.capacityCooldownUntil = Math.max(this.capacityCooldownUntil, cooldownUntil);
|
|
94
|
+
log.warn(`adapter capacity exhausted; pausing new claims until ${new Date(this.capacityCooldownUntil).toISOString()}`);
|
|
95
|
+
})
|
|
88
96
|
.catch((err) => log.error(`session ${sessionId} failed: ${String(err)}`))
|
|
89
97
|
.finally(() => this.capacity.release(claimed.lease_id));
|
|
90
98
|
}
|
package/dist/config.cjs
CHANGED
|
@@ -3,10 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_SANDBOX_IMAGE = void 0;
|
|
6
7
|
exports.isRuntimeAgentType = isRuntimeAgentType;
|
|
7
8
|
exports.loadRuntimeConfig = loadRuntimeConfig;
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
/** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
|
|
12
|
+
exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
|
|
10
13
|
function isRuntimeAgentType(value) {
|
|
11
14
|
return value === "claude-code" || value === "codex" || value === "gemini";
|
|
12
15
|
}
|
|
@@ -66,7 +69,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
66
69
|
gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
|
|
67
70
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
68
71
|
sandboxMode,
|
|
69
|
-
dockerImage: env.NAVARCH_DOCKER_IMAGE ??
|
|
72
|
+
dockerImage: env.NAVARCH_DOCKER_IMAGE ?? exports.DEFAULT_SANDBOX_IMAGE,
|
|
70
73
|
// Multiple sessions share one machine; keeping each agent inside its own
|
|
71
74
|
// worktree is the safe default, so disabling is the explicit opt-out.
|
|
72
75
|
worktreeGuard: !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? ""),
|
package/dist/session.cjs
CHANGED
|
@@ -19,6 +19,7 @@ const logger_cjs_1 = require("./logger.cjs");
|
|
|
19
19
|
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
20
20
|
const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
21
21
|
const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
22
|
+
const adapter_capacity_cjs_1 = require("./adapter-capacity.cjs");
|
|
22
23
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
23
24
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
24
25
|
const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
|
|
@@ -129,6 +130,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
129
130
|
let leaseLost = false;
|
|
130
131
|
let leaseGone = false;
|
|
131
132
|
let heartbeatInFlight = null;
|
|
133
|
+
const sessionOutcome = {};
|
|
132
134
|
const pollLease = () => {
|
|
133
135
|
if (heartbeatInFlight)
|
|
134
136
|
return heartbeatInFlight;
|
|
@@ -302,6 +304,10 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
302
304
|
});
|
|
303
305
|
activeAbortController = null;
|
|
304
306
|
attempts.push(turnResult);
|
|
307
|
+
const capacityLimit = (0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(turnResult);
|
|
308
|
+
if (capacityLimit) {
|
|
309
|
+
sessionOutcome.claimCooldownUntil = Math.max(sessionOutcome.claimCooldownUntil ?? 0, capacityLimit.retryAtMs);
|
|
310
|
+
}
|
|
305
311
|
// Close the small race between a naturally completed turn and the next
|
|
306
312
|
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
307
313
|
// lease can be completed. A terminal heartbeat response means the
|
|
@@ -311,7 +317,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
311
317
|
await pollLease();
|
|
312
318
|
if (leaseGone) {
|
|
313
319
|
log.warn(`session ${leaseId} stopped without completion because its lease is no longer active.`);
|
|
314
|
-
return;
|
|
320
|
+
return sessionOutcome;
|
|
315
321
|
}
|
|
316
322
|
if (!leaseLost && pendingGuidance.length > 0)
|
|
317
323
|
continue;
|
|
@@ -383,7 +389,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
383
389
|
catch (err) {
|
|
384
390
|
if (isAlreadyReleasedCompletionError(err)) {
|
|
385
391
|
log.warn(`completion skipped for ${leaseId}: the lease was already released.`);
|
|
386
|
-
return;
|
|
392
|
+
return sessionOutcome;
|
|
387
393
|
}
|
|
388
394
|
const rejection = mapping.leaseOutcome === "completed" ? completionRemediationMessage(err) : null;
|
|
389
395
|
if (!rejection)
|
|
@@ -432,6 +438,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
432
438
|
await gitWorktree.cleanup();
|
|
433
439
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
434
440
|
}
|
|
441
|
+
return sessionOutcome;
|
|
435
442
|
}
|
|
436
443
|
function adapterCommand(config, runtime) {
|
|
437
444
|
switch (runtime) {
|
package/package.json
CHANGED