@sagentlab/navarch-runtime 0.1.42 → 0.1.44
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 +28 -8
- package/dist/cli.cjs +17 -0
- package/dist/heartbeat-loop.cjs +43 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,7 +326,7 @@ unchanged across the deployment.
|
|
|
326
326
|
| `NAVARCH_ACP_BIN` | `dsh` | Path/name of an Agent Client Protocol v1 stdio server. DeepSeek Harness is the default implementation. |
|
|
327
327
|
| `NAVARCH_ACP_EXTRA_ARGS` | `--profile,acp` | Comma list of arguments used to start the ACP server. Override this together with `NAVARCH_ACP_BIN` for another ACP-compatible coding agent. |
|
|
328
328
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
329
|
-
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable. |
|
|
329
|
+
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode sessions get an adapter-native per-session worktree boundary guard (see below). Set `off` to disable it for every runtime on the machine — required to run OpenCode on the host. |
|
|
330
330
|
| `NAVARCH_GUARD_EXTRA_ROOTS` | — | `path.delimiter`-separated (`:` on POSIX) extra directories the worktree guard allows beyond the session worktree, shared bare repo, and temp dirs. |
|
|
331
331
|
|
|
332
332
|
## Worktree boundary guard (host mode)
|
|
@@ -378,10 +378,13 @@ as an explicit read-only mount.
|
|
|
378
378
|
Docker-mode sessions disable Gemini's implicit YOLO sandbox to avoid nesting
|
|
379
379
|
it inside Navarch's already isolated session container.
|
|
380
380
|
- **OpenCode:** the CLI cannot currently express a host-side boundary for all
|
|
381
|
-
shell side effects
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
shell side effects, so with the guard enabled it is resolved out of the
|
|
382
|
+
machine's advertised runtimes at startup — and a machine that offers only
|
|
383
|
+
OpenCode there refuses to start rather than claim tasks it can only fail.
|
|
384
|
+
Run OpenCode in Docker with an operator-owned image that contains an
|
|
385
|
+
authenticated `opencode` binary, or set `NAVARCH_WORKTREE_GUARD=off` only
|
|
386
|
+
when the whole machine is already isolated (the opt-out is machine-wide:
|
|
387
|
+
every runtime on it loses the boundary).
|
|
385
388
|
OpenCode receives a private, per-session config, project config discovery is
|
|
386
389
|
disabled, and lease MCP headers are referenced through child-only environment
|
|
387
390
|
variables instead of being copied into its config file or argv.
|
|
@@ -445,15 +448,18 @@ export NAVARCH_AGENT=codex
|
|
|
445
448
|
export NAVARCH_AGENT=gemini
|
|
446
449
|
|
|
447
450
|
# OpenCode — requires an authenticated `opencode` CLI (or
|
|
448
|
-
# NAVARCH_OPENCODE_BIN pointing at it).
|
|
449
|
-
# use a Docker image containing OpenCode for the normal
|
|
451
|
+
# NAVARCH_OPENCODE_BIN pointing at it). A guarded host refuses to start with
|
|
452
|
+
# only this runtime; use a Docker image containing OpenCode for the normal
|
|
453
|
+
# isolated path, or NAVARCH_WORKTREE_GUARD=off on an isolated machine.
|
|
450
454
|
export NAVARCH_AGENT=opencode
|
|
451
455
|
|
|
452
456
|
# Agent Client Protocol — defaults to DeepSeek Harness `dsh --profile acp`.
|
|
453
457
|
# Override NAVARCH_ACP_BIN / NAVARCH_ACP_EXTRA_ARGS for another ACP v1 server.
|
|
454
458
|
export NAVARCH_AGENT=acp
|
|
455
459
|
|
|
456
|
-
# Advanced compatibility mode: advertise every installed adapter.
|
|
460
|
+
# Advanced compatibility mode: advertise every installed adapter. On a guarded
|
|
461
|
+
# host, `opencode` is dropped from this list (see the boundary notes above);
|
|
462
|
+
# the rest are advertised as written.
|
|
457
463
|
export NAVARCH_RUNTIMES=claude-code,codex,gemini,opencode,acp
|
|
458
464
|
```
|
|
459
465
|
|
|
@@ -624,6 +630,20 @@ contract used by `api.cts`, including:
|
|
|
624
630
|
`POST /api/machines/connect` is the project-scoped alternative: it redeems a
|
|
625
631
|
single-use token minted by an owner through the onboarding or Fleet UI.
|
|
626
632
|
|
|
633
|
+
Machine-authenticated routes answer a rejected identity in three distinct
|
|
634
|
+
ways, and the runtime treats them differently:
|
|
635
|
+
|
|
636
|
+
- **503** — the control plane could not complete the token lookup. Transient;
|
|
637
|
+
retried like any other failure. (Before this split a failed lookup came back
|
|
638
|
+
as 401, so one database blip looked exactly like a revoked token.)
|
|
639
|
+
- **401** — the token is not recognized.
|
|
640
|
+
- **410** — this agent was removed from the fleet.
|
|
641
|
+
|
|
642
|
+
Three *consecutive* 401/410 heartbeats end the run: the runtime stops claiming,
|
|
643
|
+
lets active sessions finish, prints how to re-enroll, and exits 78. The
|
|
644
|
+
supervisor does not respawn on that code — a new process cannot fix a
|
|
645
|
+
credential the control plane no longer honors.
|
|
646
|
+
|
|
627
647
|
## What needs live verification
|
|
628
648
|
|
|
629
649
|
Most runtime behavior is covered offline. The Codex host adapter was also
|
package/dist/cli.cjs
CHANGED
|
@@ -20,6 +20,12 @@ const supervisor_cjs_1 = require("./supervisor.cjs");
|
|
|
20
20
|
const worktree_janitor_cjs_1 = require("./worktree-janitor.cjs");
|
|
21
21
|
const log = (0, logger_cjs_1.createLogger)("cli");
|
|
22
22
|
const PACKAGE_NAME = "@sagentlab/navarch-runtime";
|
|
23
|
+
/**
|
|
24
|
+
* Exit code for "this machine's identity is gone". Deliberately outside the
|
|
25
|
+
* supervisor's restart codes (75 update, 76 remote restart): respawning cannot
|
|
26
|
+
* fix a revoked or removed credential, so the supervisor must exit too.
|
|
27
|
+
*/
|
|
28
|
+
const CREDENTIALS_REJECTED_EXIT_CODE = 78;
|
|
23
29
|
/** Include a control-plane response's safe error detail in top-level CLI failures. */
|
|
24
30
|
function describeCliError(err) {
|
|
25
31
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -217,6 +223,17 @@ async function startCommand(flags) {
|
|
|
217
223
|
readySent = true;
|
|
218
224
|
process.send({ type: "navarch-ready", boot_id: bootId });
|
|
219
225
|
}
|
|
226
|
+
}, (status) => {
|
|
227
|
+
claimLoop.stop();
|
|
228
|
+
worktreeJanitor.stop();
|
|
229
|
+
log.error(status === 410
|
|
230
|
+
? `agent ${identity.name} has been removed from the fleet; it can no longer claim tasks.`
|
|
231
|
+
: `machine credentials for ${identity.name} were rejected by ${identity.api_base}; it can no longer claim tasks.`);
|
|
232
|
+
log.error(`Re-enroll this machine with a fresh "Connect an agent" token, then run ` +
|
|
233
|
+
`\`${superviseInvocation(config.configDir)}\` again.`);
|
|
234
|
+
// Active sessions keep running to completion; exiting mid-task would
|
|
235
|
+
// strand a worktree the control plane can no longer hear about either.
|
|
236
|
+
void capacity.waitForIdle().then(() => process.exit(CREDENTIALS_REJECTED_EXIT_CODE));
|
|
220
237
|
});
|
|
221
238
|
updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
|
|
222
239
|
config,
|
package/dist/heartbeat-loop.cjs
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MachineHeartbeatLoop = void 0;
|
|
4
|
+
const api_cjs_1 = require("./api.cjs");
|
|
4
5
|
const version_cjs_1 = require("./version.cjs");
|
|
5
6
|
const usage_limits_cjs_1 = require("./usage-limits.cjs");
|
|
6
7
|
const logger_cjs_1 = require("./logger.cjs");
|
|
7
8
|
const log = (0, logger_cjs_1.createLogger)("heartbeat");
|
|
9
|
+
/**
|
|
10
|
+
* How many consecutive credential rejections end the loop. A single 401 is not
|
|
11
|
+
* proof of anything — before the control plane learned to answer a failed
|
|
12
|
+
* token lookup with 503, a transient database blip surfaced here as one — so
|
|
13
|
+
* the identity is only declared dead once several beats in a row agree.
|
|
14
|
+
*/
|
|
15
|
+
const CREDENTIAL_REJECTION_LIMIT = 3;
|
|
16
|
+
/** 401: the token is not recognized. 410: this agent was removed from the fleet. */
|
|
17
|
+
function credentialRejectionStatus(err) {
|
|
18
|
+
if (!(err instanceof api_cjs_1.NavarchApiError))
|
|
19
|
+
return null;
|
|
20
|
+
return err.status === 401 || err.status === 410 ? err.status : null;
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Machine-level heartbeat loop (implementation-plan.md WP-07), separate from
|
|
10
24
|
* the per-lease heartbeat in session.cts: reports this machine's online
|
|
@@ -19,13 +33,22 @@ class MachineHeartbeatLoop {
|
|
|
19
33
|
bootId;
|
|
20
34
|
onResult;
|
|
21
35
|
onHealthy;
|
|
36
|
+
onCredentialsRejected;
|
|
22
37
|
timer = null;
|
|
23
38
|
heartbeatInFlight = false;
|
|
24
39
|
heartbeatPending = false;
|
|
25
40
|
updateState = "idle";
|
|
26
41
|
lastUpdateError;
|
|
27
42
|
draining = false;
|
|
28
|
-
|
|
43
|
+
credentialRejections = 0;
|
|
44
|
+
constructor(api, machineId, config, capacity, bootId, onResult, onHealthy,
|
|
45
|
+
/**
|
|
46
|
+
* Called once the machine's credentials have been rejected
|
|
47
|
+
* CREDENTIAL_REJECTION_LIMIT times running. The loop has stopped itself by
|
|
48
|
+
* then: without this the runtime would warn every interval forever while
|
|
49
|
+
* claiming nothing, which is indistinguishable from an idle agent.
|
|
50
|
+
*/
|
|
51
|
+
onCredentialsRejected) {
|
|
29
52
|
this.api = api;
|
|
30
53
|
this.machineId = machineId;
|
|
31
54
|
this.config = config;
|
|
@@ -33,6 +56,7 @@ class MachineHeartbeatLoop {
|
|
|
33
56
|
this.bootId = bootId;
|
|
34
57
|
this.onResult = onResult;
|
|
35
58
|
this.onHealthy = onHealthy;
|
|
59
|
+
this.onCredentialsRejected = onCredentialsRejected;
|
|
36
60
|
}
|
|
37
61
|
start() {
|
|
38
62
|
if (this.timer)
|
|
@@ -88,11 +112,28 @@ class MachineHeartbeatLoop {
|
|
|
88
112
|
...(this.lastUpdateError ? { last_update_error: this.lastUpdateError } : {}),
|
|
89
113
|
},
|
|
90
114
|
});
|
|
115
|
+
this.credentialRejections = 0;
|
|
91
116
|
this.onHealthy?.();
|
|
92
117
|
this.onResult?.(result);
|
|
93
118
|
}
|
|
94
119
|
catch (err) {
|
|
95
|
-
|
|
120
|
+
const rejection = credentialRejectionStatus(err);
|
|
121
|
+
if (rejection === null) {
|
|
122
|
+
this.credentialRejections = 0;
|
|
123
|
+
log.warn(`machine heartbeat failed: ${String(err)}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
this.credentialRejections += 1;
|
|
127
|
+
const reason = rejection === 410
|
|
128
|
+
? "this agent has been removed from the fleet"
|
|
129
|
+
: "the control plane does not recognize this machine token";
|
|
130
|
+
log.warn(`machine heartbeat rejected (${rejection}): ${reason} ` +
|
|
131
|
+
`(${this.credentialRejections}/${CREDENTIAL_REJECTION_LIMIT})`);
|
|
132
|
+
if (this.credentialRejections >= CREDENTIAL_REJECTION_LIMIT) {
|
|
133
|
+
this.stop();
|
|
134
|
+
this.onCredentialsRejected?.(rejection);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
96
137
|
}
|
|
97
138
|
} while (this.heartbeatPending);
|
|
98
139
|
}
|
package/package.json
CHANGED