@sagentlab/navarch-runtime 0.1.5 → 0.1.7
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 +116 -12
- package/bin/worktree-guard-hook.cjs +311 -0
- package/dist/adapters/claude.cjs +20 -19
- package/dist/adapters/codex.cjs +41 -10
- package/dist/capacity.cjs +12 -0
- package/dist/claim-loop.cjs +22 -0
- package/dist/cli.cjs +46 -7
- package/dist/config.cjs +10 -0
- package/dist/git-worktree.cjs +28 -1
- package/dist/heartbeat-loop.cjs +29 -3
- package/dist/session.cjs +39 -1
- package/dist/supervisor.cjs +149 -0
- package/dist/update-coordinator.cjs +53 -0
- package/dist/update-installer.cjs +164 -0
- package/dist/version.cjs +9 -0
- package/dist/worktree-guard.cjs +125 -0
- package/package.json +2 -1
package/dist/claim-loop.cjs
CHANGED
|
@@ -20,6 +20,7 @@ class ClaimLoop {
|
|
|
20
20
|
timer = null;
|
|
21
21
|
stopped = false;
|
|
22
22
|
claimInFlight = false;
|
|
23
|
+
quiescenceWaiters = new Set();
|
|
23
24
|
constructor(api, config, capacity, runSession) {
|
|
24
25
|
this.api = api;
|
|
25
26
|
this.config = config;
|
|
@@ -29,6 +30,7 @@ class ClaimLoop {
|
|
|
29
30
|
start() {
|
|
30
31
|
if (this.timer)
|
|
31
32
|
return;
|
|
33
|
+
this.stopped = false;
|
|
32
34
|
this.timer = setInterval(() => void this.tick(), this.config.pollIntervalMs);
|
|
33
35
|
}
|
|
34
36
|
stop() {
|
|
@@ -36,6 +38,18 @@ class ClaimLoop {
|
|
|
36
38
|
if (this.timer)
|
|
37
39
|
clearInterval(this.timer);
|
|
38
40
|
this.timer = null;
|
|
41
|
+
this.resolveQuiescenceWaiters();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stops new polls and waits for a claim request already on the wire to
|
|
45
|
+
* settle. If that request returns a lease, the session is still run; callers
|
|
46
|
+
* must then wait for CapacityTracker.waitForIdle() before restarting.
|
|
47
|
+
*/
|
|
48
|
+
async drain() {
|
|
49
|
+
this.stop();
|
|
50
|
+
if (!this.claimInFlight)
|
|
51
|
+
return;
|
|
52
|
+
await new Promise((resolve) => this.quiescenceWaiters.add(resolve));
|
|
39
53
|
}
|
|
40
54
|
async tick() {
|
|
41
55
|
if (this.stopped || this.claimInFlight || !this.capacity.hasCapacity())
|
|
@@ -75,7 +89,15 @@ class ClaimLoop {
|
|
|
75
89
|
}
|
|
76
90
|
finally {
|
|
77
91
|
this.claimInFlight = false;
|
|
92
|
+
this.resolveQuiescenceWaiters();
|
|
78
93
|
}
|
|
79
94
|
}
|
|
95
|
+
resolveQuiescenceWaiters() {
|
|
96
|
+
if (this.claimInFlight)
|
|
97
|
+
return;
|
|
98
|
+
for (const resolve of this.quiescenceWaiters)
|
|
99
|
+
resolve();
|
|
100
|
+
this.quiescenceWaiters.clear();
|
|
101
|
+
}
|
|
80
102
|
}
|
|
81
103
|
exports.ClaimLoop = ClaimLoop;
|
package/dist/cli.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.main = main;
|
|
4
4
|
const config_cjs_1 = require("./config.cjs");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const machine_store_cjs_1 = require("./machine-store.cjs");
|
|
6
7
|
const api_cjs_1 = require("./api.cjs");
|
|
7
8
|
const capacity_cjs_1 = require("./capacity.cjs");
|
|
@@ -10,6 +11,8 @@ const claim_loop_cjs_1 = require("./claim-loop.cjs");
|
|
|
10
11
|
const session_cjs_1 = require("./session.cjs");
|
|
11
12
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
12
13
|
const logger_cjs_1 = require("./logger.cjs");
|
|
14
|
+
const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
|
|
15
|
+
const supervisor_cjs_1 = require("./supervisor.cjs");
|
|
13
16
|
const log = (0, logger_cjs_1.createLogger)("cli");
|
|
14
17
|
const PACKAGE_NAME = "@sagentlab/navarch-runtime";
|
|
15
18
|
/**
|
|
@@ -99,7 +102,7 @@ async function registerCommand(flags) {
|
|
|
99
102
|
console.log("Machine registered.");
|
|
100
103
|
console.log(` machine_id: ${result.machine_id}`);
|
|
101
104
|
console.log(` token: ${result.token}`);
|
|
102
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("
|
|
105
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
105
108
|
* `navarch-runtime connect` — "Connect an agent to a project"
|
|
@@ -147,7 +150,7 @@ async function connectCommand(flags) {
|
|
|
147
150
|
console.log("Machine connected.");
|
|
148
151
|
console.log(` machine_id: ${result.machine_id}`);
|
|
149
152
|
console.log(` token: ${result.token}`);
|
|
150
|
-
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("
|
|
153
|
+
console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
|
|
151
154
|
}
|
|
152
155
|
async function startCommand(flags) {
|
|
153
156
|
const baseConfig = (0, config_cjs_1.loadRuntimeConfig)();
|
|
@@ -160,20 +163,52 @@ async function startCommand(flags) {
|
|
|
160
163
|
const config = { ...baseConfig, agentType };
|
|
161
164
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
162
165
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
163
|
-
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity);
|
|
164
166
|
const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId));
|
|
167
|
+
const bootId = (0, node_crypto_1.randomUUID)();
|
|
168
|
+
const updateCoordinatorRef = {};
|
|
169
|
+
let readySent = false;
|
|
170
|
+
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity, bootId, (result) => updateCoordinatorRef.current?.consider(result.update), () => {
|
|
171
|
+
if (!readySent && process.send) {
|
|
172
|
+
readySent = true;
|
|
173
|
+
process.send({ type: "navarch-ready", boot_id: bootId });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
|
|
177
|
+
config,
|
|
178
|
+
claimLoop,
|
|
179
|
+
capacity,
|
|
180
|
+
heartbeat,
|
|
181
|
+
});
|
|
165
182
|
heartbeat.start();
|
|
166
183
|
claimLoop.start();
|
|
167
184
|
log.info(`navarch-runtime started: machine=${identity.name} agent=${config.agentType} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
|
|
185
|
+
let shuttingDown = false;
|
|
168
186
|
const shutdown = () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
if (shuttingDown) {
|
|
188
|
+
log.warn("second shutdown signal received; forcing exit with active work");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
shuttingDown = true;
|
|
192
|
+
void (async () => {
|
|
193
|
+
log.info("shutting down: draining active sessions...");
|
|
194
|
+
heartbeat.setUpdateState("draining");
|
|
195
|
+
await claimLoop.drain();
|
|
196
|
+
await capacity.waitForIdle();
|
|
197
|
+
heartbeat.stop();
|
|
198
|
+
log.info("shutdown drain complete");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
})();
|
|
173
201
|
};
|
|
174
202
|
process.on("SIGINT", shutdown);
|
|
175
203
|
process.on("SIGTERM", shutdown);
|
|
176
204
|
}
|
|
205
|
+
async function superviseCommand(flags) {
|
|
206
|
+
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
207
|
+
const agentType = agentFromFlag(flags);
|
|
208
|
+
const workerArgs = agentType ? ["--agent", agentType] : [];
|
|
209
|
+
const exitCode = await (0, supervisor_cjs_1.superviseRuntime)(config.configDir, process.argv[1] ?? "", workerArgs);
|
|
210
|
+
process.exitCode = exitCode;
|
|
211
|
+
}
|
|
177
212
|
async function doctorCommand(flags) {
|
|
178
213
|
const config = (0, config_cjs_1.loadRuntimeConfig)();
|
|
179
214
|
const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
|
|
@@ -206,6 +241,7 @@ Usage:
|
|
|
206
241
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
207
242
|
[--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
208
243
|
navarch-runtime start [--agent claude-code|codex]
|
|
244
|
+
navarch-runtime supervise [--agent claude-code|codex]
|
|
209
245
|
navarch-runtime doctor
|
|
210
246
|
|
|
211
247
|
Configuration is via NAVARCH_* environment variables; see runtime/README.md.
|
|
@@ -224,6 +260,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
224
260
|
case "start":
|
|
225
261
|
await startCommand(flags);
|
|
226
262
|
break;
|
|
263
|
+
case "supervise":
|
|
264
|
+
await superviseCommand(flags);
|
|
265
|
+
break;
|
|
227
266
|
case "doctor":
|
|
228
267
|
await doctorCommand(flags);
|
|
229
268
|
break;
|
package/dist/config.cjs
CHANGED
|
@@ -60,5 +60,15 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
60
60
|
mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
|
|
61
61
|
sandboxMode,
|
|
62
62
|
dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
|
|
63
|
+
// Multiple sessions share one machine; keeping each agent inside its own
|
|
64
|
+
// worktree is the safe default, so disabling is the explicit opt-out.
|
|
65
|
+
worktreeGuard: !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? ""),
|
|
66
|
+
guardExtraRoots: (env.NAVARCH_GUARD_EXTRA_ROOTS ?? "")
|
|
67
|
+
.split(node_path_1.default.delimiter)
|
|
68
|
+
.map((s) => s.trim())
|
|
69
|
+
.filter(Boolean),
|
|
70
|
+
updateChannel: env.NAVARCH_UPDATE_CHANNEL === "canary" ? "canary" : "stable",
|
|
71
|
+
autoUpdate: env.NAVARCH_SUPERVISED === "1" &&
|
|
72
|
+
!["off", "false", "0"].includes(env.NAVARCH_AUTO_UPDATE ?? ""),
|
|
63
73
|
};
|
|
64
74
|
}
|
package/dist/git-worktree.cjs
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.GitWorktree = void 0;
|
|
7
|
+
exports.branchSlug = branchSlug;
|
|
7
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const sandbox_cjs_1 = require("./sandbox.cjs");
|
|
@@ -28,7 +29,7 @@ class GitWorktree {
|
|
|
28
29
|
this.sessionRoot = node_path_1.default.join(options.workspaceRoot, "sessions", sessionKey);
|
|
29
30
|
this.worktreePath = node_path_1.default.join(this.sessionRoot, "repo");
|
|
30
31
|
this.repositoryPath = node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
|
|
31
|
-
this.branch = `navarch/${
|
|
32
|
+
this.branch = `navarch/${branchSlug(options)}-${sessionKey.toLowerCase().slice(0, 8)}`;
|
|
32
33
|
this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
|
|
33
34
|
this.cloneUrl = options.cloneUrl;
|
|
34
35
|
this.githubToken = options.githubToken;
|
|
@@ -130,6 +131,32 @@ async function pathExists(value) {
|
|
|
130
131
|
return false;
|
|
131
132
|
}
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Kebab-case slug for the session branch, derived from the task summary so
|
|
136
|
+
* branch names read like `navarch/fix-login-redirect-a1b2c3d4` instead of a
|
|
137
|
+
* UUID mash. Falls back to the task type, then a task-id prefix, when the
|
|
138
|
+
* summary yields nothing. Output contains only [a-z0-9-] with no leading or
|
|
139
|
+
* trailing dash, so it is always a valid git ref component (no `..`, no
|
|
140
|
+
* trailing `.lock`).
|
|
141
|
+
*/
|
|
142
|
+
function branchSlug(options) {
|
|
143
|
+
for (const candidate of [options.taskSummary, options.taskType]) {
|
|
144
|
+
const slug = (candidate ?? "")
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
147
|
+
.replace(/^-+/, "")
|
|
148
|
+
.slice(0, 40)
|
|
149
|
+
.replace(/-+$/, "");
|
|
150
|
+
if (slug)
|
|
151
|
+
return slug;
|
|
152
|
+
}
|
|
153
|
+
const idSlug = safePathSegment(options.taskId)
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
156
|
+
.slice(0, 8)
|
|
157
|
+
.replace(/^-+|-+$/g, "");
|
|
158
|
+
return idSlug || "task";
|
|
159
|
+
}
|
|
133
160
|
function safePathSegment(value) {
|
|
134
161
|
const safe = value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
|
|
135
162
|
if (!safe)
|
package/dist/heartbeat-loop.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MachineHeartbeatLoop = void 0;
|
|
4
|
+
const version_cjs_1 = require("./version.cjs");
|
|
4
5
|
const logger_cjs_1 = require("./logger.cjs");
|
|
5
6
|
const log = (0, logger_cjs_1.createLogger)("heartbeat");
|
|
6
7
|
/**
|
|
@@ -14,12 +15,21 @@ class MachineHeartbeatLoop {
|
|
|
14
15
|
machineId;
|
|
15
16
|
config;
|
|
16
17
|
capacity;
|
|
18
|
+
bootId;
|
|
19
|
+
onResult;
|
|
20
|
+
onHealthy;
|
|
17
21
|
timer = null;
|
|
18
|
-
|
|
22
|
+
updateState = "idle";
|
|
23
|
+
lastUpdateError;
|
|
24
|
+
draining = false;
|
|
25
|
+
constructor(api, machineId, config, capacity, bootId, onResult, onHealthy) {
|
|
19
26
|
this.api = api;
|
|
20
27
|
this.machineId = machineId;
|
|
21
28
|
this.config = config;
|
|
22
29
|
this.capacity = capacity;
|
|
30
|
+
this.bootId = bootId;
|
|
31
|
+
this.onResult = onResult;
|
|
32
|
+
this.onHealthy = onHealthy;
|
|
23
33
|
}
|
|
24
34
|
start() {
|
|
25
35
|
if (this.timer)
|
|
@@ -32,12 +42,28 @@ class MachineHeartbeatLoop {
|
|
|
32
42
|
clearInterval(this.timer);
|
|
33
43
|
this.timer = null;
|
|
34
44
|
}
|
|
45
|
+
setUpdateState(state, error) {
|
|
46
|
+
this.updateState = state;
|
|
47
|
+
this.lastUpdateError = error;
|
|
48
|
+
this.draining = state === "draining";
|
|
49
|
+
void this.tick();
|
|
50
|
+
}
|
|
35
51
|
async tick() {
|
|
36
52
|
try {
|
|
37
|
-
await this.api.machineHeartbeat(this.machineId, {
|
|
38
|
-
available_capacity: this.capacity.available(),
|
|
53
|
+
const result = await this.api.machineHeartbeat(this.machineId, {
|
|
54
|
+
available_capacity: this.draining ? 0 : this.capacity.available(),
|
|
39
55
|
capabilities: this.config.capabilities,
|
|
56
|
+
runtime: {
|
|
57
|
+
version: version_cjs_1.RUNTIME_VERSION,
|
|
58
|
+
updater_protocol: version_cjs_1.UPDATER_PROTOCOL_VERSION,
|
|
59
|
+
channel: this.config.updateChannel,
|
|
60
|
+
state: this.updateState,
|
|
61
|
+
boot_id: this.bootId,
|
|
62
|
+
...(this.lastUpdateError ? { last_update_error: this.lastUpdateError } : {}),
|
|
63
|
+
},
|
|
40
64
|
});
|
|
65
|
+
this.onHealthy?.();
|
|
66
|
+
this.onResult?.(result);
|
|
41
67
|
}
|
|
42
68
|
catch (err) {
|
|
43
69
|
log.warn(`machine heartbeat failed: ${String(err)}`);
|
package/dist/session.cjs
CHANGED
|
@@ -16,6 +16,7 @@ const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
|
16
16
|
const logger_cjs_1 = require("./logger.cjs");
|
|
17
17
|
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
18
18
|
const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
19
|
+
const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
|
|
19
20
|
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
20
21
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
21
22
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
@@ -42,7 +43,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
42
43
|
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
43
44
|
const execution = bundle.execution ?? {
|
|
44
45
|
profile: task.execution_profile ?? "standard",
|
|
45
|
-
model: config.agentType === "codex" ? "gpt-5.6" : "best",
|
|
46
|
+
model: config.agentType === "codex" ? "gpt-5.6-sol" : "best",
|
|
46
47
|
reasoning_effort: "medium",
|
|
47
48
|
};
|
|
48
49
|
const executionReport = {
|
|
@@ -91,6 +92,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
91
92
|
workspaceRoot: config.workspaceRoot,
|
|
92
93
|
projectId: task.project_id,
|
|
93
94
|
taskId: task.id,
|
|
95
|
+
taskSummary: task.summary,
|
|
96
|
+
taskType: task.task_type,
|
|
94
97
|
sessionId,
|
|
95
98
|
cloneUrl,
|
|
96
99
|
githubToken,
|
|
@@ -183,6 +186,39 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
183
186
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
184
187
|
mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
|
|
185
188
|
}
|
|
189
|
+
// Worktree boundary guard (worktree-guard.cts): several sessions share this
|
|
190
|
+
// machine. Host-mode Claude gets a generated PreToolUse hook; host-mode
|
|
191
|
+
// Codex gets an OS-enforced native permission profile over the same roots.
|
|
192
|
+
// Docker mode already has a container boundary. NAVARCH_WORKTREE_GUARD=off
|
|
193
|
+
// opts out for either agent.
|
|
194
|
+
let claudeSettingsPath = null;
|
|
195
|
+
let codexGuardArgs;
|
|
196
|
+
if (config.sandboxMode === "host" && config.worktreeGuard) {
|
|
197
|
+
if (config.agentType === "claude-code") {
|
|
198
|
+
if (config.claudeExtraArgs.includes("--settings")) {
|
|
199
|
+
log.warn("NAVARCH_CLAUDE_EXTRA_ARGS supplies --settings; skipping the generated worktree-guard settings for this session.");
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
|
|
203
|
+
workDir,
|
|
204
|
+
worktreePath: gitWorktree.worktreePath,
|
|
205
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
206
|
+
workspaceRoot: config.workspaceRoot,
|
|
207
|
+
extraRoots: config.guardExtraRoots,
|
|
208
|
+
});
|
|
209
|
+
claudeSettingsPath = guard.settingsPath;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
214
|
+
workDir,
|
|
215
|
+
worktreePath: gitWorktree.worktreePath,
|
|
216
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
217
|
+
workspaceRoot: config.workspaceRoot,
|
|
218
|
+
extraRoots: config.guardExtraRoots,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
186
222
|
try {
|
|
187
223
|
await gitWorktree.prepare();
|
|
188
224
|
if (sandbox) {
|
|
@@ -220,6 +256,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
220
256
|
reasoningEffort: execution.reasoning_effort,
|
|
221
257
|
timeoutMs: config.sessionTimeoutMs,
|
|
222
258
|
env: toEnvMap(secrets),
|
|
259
|
+
settingsPath: claudeSettingsPath,
|
|
260
|
+
codexGuardArgs,
|
|
223
261
|
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
224
262
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
225
263
|
signal: activeAbortController.signal,
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.superviseRuntime = superviseRuntime;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const logger_cjs_1 = require("./logger.cjs");
|
|
6
|
+
const update_installer_cjs_1 = require("./update-installer.cjs");
|
|
7
|
+
const log = (0, logger_cjs_1.createLogger)("supervisor");
|
|
8
|
+
const UPDATE_RESTART_EXIT_CODE = 75;
|
|
9
|
+
function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [binPath, "start", ...args], {
|
|
12
|
+
env: { ...process.env, NAVARCH_SUPERVISED: "1" },
|
|
13
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
14
|
+
});
|
|
15
|
+
setCurrentChild(child);
|
|
16
|
+
let healthy = false;
|
|
17
|
+
let healthSignalReceived = false;
|
|
18
|
+
let healthTimedOut = false;
|
|
19
|
+
let healthCommit = Promise.resolve();
|
|
20
|
+
let healthTimer = null;
|
|
21
|
+
let forceKillTimer = null;
|
|
22
|
+
let settled = false;
|
|
23
|
+
const finish = (code) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
if (healthTimer)
|
|
28
|
+
clearTimeout(healthTimer);
|
|
29
|
+
if (forceKillTimer)
|
|
30
|
+
clearTimeout(forceKillTimer);
|
|
31
|
+
setCurrentChild(null);
|
|
32
|
+
void healthCommit.finally(() => resolve({ code, healthy }));
|
|
33
|
+
};
|
|
34
|
+
if (healthTimeoutMs !== null) {
|
|
35
|
+
healthTimer = setTimeout(() => {
|
|
36
|
+
if (!healthy) {
|
|
37
|
+
healthTimedOut = true;
|
|
38
|
+
log.error("updated runtime missed its startup health deadline; rolling back");
|
|
39
|
+
child.kill("SIGTERM");
|
|
40
|
+
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 10_000);
|
|
41
|
+
}
|
|
42
|
+
}, healthTimeoutMs);
|
|
43
|
+
}
|
|
44
|
+
child.on("message", (message) => {
|
|
45
|
+
if (!healthSignalReceived &&
|
|
46
|
+
!healthTimedOut &&
|
|
47
|
+
typeof message === "object" &&
|
|
48
|
+
message !== null &&
|
|
49
|
+
message.type === "navarch-ready") {
|
|
50
|
+
healthSignalReceived = true;
|
|
51
|
+
healthCommit = onHealthy()
|
|
52
|
+
.then(() => {
|
|
53
|
+
healthy = true;
|
|
54
|
+
if (healthTimer)
|
|
55
|
+
clearTimeout(healthTimer);
|
|
56
|
+
if (forceKillTimer)
|
|
57
|
+
clearTimeout(forceKillTimer);
|
|
58
|
+
})
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
log.error(`could not commit runtime activation: ${String(error)}`);
|
|
61
|
+
child.kill("SIGTERM");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
child.once("error", (error) => {
|
|
66
|
+
log.error(`could not start runtime worker: ${String(error)}`);
|
|
67
|
+
finish(1);
|
|
68
|
+
});
|
|
69
|
+
child.once("exit", (code) => finish(code ?? 1));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Runs the worker and owns update activation, health checking, and rollback. */
|
|
73
|
+
async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000) {
|
|
74
|
+
let currentBin = initialBinPath;
|
|
75
|
+
let rollbackBin = null;
|
|
76
|
+
let candidatePending = null;
|
|
77
|
+
let awaitingCandidateHealth = false;
|
|
78
|
+
let currentChild = null;
|
|
79
|
+
let stopping = false;
|
|
80
|
+
const forwardSignal = (signal) => {
|
|
81
|
+
stopping = true;
|
|
82
|
+
currentChild?.kill(signal);
|
|
83
|
+
};
|
|
84
|
+
const onSigint = () => forwardSignal("SIGINT");
|
|
85
|
+
const onSigterm = () => forwardSignal("SIGTERM");
|
|
86
|
+
process.on("SIGINT", onSigint);
|
|
87
|
+
process.on("SIGTERM", onSigterm);
|
|
88
|
+
try {
|
|
89
|
+
const active = await (0, update_installer_cjs_1.readActiveRuntime)(configDir);
|
|
90
|
+
if (active) {
|
|
91
|
+
try {
|
|
92
|
+
const verified = await (0, update_installer_cjs_1.verifyManagedRuntime)(configDir, active);
|
|
93
|
+
currentBin = verified.bin_path;
|
|
94
|
+
log.info(`starting active managed runtime ${active.version}`);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
log.warn(`ignoring invalid active runtime pointer: ${String(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
while (!stopping) {
|
|
101
|
+
const candidateThisRun = awaitingCandidateHealth;
|
|
102
|
+
const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, (child) => {
|
|
103
|
+
currentChild = child;
|
|
104
|
+
}, async () => {
|
|
105
|
+
if (candidateThisRun) {
|
|
106
|
+
if (!candidatePending)
|
|
107
|
+
throw new Error("candidate has no pending release metadata");
|
|
108
|
+
await (0, update_installer_cjs_1.writeActiveRuntime)(configDir, candidatePending);
|
|
109
|
+
await (0, update_installer_cjs_1.clearPendingUpdate)(configDir);
|
|
110
|
+
awaitingCandidateHealth = false;
|
|
111
|
+
rollbackBin = null;
|
|
112
|
+
candidatePending = null;
|
|
113
|
+
log.info("updated runtime reported healthy; activation committed");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
if (stopping)
|
|
117
|
+
return result.code;
|
|
118
|
+
if (candidateThisRun && !result.healthy) {
|
|
119
|
+
if (!rollbackBin)
|
|
120
|
+
return 1;
|
|
121
|
+
log.warn("updated runtime failed before becoming healthy; restoring previous version");
|
|
122
|
+
currentBin = rollbackBin;
|
|
123
|
+
rollbackBin = null;
|
|
124
|
+
awaitingCandidateHealth = false;
|
|
125
|
+
candidatePending = null;
|
|
126
|
+
await (0, update_installer_cjs_1.clearPendingUpdate)(configDir);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (result.code !== UPDATE_RESTART_EXIT_CODE)
|
|
130
|
+
return result.code;
|
|
131
|
+
const pending = await (0, update_installer_cjs_1.readPendingUpdate)(configDir);
|
|
132
|
+
if (!pending) {
|
|
133
|
+
log.error("worker requested an update restart without a pending update");
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const verified = await (0, update_installer_cjs_1.verifyManagedRuntime)(configDir, pending);
|
|
137
|
+
rollbackBin = currentBin;
|
|
138
|
+
currentBin = verified.bin_path;
|
|
139
|
+
candidatePending = pending;
|
|
140
|
+
awaitingCandidateHealth = true;
|
|
141
|
+
log.info(`activating staged runtime ${pending.version}`);
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
process.off("SIGINT", onSigint);
|
|
147
|
+
process.off("SIGTERM", onSigterm);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RuntimeUpdateCoordinator = void 0;
|
|
4
|
+
const logger_cjs_1 = require("./logger.cjs");
|
|
5
|
+
const update_installer_cjs_1 = require("./update-installer.cjs");
|
|
6
|
+
const version_cjs_1 = require("./version.cjs");
|
|
7
|
+
const log = (0, logger_cjs_1.createLogger)("update");
|
|
8
|
+
class RuntimeUpdateCoordinator {
|
|
9
|
+
options;
|
|
10
|
+
targetInProgress = null;
|
|
11
|
+
warnedUnsupervised = false;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
consider(directive) {
|
|
16
|
+
if (!directive || directive.target_version === version_cjs_1.RUNTIME_VERSION)
|
|
17
|
+
return;
|
|
18
|
+
if (!this.options.config.autoUpdate) {
|
|
19
|
+
if (!this.warnedUnsupervised) {
|
|
20
|
+
log.warn(`runtime ${directive.target_version} is available; run under \`navarch-runtime supervise\` to activate updates automatically`);
|
|
21
|
+
this.warnedUnsupervised = true;
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (this.targetInProgress)
|
|
26
|
+
return;
|
|
27
|
+
this.targetInProgress = directive.target_version;
|
|
28
|
+
void this.apply(directive);
|
|
29
|
+
}
|
|
30
|
+
async apply(directive) {
|
|
31
|
+
const { config, heartbeat, claimLoop, capacity } = this.options;
|
|
32
|
+
try {
|
|
33
|
+
heartbeat.setUpdateState("staging");
|
|
34
|
+
log.info(`staging runtime ${directive.target_version}`);
|
|
35
|
+
const staged = await (this.options.stage ?? update_installer_cjs_1.stageRuntimeUpdate)(config.configDir, directive);
|
|
36
|
+
heartbeat.setUpdateState("draining");
|
|
37
|
+
log.info("runtime update staged; draining active sessions before restart");
|
|
38
|
+
await claimLoop.drain();
|
|
39
|
+
await capacity.waitForIdle();
|
|
40
|
+
await (0, update_installer_cjs_1.writePendingUpdate)(config.configDir, staged, directive.rollout_id);
|
|
41
|
+
log.info(`runtime ${directive.target_version} ready; handing off to supervisor`);
|
|
42
|
+
(this.options.requestRestart ?? ((code) => process.exit(code)))(75);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
log.error(`runtime update failed: ${message}`);
|
|
47
|
+
heartbeat.setUpdateState("failed", message.slice(0, 500));
|
|
48
|
+
claimLoop.start();
|
|
49
|
+
this.targetInProgress = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.RuntimeUpdateCoordinator = RuntimeUpdateCoordinator;
|