@sagentlab/navarch-runtime 0.1.43 → 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 +14 -0
- package/dist/cli.cjs +17 -0
- package/dist/heartbeat-loop.cjs +43 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -630,6 +630,20 @@ contract used by `api.cts`, including:
|
|
|
630
630
|
`POST /api/machines/connect` is the project-scoped alternative: it redeems a
|
|
631
631
|
single-use token minted by an owner through the onboarding or Fleet UI.
|
|
632
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
|
+
|
|
633
647
|
## What needs live verification
|
|
634
648
|
|
|
635
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