@sagentlab/navarch-runtime 0.1.35 → 0.1.37
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 +15 -0
- package/dist/cli.cjs +6 -1
- package/dist/config.cjs +1 -0
- package/dist/heartbeat-loop.cjs +3 -0
- package/dist/supervisor.cjs +6 -0
- package/dist/update-coordinator.cjs +31 -1
- package/dist/usage-limits.cjs +12 -5
- package/dist/worktree-guard.cjs +29 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,15 @@ accepted. Rollouts are assigned by the machine's server-managed `stable` or
|
|
|
233
233
|
`canary` channel and deterministic rollout percentage. Set
|
|
234
234
|
`NAVARCH_AUTO_UPDATE=off` to keep telemetry but disable automatic activation.
|
|
235
235
|
|
|
236
|
+
Project owners can also request a same-version restart from the project Agents
|
|
237
|
+
page. Only a worker running under `navarch-runtime supervise` advertises this
|
|
238
|
+
capability. The restart command arrives on the normal machine heartbeat; the
|
|
239
|
+
worker stops claiming, reports zero capacity, drains active sessions, and exits
|
|
240
|
+
with a restart-only handoff code. The supervisor respawns the current verified
|
|
241
|
+
binary without staging an update. The control plane acknowledges the one-shot
|
|
242
|
+
command only after a heartbeat reports a different boot ID, preventing a stale
|
|
243
|
+
command from causing a restart loop.
|
|
244
|
+
|
|
236
245
|
Plain `navarch-runtime start` continues to work and reports available releases,
|
|
237
246
|
but deliberately does not activate them because no parent process would exist
|
|
238
247
|
to perform a safe handoff or rollback.
|
|
@@ -352,6 +361,12 @@ as an explicit read-only mount.
|
|
|
352
361
|
user or checked-in legacy `sandbox_mode` from silently disabling the
|
|
353
362
|
generated profile; Codex authentication still comes from `CODEX_HOME`, and
|
|
354
363
|
repository instructions such as `AGENTS.md` still load.
|
|
364
|
+
On macOS the profile also grants read access to `/Library/Developer` and
|
|
365
|
+
`/Applications` (Xcode and CoreSimulator, which `xcrun simctl` and
|
|
366
|
+
`xcodebuild` load) and write access to `~/Library/Developer` (simulator
|
|
367
|
+
devices and DerivedData); the Claude hook gets the same write root. Without
|
|
368
|
+
these, iOS builds and simulator tests fail under the sandbox with
|
|
369
|
+
`access to /Library/Developer/PrivateFrameworks/CoreSimulator.framework is blocked`.
|
|
355
370
|
- **Gemini:** the runtime uses Gemini CLI's native sandbox with only the
|
|
356
371
|
current worktree, shared gitdir, session `gh` config, and approved extra
|
|
357
372
|
roots mounted. The `gh` config and lease MCP settings are read-only; header
|
package/dist/cli.cjs
CHANGED
|
@@ -207,7 +207,12 @@ async function startCommand(flags) {
|
|
|
207
207
|
const bootId = (0, node_crypto_1.randomUUID)();
|
|
208
208
|
const updateCoordinatorRef = {};
|
|
209
209
|
let readySent = false;
|
|
210
|
-
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity, bootId, (result) =>
|
|
210
|
+
const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity, bootId, (result) => {
|
|
211
|
+
if (result.restart)
|
|
212
|
+
updateCoordinatorRef.current?.considerRestart(result.restart);
|
|
213
|
+
else
|
|
214
|
+
updateCoordinatorRef.current?.consider(result.update);
|
|
215
|
+
}, () => {
|
|
211
216
|
if (!readySent && process.send) {
|
|
212
217
|
readySent = true;
|
|
213
218
|
process.send({ type: "navarch-ready", boot_id: bootId });
|
package/dist/config.cjs
CHANGED
|
@@ -103,6 +103,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
103
103
|
.map((s) => s.trim())
|
|
104
104
|
.filter(Boolean),
|
|
105
105
|
updateChannel: env.NAVARCH_UPDATE_CHANNEL === "canary" ? "canary" : "stable",
|
|
106
|
+
supervised: env.NAVARCH_SUPERVISED === "1",
|
|
106
107
|
autoUpdate: env.NAVARCH_SUPERVISED === "1" &&
|
|
107
108
|
!["off", "false", "0"].includes(env.NAVARCH_AUTO_UPDATE ?? ""),
|
|
108
109
|
// Reads only the quota numbers providers already write to disk, so it is on
|
package/dist/heartbeat-loop.cjs
CHANGED
|
@@ -70,6 +70,8 @@ class MachineHeartbeatLoop {
|
|
|
70
70
|
? await (0, usage_limits_cjs_1.collectAgentUsage)({
|
|
71
71
|
codexHome: this.config.codexHome,
|
|
72
72
|
claudeConfigPath: this.config.claudeConfigPath,
|
|
73
|
+
providers: [...new Set([this.config.agentType, ...(this.config.runtimes ?? [])])]
|
|
74
|
+
.filter((runtime) => runtime === "codex" || runtime === "claude-code"),
|
|
73
75
|
})
|
|
74
76
|
: null;
|
|
75
77
|
const result = await this.api.machineHeartbeat(this.machineId, {
|
|
@@ -82,6 +84,7 @@ class MachineHeartbeatLoop {
|
|
|
82
84
|
channel: this.config.updateChannel,
|
|
83
85
|
state: this.updateState,
|
|
84
86
|
boot_id: this.bootId,
|
|
87
|
+
supervised: this.config.supervised,
|
|
85
88
|
...(this.lastUpdateError ? { last_update_error: this.lastUpdateError } : {}),
|
|
86
89
|
},
|
|
87
90
|
});
|
package/dist/supervisor.cjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REMOTE_RESTART_EXIT_CODE = void 0;
|
|
3
4
|
exports.superviseRuntime = superviseRuntime;
|
|
4
5
|
const node_child_process_1 = require("node:child_process");
|
|
5
6
|
const logger_cjs_1 = require("./logger.cjs");
|
|
6
7
|
const update_installer_cjs_1 = require("./update-installer.cjs");
|
|
7
8
|
const log = (0, logger_cjs_1.createLogger)("supervisor");
|
|
8
9
|
const UPDATE_RESTART_EXIT_CODE = 75;
|
|
10
|
+
exports.REMOTE_RESTART_EXIT_CODE = 76;
|
|
9
11
|
function runWorker(binPath, args, healthTimeoutMs, workerEnv, setCurrentChild, onHealthy) {
|
|
10
12
|
return new Promise((resolve) => {
|
|
11
13
|
const child = (0, node_child_process_1.spawn)(process.execPath, [binPath, "start", ...args], {
|
|
@@ -126,6 +128,10 @@ async function superviseRuntime(configDir, initialBinPath, workerArgs = [], heal
|
|
|
126
128
|
await (0, update_installer_cjs_1.clearPendingUpdate)(configDir);
|
|
127
129
|
continue;
|
|
128
130
|
}
|
|
131
|
+
if (result.code === exports.REMOTE_RESTART_EXIT_CODE) {
|
|
132
|
+
log.info("remote restart requested; respawning current runtime");
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
129
135
|
if (result.code !== UPDATE_RESTART_EXIT_CODE)
|
|
130
136
|
return result.code;
|
|
131
137
|
const pending = await (0, update_installer_cjs_1.readPendingUpdate)(configDir);
|
|
@@ -4,16 +4,18 @@ exports.RuntimeUpdateCoordinator = void 0;
|
|
|
4
4
|
const logger_cjs_1 = require("./logger.cjs");
|
|
5
5
|
const update_installer_cjs_1 = require("./update-installer.cjs");
|
|
6
6
|
const version_cjs_1 = require("./version.cjs");
|
|
7
|
+
const supervisor_cjs_1 = require("./supervisor.cjs");
|
|
7
8
|
const log = (0, logger_cjs_1.createLogger)("update");
|
|
8
9
|
class RuntimeUpdateCoordinator {
|
|
9
10
|
options;
|
|
10
11
|
targetInProgress = null;
|
|
12
|
+
restartInProgress = null;
|
|
11
13
|
warnedUnsupervised = false;
|
|
12
14
|
constructor(options) {
|
|
13
15
|
this.options = options;
|
|
14
16
|
}
|
|
15
17
|
consider(directive) {
|
|
16
|
-
if (!directive || directive.target_version === version_cjs_1.RUNTIME_VERSION)
|
|
18
|
+
if (!directive || directive.target_version === version_cjs_1.RUNTIME_VERSION || this.restartInProgress)
|
|
17
19
|
return;
|
|
18
20
|
if (!this.options.config.autoUpdate) {
|
|
19
21
|
if (!this.warnedUnsupervised) {
|
|
@@ -27,6 +29,34 @@ class RuntimeUpdateCoordinator {
|
|
|
27
29
|
this.targetInProgress = directive.target_version;
|
|
28
30
|
void this.apply(directive);
|
|
29
31
|
}
|
|
32
|
+
considerRestart(directive) {
|
|
33
|
+
if (!directive || this.targetInProgress || this.restartInProgress)
|
|
34
|
+
return;
|
|
35
|
+
if (!this.options.config.supervised) {
|
|
36
|
+
log.warn(`ignoring remote restart ${directive.command_id}: runtime is not supervised`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.restartInProgress = directive.command_id;
|
|
40
|
+
void this.applyRestart(directive);
|
|
41
|
+
}
|
|
42
|
+
async applyRestart(directive) {
|
|
43
|
+
const { heartbeat, claimLoop, capacity } = this.options;
|
|
44
|
+
try {
|
|
45
|
+
heartbeat.setUpdateState("draining");
|
|
46
|
+
log.info(`remote restart ${directive.command_id} received; draining active sessions`);
|
|
47
|
+
await claimLoop.drain();
|
|
48
|
+
await capacity.waitForIdle();
|
|
49
|
+
log.info(`remote restart ${directive.command_id} ready; handing off to supervisor`);
|
|
50
|
+
(this.options.requestRestart ?? ((code) => process.exit(code)))(supervisor_cjs_1.REMOTE_RESTART_EXIT_CODE);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
log.error(`remote restart failed: ${message}`);
|
|
55
|
+
heartbeat.setUpdateState("failed", message.slice(0, 500));
|
|
56
|
+
claimLoop.start();
|
|
57
|
+
this.restartInProgress = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
30
60
|
async apply(directive) {
|
|
31
61
|
const { config, heartbeat, claimLoop, capacity } = this.options;
|
|
32
62
|
try {
|
package/dist/usage-limits.cjs
CHANGED
|
@@ -359,7 +359,9 @@ async function collectClaudeUsage(options) {
|
|
|
359
359
|
}
|
|
360
360
|
const DEFAULT_COLLECT_TIMEOUT_MS = 5_000;
|
|
361
361
|
/**
|
|
362
|
-
* Gathers
|
|
362
|
+
* Gathers reports for the providers this runtime actually advertises. Provider
|
|
363
|
+
* CLIs can share a host, so scanning every state directory would attribute a
|
|
364
|
+
* different agent's quota to this machine. Selected providers are still read
|
|
363
365
|
* independently so one unreadable or malformed source cannot suppress another,
|
|
364
366
|
* and any thrown error degrades to "no report" rather than failing the caller.
|
|
365
367
|
*/
|
|
@@ -372,10 +374,15 @@ async function collectAgentUsage(options) {
|
|
|
372
374
|
timer.unref?.();
|
|
373
375
|
});
|
|
374
376
|
try {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
const providers = new Set(options.providers ?? ["codex", "claude-code"]);
|
|
378
|
+
const collectors = [];
|
|
379
|
+
if (providers.has("codex")) {
|
|
380
|
+
collectors.push(collectCodexUsage({ codexHome: options.codexHome, now: options.now }));
|
|
381
|
+
}
|
|
382
|
+
if (providers.has("claude-code")) {
|
|
383
|
+
collectors.push(collectClaudeUsage({ configPath: options.claudeConfigPath, now: options.now }));
|
|
384
|
+
}
|
|
385
|
+
const collection = Promise.allSettled(collectors);
|
|
379
386
|
const settled = await Promise.race([collection, deadline]);
|
|
380
387
|
if (!settled) {
|
|
381
388
|
log.warn(`usage collection timed out after ${timeoutMs}ms`);
|
package/dist/worktree-guard.cjs
CHANGED
|
@@ -7,6 +7,7 @@ exports.guardHookScriptPath = guardHookScriptPath;
|
|
|
7
7
|
exports.readClaudeApiKeyHelper = readClaudeApiKeyHelper;
|
|
8
8
|
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
9
9
|
exports.codexToolReadRoots = codexToolReadRoots;
|
|
10
|
+
exports.macosDeveloperWriteRoots = macosDeveloperWriteRoots;
|
|
10
11
|
exports.sessionGhConfigDir = sessionGhConfigDir;
|
|
11
12
|
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
12
13
|
exports.geminiSandboxMounts = geminiSandboxMounts;
|
|
@@ -74,9 +75,12 @@ async function prepareWorktreeGuard(options) {
|
|
|
74
75
|
const hookScriptPath = guardHookScriptPath();
|
|
75
76
|
const configPath = node_path_1.default.join(options.workDir, "worktree-guard.json");
|
|
76
77
|
const settingsPath = node_path_1.default.join(options.workDir, "claude-settings.json");
|
|
78
|
+
const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot);
|
|
77
79
|
const allowedRoots = [
|
|
78
80
|
options.worktreePath,
|
|
79
81
|
options.repositoryPath,
|
|
82
|
+
// Xcode's per-user simulator/DerivedData state; see macosDeveloperWriteRoots.
|
|
83
|
+
...macosDeveloperWriteRoots().filter((root) => !isPathInside(node_path_1.default.resolve(root), workspaceRoot)),
|
|
80
84
|
...(options.extraRoots ?? []),
|
|
81
85
|
];
|
|
82
86
|
const deniedRoots = [options.workspaceRoot];
|
|
@@ -110,7 +114,7 @@ async function prepareWorktreeGuard(options) {
|
|
|
110
114
|
* installations are sufficient for helpers such as apply_patch, npm, gh, and
|
|
111
115
|
* browser evidence tooling.
|
|
112
116
|
*/
|
|
113
|
-
function codexToolReadRoots(env = process.env) {
|
|
117
|
+
function codexToolReadRoots(env = process.env, platform = process.platform) {
|
|
114
118
|
const homeDir = env.HOME?.trim() || node_os_1.default.homedir();
|
|
115
119
|
const codexHome = env.CODEX_HOME?.trim() || node_path_1.default.join(homeDir, ".codex");
|
|
116
120
|
return [
|
|
@@ -126,8 +130,31 @@ function codexToolReadRoots(env = process.env) {
|
|
|
126
130
|
"/System/Library/CoreServices/SystemAppearance.bundle",
|
|
127
131
|
node_path_1.default.join(homeDir, "Library", "Caches", "ms-playwright"),
|
|
128
132
|
node_path_1.default.join(homeDir, ".cache", "ms-playwright"),
|
|
133
|
+
...(platform === "darwin" ? MACOS_TOOLCHAIN_READ_ROOTS : []),
|
|
129
134
|
];
|
|
130
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* World-readable macOS toolchain locations. `xcrun simctl` / `xcodebuild`
|
|
138
|
+
* load /Library/Developer/PrivateFrameworks/CoreSimulator.framework and the
|
|
139
|
+
* Xcode bundle itself; without these an iOS build or simulator test dies
|
|
140
|
+
* under Codex's seatbelt with "access to ... is blocked". Both are already
|
|
141
|
+
* system prefixes for the Claude hook (bin/worktree-guard-hook.cjs), so this
|
|
142
|
+
* only brings the Codex profile level with it.
|
|
143
|
+
*/
|
|
144
|
+
const MACOS_TOOLCHAIN_READ_ROOTS = ["/Library/Developer", "/Applications"];
|
|
145
|
+
/**
|
|
146
|
+
* Per-user macOS developer state that Xcode writes during a build or test
|
|
147
|
+
* run: simulator devices under `~/Library/Developer/CoreSimulator` and
|
|
148
|
+
* DerivedData under `~/Library/Developer/Xcode`. Shared between sessions on
|
|
149
|
+
* one machine (that is inherent to Xcode), but nothing under it is Navarch
|
|
150
|
+
* session state or credentials. Empty off macOS.
|
|
151
|
+
*/
|
|
152
|
+
function macosDeveloperWriteRoots(env = process.env, platform = process.platform) {
|
|
153
|
+
if (platform !== "darwin")
|
|
154
|
+
return [];
|
|
155
|
+
const homeDir = env.HOME?.trim() || node_os_1.default.homedir();
|
|
156
|
+
return [node_path_1.default.join(homeDir, "Library", "Developer")];
|
|
157
|
+
}
|
|
131
158
|
/**
|
|
132
159
|
* Isolated gh configuration root for one coding-agent session.
|
|
133
160
|
*
|
|
@@ -169,7 +196,7 @@ function codexWorktreeGuardArgs(options) {
|
|
|
169
196
|
continue;
|
|
170
197
|
filesystem[resolved] = "read";
|
|
171
198
|
}
|
|
172
|
-
for (const root of options.extraRoots ?? []) {
|
|
199
|
+
for (const root of [...macosDeveloperWriteRoots(), ...(options.extraRoots ?? [])]) {
|
|
173
200
|
const resolved = node_path_1.default.resolve(root);
|
|
174
201
|
// Match the Claude hook's denied-root precedence: an extra root cannot
|
|
175
202
|
// reopen sibling sessions or metadata inside the Navarch workspace.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, Gemini, or OpenCode.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|