@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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validateUpdateDirective = validateUpdateDirective;
|
|
7
|
+
exports.stageRuntimeUpdate = stageRuntimeUpdate;
|
|
8
|
+
exports.writePendingUpdate = writePendingUpdate;
|
|
9
|
+
exports.readPendingUpdate = readPendingUpdate;
|
|
10
|
+
exports.clearPendingUpdate = clearPendingUpdate;
|
|
11
|
+
exports.writeActiveRuntime = writeActiveRuntime;
|
|
12
|
+
exports.readActiveRuntime = readActiveRuntime;
|
|
13
|
+
exports.assertSafePendingUpdate = assertSafePendingUpdate;
|
|
14
|
+
exports.verifyManagedRuntime = verifyManagedRuntime;
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const node_util_1 = require("node:util");
|
|
20
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
21
|
+
const PACKAGE_NAME = "@sagentlab/navarch-runtime";
|
|
22
|
+
const EXACT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
|
|
23
|
+
const SHA512_INTEGRITY = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
|
|
24
|
+
const defaultRunner = async (file, args) => {
|
|
25
|
+
const result = await execFileAsync(file, args, { maxBuffer: 10 * 1024 * 1024 });
|
|
26
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
27
|
+
};
|
|
28
|
+
function validateUpdateDirective(directive) {
|
|
29
|
+
if (!EXACT_SEMVER.test(directive.target_version)) {
|
|
30
|
+
throw new Error(`Invalid runtime target version: ${directive.target_version}`);
|
|
31
|
+
}
|
|
32
|
+
if (!SHA512_INTEGRITY.test(directive.integrity)) {
|
|
33
|
+
throw new Error("Runtime update is missing a valid SHA-512 integrity value.");
|
|
34
|
+
}
|
|
35
|
+
if (!directive.rollout_id || directive.rollout_id.length > 128) {
|
|
36
|
+
throw new Error("Runtime update has an invalid rollout id.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function verifyInstalledPackage(packageDir, version, integrity) {
|
|
40
|
+
const manifest = JSON.parse(await node_fs_1.promises.readFile(node_path_1.default.join(packageDir, "package.json"), "utf8"));
|
|
41
|
+
if (manifest.name !== PACKAGE_NAME || manifest.version !== version) {
|
|
42
|
+
throw new Error("Staged runtime package identity did not match the requested release.");
|
|
43
|
+
}
|
|
44
|
+
const recordedIntegrity = (await node_fs_1.promises.readFile(node_path_1.default.join(packageDir, ".navarch-integrity"), "utf8")).trim();
|
|
45
|
+
if (recordedIntegrity !== integrity) {
|
|
46
|
+
throw new Error("Staged runtime integrity did not match the release directive.");
|
|
47
|
+
}
|
|
48
|
+
const binPath = node_path_1.default.join(packageDir, "bin", "navarch.cjs");
|
|
49
|
+
await node_fs_1.promises.access(binPath);
|
|
50
|
+
return { version, integrity, bin_path: binPath };
|
|
51
|
+
}
|
|
52
|
+
/** Downloads and verifies an exact immutable npm release without running package scripts. */
|
|
53
|
+
async function stageRuntimeUpdate(configDir, directive, runner = defaultRunner) {
|
|
54
|
+
validateUpdateDirective(directive);
|
|
55
|
+
const versionsDir = node_path_1.default.join(configDir, "versions");
|
|
56
|
+
const targetDir = node_path_1.default.join(versionsDir, directive.target_version);
|
|
57
|
+
await node_fs_1.promises.mkdir(versionsDir, { recursive: true, mode: 0o700 });
|
|
58
|
+
try {
|
|
59
|
+
const staged = await verifyInstalledPackage(targetDir, directive.target_version, directive.integrity);
|
|
60
|
+
await runner(process.execPath, [staged.bin_path, "--help"]);
|
|
61
|
+
return staged;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== "ENOENT")
|
|
65
|
+
throw error;
|
|
66
|
+
await node_fs_1.promises.rm(targetDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
const stagingDir = node_path_1.default.join(versionsDir, `.staging-${(0, node_crypto_1.randomUUID)()}`);
|
|
69
|
+
const installDir = node_path_1.default.join(stagingDir, "install");
|
|
70
|
+
await node_fs_1.promises.mkdir(installDir, { recursive: true, mode: 0o700 });
|
|
71
|
+
try {
|
|
72
|
+
const packed = await runner("npm", [
|
|
73
|
+
"pack",
|
|
74
|
+
`${PACKAGE_NAME}@${directive.target_version}`,
|
|
75
|
+
"--json",
|
|
76
|
+
"--ignore-scripts",
|
|
77
|
+
"--pack-destination",
|
|
78
|
+
stagingDir,
|
|
79
|
+
]);
|
|
80
|
+
const packResult = JSON.parse(packed.stdout);
|
|
81
|
+
const artifact = packResult[0];
|
|
82
|
+
if (!artifact ||
|
|
83
|
+
typeof artifact.filename !== "string" ||
|
|
84
|
+
artifact.integrity !== directive.integrity) {
|
|
85
|
+
throw new Error("Downloaded runtime tarball failed its release integrity check.");
|
|
86
|
+
}
|
|
87
|
+
const tarballPath = node_path_1.default.join(stagingDir, node_path_1.default.basename(artifact.filename));
|
|
88
|
+
await runner("npm", [
|
|
89
|
+
"install",
|
|
90
|
+
"--prefix",
|
|
91
|
+
installDir,
|
|
92
|
+
"--ignore-scripts",
|
|
93
|
+
"--omit=dev",
|
|
94
|
+
"--no-audit",
|
|
95
|
+
"--no-fund",
|
|
96
|
+
"--package-lock=false",
|
|
97
|
+
tarballPath,
|
|
98
|
+
]);
|
|
99
|
+
const installedPackage = node_path_1.default.join(installDir, "node_modules", "@sagentlab", "navarch-runtime");
|
|
100
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(installedPackage, ".navarch-integrity"), `${directive.integrity}\n`, { mode: 0o600 });
|
|
101
|
+
await node_fs_1.promises.rename(installedPackage, targetDir);
|
|
102
|
+
const staged = await verifyInstalledPackage(targetDir, directive.target_version, directive.integrity);
|
|
103
|
+
await runner(process.execPath, [staged.bin_path, "--help"]);
|
|
104
|
+
return staged;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await node_fs_1.promises.rm(stagingDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function pendingPath(configDir) {
|
|
111
|
+
return node_path_1.default.join(configDir, "pending-update.json");
|
|
112
|
+
}
|
|
113
|
+
function activePath(configDir) {
|
|
114
|
+
return node_path_1.default.join(configDir, "active-runtime.json");
|
|
115
|
+
}
|
|
116
|
+
async function writePendingUpdate(configDir, staged, rolloutId) {
|
|
117
|
+
const pending = { ...staged, rollout_id: rolloutId };
|
|
118
|
+
const tempPath = `${pendingPath(configDir)}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
119
|
+
await node_fs_1.promises.writeFile(tempPath, `${JSON.stringify(pending, null, 2)}\n`, { mode: 0o600 });
|
|
120
|
+
await node_fs_1.promises.rename(tempPath, pendingPath(configDir));
|
|
121
|
+
}
|
|
122
|
+
async function readPendingUpdate(configDir) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(await node_fs_1.promises.readFile(pendingPath(configDir), "utf8"));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error.code === "ENOENT")
|
|
128
|
+
return null;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function clearPendingUpdate(configDir) {
|
|
133
|
+
await node_fs_1.promises.rm(pendingPath(configDir), { force: true });
|
|
134
|
+
}
|
|
135
|
+
async function writeActiveRuntime(configDir, pending) {
|
|
136
|
+
const active = { ...pending, activated_at: new Date().toISOString() };
|
|
137
|
+
const tempPath = `${activePath(configDir)}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
138
|
+
await node_fs_1.promises.writeFile(tempPath, `${JSON.stringify(active, null, 2)}\n`, { mode: 0o600 });
|
|
139
|
+
await node_fs_1.promises.rename(tempPath, activePath(configDir));
|
|
140
|
+
}
|
|
141
|
+
async function readActiveRuntime(configDir) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(await node_fs_1.promises.readFile(activePath(configDir), "utf8"));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code === "ENOENT")
|
|
147
|
+
return null;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function assertSafePendingUpdate(configDir, pending) {
|
|
152
|
+
if (!EXACT_SEMVER.test(pending.version))
|
|
153
|
+
throw new Error("Pending update has an invalid version.");
|
|
154
|
+
const expectedBin = node_path_1.default.resolve(configDir, "versions", pending.version, "bin", "navarch.cjs");
|
|
155
|
+
if (node_path_1.default.resolve(pending.bin_path) !== expectedBin) {
|
|
156
|
+
throw new Error("Pending update executable is outside the managed versions directory.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Re-verifies a persisted managed pointer before the supervisor executes it. */
|
|
160
|
+
async function verifyManagedRuntime(configDir, runtime) {
|
|
161
|
+
assertSafePendingUpdate(configDir, runtime);
|
|
162
|
+
const packageDir = node_path_1.default.resolve(configDir, "versions", runtime.version);
|
|
163
|
+
return verifyInstalledPackage(packageDir, runtime.version, runtime.integrity);
|
|
164
|
+
}
|
package/dist/version.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UPDATER_PROTOCOL_VERSION = exports.RUNTIME_VERSION = void 0;
|
|
4
|
+
// package.json is always included in an npm package, even though the runtime's
|
|
5
|
+
// explicit `files` allowlist only names dist/bin/README.
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
7
|
+
const packageJson = require("../package.json");
|
|
8
|
+
exports.RUNTIME_VERSION = typeof packageJson.version === "string" ? packageJson.version : "0.0.0-unknown";
|
|
9
|
+
exports.UPDATER_PROTOCOL_VERSION = 1;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.guardHookScriptPath = guardHookScriptPath;
|
|
7
|
+
exports.prepareWorktreeGuard = prepareWorktreeGuard;
|
|
8
|
+
exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const CODEX_GUARD_PROFILE = "navarch-worktree";
|
|
12
|
+
/**
|
|
13
|
+
* Tools the hook screens. Everything else — the lease-scoped Navarch MCP
|
|
14
|
+
* tools, WebFetch, Task, ... — carries no direct filesystem path and passes
|
|
15
|
+
* through unmatched.
|
|
16
|
+
*/
|
|
17
|
+
const GUARDED_TOOL_MATCHER = "^(Read|Write|Edit|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash)$";
|
|
18
|
+
/**
|
|
19
|
+
* The hook ships as plain CommonJS in bin/ (see its header for why), which
|
|
20
|
+
* sits one level above this module both in the source tree (src/) and in the
|
|
21
|
+
* published package (dist/), so __dirname-relative resolution works in both.
|
|
22
|
+
*/
|
|
23
|
+
function guardHookScriptPath() {
|
|
24
|
+
return node_path_1.default.join(__dirname, "..", "bin", "worktree-guard-hook.cjs");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Writes the per-session guard config + Claude settings file into workDir and
|
|
28
|
+
* returns their paths. The settings file is passed to the CLI as `--settings`
|
|
29
|
+
* by adapters/claude.cts.
|
|
30
|
+
*/
|
|
31
|
+
async function prepareWorktreeGuard(options) {
|
|
32
|
+
const hookScriptPath = guardHookScriptPath();
|
|
33
|
+
const configPath = node_path_1.default.join(options.workDir, "worktree-guard.json");
|
|
34
|
+
const settingsPath = node_path_1.default.join(options.workDir, "claude-settings.json");
|
|
35
|
+
const allowedRoots = [
|
|
36
|
+
options.worktreePath,
|
|
37
|
+
options.repositoryPath,
|
|
38
|
+
...(options.extraRoots ?? []),
|
|
39
|
+
];
|
|
40
|
+
const deniedRoots = [options.workspaceRoot];
|
|
41
|
+
await node_fs_1.promises.writeFile(configPath, JSON.stringify({ allowedRoots, deniedRoots }, null, 2), "utf8");
|
|
42
|
+
// process.execPath rather than a bare `node`: the hook must run with the
|
|
43
|
+
// same interpreter as the runtime regardless of the agent's PATH.
|
|
44
|
+
const command = [process.execPath, hookScriptPath, configPath].map(shellQuote).join(" ");
|
|
45
|
+
const settings = {
|
|
46
|
+
hooks: {
|
|
47
|
+
PreToolUse: [
|
|
48
|
+
{
|
|
49
|
+
matcher: GUARDED_TOOL_MATCHER,
|
|
50
|
+
hooks: [{ type: "command", command }],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
await node_fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
56
|
+
return { settingsPath, configPath, hookScriptPath };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Builds one-off Codex permission-profile arguments for a host session.
|
|
60
|
+
*
|
|
61
|
+
* `--ignore-user-config` is deliberate: a sandbox_mode in any loaded user
|
|
62
|
+
* config makes Codex ignore permission profiles, which would silently discard
|
|
63
|
+
* this boundary. Authentication still comes from CODEX_HOME. The profile
|
|
64
|
+
* grants only the runtime paths common tools need, the session worktree, the
|
|
65
|
+
* shared Git directory, temp space, and operator-approved extra roots. The
|
|
66
|
+
* surrounding Navarch workspace is denied, then the more-specific current
|
|
67
|
+
* worktree/repository grants reopen only this session's paths. The worktree
|
|
68
|
+
* is marked untrusted for configuration purposes so a checked-in legacy
|
|
69
|
+
* sandbox_mode cannot disable the generated permission profile; repository
|
|
70
|
+
* instructions such as AGENTS.md still load normally.
|
|
71
|
+
*/
|
|
72
|
+
function codexWorktreeGuardArgs(options) {
|
|
73
|
+
const filesystem = {
|
|
74
|
+
":minimal": "read",
|
|
75
|
+
":tmpdir": "write",
|
|
76
|
+
":slash_tmp": "write",
|
|
77
|
+
[node_path_1.default.resolve(options.workspaceRoot)]: "deny",
|
|
78
|
+
[node_path_1.default.resolve(options.worktreePath)]: "write",
|
|
79
|
+
[node_path_1.default.resolve(options.repositoryPath)]: "write",
|
|
80
|
+
};
|
|
81
|
+
for (const root of options.extraRoots ?? []) {
|
|
82
|
+
const resolved = node_path_1.default.resolve(root);
|
|
83
|
+
// Match the Claude hook's denied-root precedence: an extra root cannot
|
|
84
|
+
// reopen sibling sessions or metadata inside the Navarch workspace.
|
|
85
|
+
if (isPathInside(resolved, node_path_1.default.resolve(options.workspaceRoot)))
|
|
86
|
+
continue;
|
|
87
|
+
filesystem[resolved] = "write";
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
"--ignore-user-config",
|
|
91
|
+
// Keep approvals interactive at the policy layer, but route them to
|
|
92
|
+
// Codex's automatic reviewer because `codex exec` has no human available.
|
|
93
|
+
"-c",
|
|
94
|
+
'approval_policy="on-request"',
|
|
95
|
+
"-c",
|
|
96
|
+
'approvals_reviewer="auto_review"',
|
|
97
|
+
"-c",
|
|
98
|
+
`projects.${tomlString(node_path_1.default.resolve(options.worktreePath))}.trust_level="untrusted"`,
|
|
99
|
+
"-c",
|
|
100
|
+
`default_permissions=${tomlString(CODEX_GUARD_PROFILE)}`,
|
|
101
|
+
"-c",
|
|
102
|
+
`permissions.${CODEX_GUARD_PROFILE}.filesystem=${tomlInlineTable(filesystem)}`,
|
|
103
|
+
// Navarch coding tasks must be able to fetch dependencies and push their
|
|
104
|
+
// branch. The profile still constrains filesystem access independently.
|
|
105
|
+
"-c",
|
|
106
|
+
`permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
function isPathInside(candidate, root) {
|
|
110
|
+
const relative = node_path_1.default.relative(root, candidate);
|
|
111
|
+
return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
|
|
112
|
+
}
|
|
113
|
+
function tomlInlineTable(table) {
|
|
114
|
+
return `{${Object.entries(table)
|
|
115
|
+
.map(([key, value]) => typeof value === "string"
|
|
116
|
+
? `${tomlString(key)}=${tomlString(value)}`
|
|
117
|
+
: `${tomlString(key)}=${tomlInlineTable(value)}`)
|
|
118
|
+
.join(",")}}`;
|
|
119
|
+
}
|
|
120
|
+
function tomlString(value) {
|
|
121
|
+
return JSON.stringify(value);
|
|
122
|
+
}
|
|
123
|
+
function shellQuote(value) {
|
|
124
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test:watch": "vitest",
|
|
39
39
|
"prepublishOnly": "npm run build && npm test",
|
|
40
40
|
"start": "node bin/navarch.cjs start",
|
|
41
|
+
"supervise": "node bin/navarch.cjs supervise",
|
|
41
42
|
"register": "node bin/navarch.cjs register",
|
|
42
43
|
"connect": "node bin/navarch.cjs connect",
|
|
43
44
|
"doctor": "node bin/navarch.cjs doctor"
|