@sagentlab/navarch-runtime 0.1.4 → 0.1.6
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 +57 -1
- package/bin/worktree-guard-hook.cjs +312 -0
- package/dist/adapters/claude.cjs +46 -0
- package/dist/adapters/codex.cjs +40 -10
- package/dist/config.cjs +7 -0
- package/dist/session.cjs +37 -1
- package/dist/worktree-guard.cjs +121 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,10 +159,60 @@ unchanged across the deployment.
|
|
|
159
159
|
| `NAVARCH_DOCKER_IMAGE` | `node:20-slim` | Image used for the per-session container. |
|
|
160
160
|
| `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code` or `codex`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
|
|
161
161
|
| `NAVARCH_CLAUDE_BIN` | `claude` | Path/name of the Claude Code CLI binary. |
|
|
162
|
-
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). |
|
|
162
|
+
| `NAVARCH_CLAUDE_EXTRA_ARGS` | — | Comma list of extra CLI args appended after `--mcp-config` (Claude Code). Supplying an explicit permission flag such as `--allowedTools` or `--permission-mode` replaces the unattended session's default `--dangerously-skip-permissions`. |
|
|
163
163
|
| `NAVARCH_CODEX_BIN` | `codex` | Path/name of the Codex CLI binary. |
|
|
164
164
|
| `NAVARCH_CODEX_EXTRA_ARGS` | — | Comma list of extra CLI args appended after the generated MCP `-c` overrides and `--json` (Codex). |
|
|
165
165
|
| `NAVARCH_MCP_CONFIG_PATH` | — | Path to the platform MCP config passed as `--mcp-config`. |
|
|
166
|
+
| `NAVARCH_WORKTREE_GUARD` | on | Host-mode Claude and Codex sessions get a per-session worktree boundary guard (see below). Set `off` to disable. |
|
|
167
|
+
| `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. |
|
|
168
|
+
|
|
169
|
+
## Worktree boundary guard (host mode)
|
|
170
|
+
|
|
171
|
+
A machine typically runs several sessions concurrently (`NAVARCH_MAX_SESSIONS`),
|
|
172
|
+
each in its own git worktree. The runtime enforces the same boundary for both
|
|
173
|
+
supported coding agents, using each CLI's native enforcement point:
|
|
174
|
+
|
|
175
|
+
- **Claude Code:** permission bypass skips prompts but not hooks, so the
|
|
176
|
+
runtime generates a Claude settings file (`src/worktree-guard.cts`, passed
|
|
177
|
+
as `--settings`) that installs `bin/worktree-guard-hook.cjs` as a fail-closed
|
|
178
|
+
`PreToolUse` hook.
|
|
179
|
+
- **Codex:** the runtime passes a one-off native permission profile with
|
|
180
|
+
`--ask-for-approval never`. Codex's OS sandbox grants read/write access only
|
|
181
|
+
to the allowed roots and denies the surrounding multi-session workspace.
|
|
182
|
+
`--ignore-user-config` and an untrusted project-config override prevent a
|
|
183
|
+
user or checked-in legacy `sandbox_mode` from silently disabling the
|
|
184
|
+
generated profile; Codex authentication still comes from `CODEX_HOME`, and
|
|
185
|
+
repository instructions such as `AGENTS.md` still load.
|
|
186
|
+
|
|
187
|
+
The resulting boundary is:
|
|
188
|
+
|
|
189
|
+
- **File tools** (`Read`/`Write`/`Edit`/`Glob`/`Grep`/...) may only touch the
|
|
190
|
+
session worktree, the project's shared bare repo, temp dirs, and any
|
|
191
|
+
`NAVARCH_GUARD_EXTRA_ROOTS`. Read-only tools may additionally read standard
|
|
192
|
+
system prefixes (`/usr`, `/etc`, ...). Symlinks are resolved before the
|
|
193
|
+
containment check.
|
|
194
|
+
- **Bash commands** are screened lexically: absolute, `~`/`$HOME`, and
|
|
195
|
+
`..`-traversal path references must land inside the allowed roots or the
|
|
196
|
+
system prefixes.
|
|
197
|
+
- The **rest of the workspace root** — sibling sessions' worktrees, other
|
|
198
|
+
projects' bare repos, and the session's own metadata dir (lease-scoped MCP
|
|
199
|
+
config, the guard files themselves) — is denied outright, so an agent can
|
|
200
|
+
neither read another agent's checkout nor rewrite its own guard policy.
|
|
201
|
+
|
|
202
|
+
The Claude hook is a strong guardrail rather than a hard security boundary
|
|
203
|
+
because shell paths are screened lexically. Codex's permission profile is
|
|
204
|
+
enforced by its OS sandbox. For container-grade whole-process isolation use
|
|
205
|
+
`NAVARCH_SANDBOX_MODE=docker`; neither host guard is installed in Docker mode.
|
|
206
|
+
An operator-supplied `--settings` in `NAVARCH_CLAUDE_EXTRA_ARGS`, or an
|
|
207
|
+
explicit Codex permission/sandbox option in `NAVARCH_CODEX_EXTRA_ARGS`, takes
|
|
208
|
+
precedence over the generated policy.
|
|
209
|
+
|
|
210
|
+
The full mechanism was verified live on 2026-07-19 against a real
|
|
211
|
+
`claude -p --dangerously-skip-permissions --settings <generated file>` run:
|
|
212
|
+
the `PreToolUse` hook fires under permission bypass, an in-worktree Read and
|
|
213
|
+
`git --version` succeed, and a cross-session Read and a Bash redirect into a
|
|
214
|
+
sibling session are both blocked with the guard's message (and nothing lands
|
|
215
|
+
on disk).
|
|
166
216
|
|
|
167
217
|
## Choosing an agent (Claude Code vs. Codex)
|
|
168
218
|
|
|
@@ -298,6 +348,12 @@ tests cover:
|
|
|
298
348
|
- `adapters/codex.cts` — arg construction on both the host path (mocked `spawn`)
|
|
299
349
|
and the docker-exec path (fake `CommandRunner`), and usage/report-text
|
|
300
350
|
attachment from fixed JSONL fixtures (`tests/adapters/codex.test.cts`).
|
|
351
|
+
- `worktree-guard.cts` + `bin/worktree-guard-hook.cjs` — generated settings/
|
|
352
|
+
config shape, native Codex permission-profile construction, and the hook's
|
|
353
|
+
containment verdicts (in-worktree vs. sibling session vs. home dir, symlink
|
|
354
|
+
escapes, Bash path screening, the fail-closed exit-2 protocol run as a real
|
|
355
|
+
subprocess) (`tests/worktree-guard.test.cts`,
|
|
356
|
+
`tests/worktree-guard-hook.test.cts`).
|
|
301
357
|
|
|
302
358
|
Not exercised by unit tests, and needing a real machine per the WP-07 DoD
|
|
303
359
|
("on a real machine: register → claim a seeded docs task → session runs
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Navarch worktree guard — the PreToolUse hook installed by
|
|
6
|
+
* src/worktree-guard.cts into every host-mode Claude Code session.
|
|
7
|
+
*
|
|
8
|
+
* Multiple sessions run concurrently on one machine (claim-loop.cts), each
|
|
9
|
+
* confined by convention to its own git worktree
|
|
10
|
+
* (`<workspaceRoot>/sessions/<sessionId>/repo` — git-worktree.cts). The
|
|
11
|
+
* adapter launches Claude with --dangerously-skip-permissions (adapters/
|
|
12
|
+
* claude.cts), which bypasses permission prompts but NOT hooks: Claude Code
|
|
13
|
+
* runs PreToolUse hooks in every permission mode, so this script is the
|
|
14
|
+
* enforcement point that keeps an agent from reading or writing another
|
|
15
|
+
* session's worktree, the operator's home directory, or anything else
|
|
16
|
+
* unrelated to its task.
|
|
17
|
+
*
|
|
18
|
+
* Protocol (Claude Code hooks): the hook receives {tool_name, tool_input,
|
|
19
|
+
* cwd, ...} as JSON on stdin. Exit 0 allows the tool call; exit 2 blocks it
|
|
20
|
+
* and feeds stderr back to the model as the reason. Any internal failure
|
|
21
|
+
* exits 2 as well — a safety harness must fail closed.
|
|
22
|
+
*
|
|
23
|
+
* Policy:
|
|
24
|
+
* - File tools (Read/Write/Edit/...) may only touch the allowed roots (the
|
|
25
|
+
* session worktree, the project's shared bare repo, temp dirs, plus any
|
|
26
|
+
* NAVARCH_GUARD_EXTRA_ROOTS). Read-only tools may additionally read
|
|
27
|
+
* standard system prefixes (/usr, /etc, ...) so toolchains keep working.
|
|
28
|
+
* - Bash commands are screened lexically: absolute, `~`/$HOME, and
|
|
29
|
+
* `..`-traversal path references must land inside the allowed roots or
|
|
30
|
+
* the system prefixes. This cannot catch every obfuscated escape (shell
|
|
31
|
+
* is Turing-complete) — it is a strong guardrail, not a security
|
|
32
|
+
* boundary. Operators needing a hard boundary should use
|
|
33
|
+
* NAVARCH_SANDBOX_MODE=docker (sandbox.cts).
|
|
34
|
+
* - Symlinks are resolved (realpath of the longest existing prefix) before
|
|
35
|
+
* the containment check, so `ln -s $HOME escape` doesn't work either.
|
|
36
|
+
*
|
|
37
|
+
* This file is plain CommonJS (not .cts) on purpose: it must be runnable by
|
|
38
|
+
* a bare `node` from both the published package (dist has compiled .cjs
|
|
39
|
+
* next to bin/) and the source tree (vitest imports it directly), without
|
|
40
|
+
* depending on the TypeScript build. Node stdlib only.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require("node:fs");
|
|
44
|
+
const os = require("node:os");
|
|
45
|
+
const path = require("node:path");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prefixes shell commands may reference freely (and read-only file tools may
|
|
49
|
+
* read): OS/toolchain locations plus shared temp. Everything here is either
|
|
50
|
+
* world-readable system state or scratch space that no Navarch session owns.
|
|
51
|
+
* Deliberately absent: /Users, /home, /root, /var (beyond tmp), and the
|
|
52
|
+
* workspace root — those are exactly what the guard exists to protect.
|
|
53
|
+
*/
|
|
54
|
+
const DEFAULT_SYSTEM_PREFIXES = [
|
|
55
|
+
"/bin",
|
|
56
|
+
"/sbin",
|
|
57
|
+
"/usr",
|
|
58
|
+
"/lib",
|
|
59
|
+
"/lib32",
|
|
60
|
+
"/lib64",
|
|
61
|
+
"/libx32",
|
|
62
|
+
"/opt",
|
|
63
|
+
"/etc",
|
|
64
|
+
"/private/etc",
|
|
65
|
+
"/dev",
|
|
66
|
+
"/proc",
|
|
67
|
+
"/sys",
|
|
68
|
+
"/run",
|
|
69
|
+
// Deliberately NOT the whole /System: /System/Volumes/Data firmlinks the
|
|
70
|
+
// entire macOS data volume (including /Users), which would be an escape.
|
|
71
|
+
"/System/Library",
|
|
72
|
+
"/System/Applications",
|
|
73
|
+
"/Applications",
|
|
74
|
+
"/Library/Developer",
|
|
75
|
+
"/tmp",
|
|
76
|
+
"/private/tmp",
|
|
77
|
+
"/var/tmp",
|
|
78
|
+
"/var/folders",
|
|
79
|
+
"/private/var/folders",
|
|
80
|
+
"/nix",
|
|
81
|
+
"/snap",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/** Path-carrying input fields per guarded file tool (Claude Code tool schemas). */
|
|
85
|
+
const FILE_TOOL_PATH_FIELDS = {
|
|
86
|
+
Read: ["file_path"],
|
|
87
|
+
Write: ["file_path"],
|
|
88
|
+
Edit: ["file_path"],
|
|
89
|
+
MultiEdit: ["file_path"],
|
|
90
|
+
NotebookEdit: ["notebook_path"],
|
|
91
|
+
Glob: ["path"],
|
|
92
|
+
Grep: ["path"],
|
|
93
|
+
LS: ["path"],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** File tools that only read — these may also touch the system prefixes. */
|
|
97
|
+
const READ_ONLY_FILE_TOOLS = new Set(["Read", "Glob", "Grep", "LS"]);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Loads the per-session guard config written by src/worktree-guard.cts:
|
|
101
|
+
* `{ allowedRoots: string[], deniedRoots?: string[], systemPrefixes?:
|
|
102
|
+
* string[] }`. Temp roots are appended here (not persisted) so the config
|
|
103
|
+
* stays portable across OSes.
|
|
104
|
+
*
|
|
105
|
+
* Precedence (checkPath below): allowedRoots (this session's own dirs) win,
|
|
106
|
+
* then deniedRoots (the whole multi-session workspace root — so sibling
|
|
107
|
+
* sessions and other projects' bare repos stay off-limits even when the
|
|
108
|
+
* workspace happens to live under a temp or system prefix), then the broad
|
|
109
|
+
* temp/system allowances.
|
|
110
|
+
*/
|
|
111
|
+
function loadGuardConfig(configPath) {
|
|
112
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
113
|
+
if (!raw || !Array.isArray(raw.allowedRoots) || raw.allowedRoots.length === 0) {
|
|
114
|
+
throw new Error(`guard config ${configPath} does not declare allowedRoots`);
|
|
115
|
+
}
|
|
116
|
+
const canonicalize = (root) => resolveWithRealpath(path.resolve(root));
|
|
117
|
+
return {
|
|
118
|
+
allowedRoots: [...new Set(raw.allowedRoots.map(canonicalize))],
|
|
119
|
+
deniedRoots: [...new Set((raw.deniedRoots ?? []).map(canonicalize))],
|
|
120
|
+
tempRoots: [...new Set([os.tmpdir(), "/tmp", "/var/tmp"].map(canonicalize))],
|
|
121
|
+
systemPrefixes:
|
|
122
|
+
Array.isArray(raw.systemPrefixes) && raw.systemPrefixes.length > 0
|
|
123
|
+
? raw.systemPrefixes
|
|
124
|
+
: DEFAULT_SYSTEM_PREFIXES,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Containment verdict for one canonicalized absolute path.
|
|
130
|
+
* `allowSystemPrefixes` is true for read-only file tools and for Bash (whose
|
|
131
|
+
* commands must keep reaching toolchains under /usr, /etc, ...).
|
|
132
|
+
*/
|
|
133
|
+
function checkPath(resolved, config, allowSystemPrefixes) {
|
|
134
|
+
if (config.allowedRoots.some((root) => isWithin(root, resolved))) return true;
|
|
135
|
+
if (config.deniedRoots.some((root) => isWithin(root, resolved))) return false;
|
|
136
|
+
if (config.tempRoots.some((root) => isWithin(root, resolved))) return true;
|
|
137
|
+
if (allowSystemPrefixes && config.systemPrefixes.some((root) => isWithin(root, resolved))) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Canonicalizes an absolute path: realpath of the longest existing ancestor
|
|
145
|
+
* with the non-existent tail re-appended. Resolving through the existing
|
|
146
|
+
* portion is what defeats symlink escapes; keeping the tail lets the guard
|
|
147
|
+
* judge not-yet-created files (Write) by where they would actually land.
|
|
148
|
+
*/
|
|
149
|
+
function resolveWithRealpath(absolutePath) {
|
|
150
|
+
let current = path.normalize(absolutePath);
|
|
151
|
+
const tail = [];
|
|
152
|
+
for (;;) {
|
|
153
|
+
try {
|
|
154
|
+
const real = fs.realpathSync(current);
|
|
155
|
+
return tail.length > 0 ? path.join(real, ...tail.reverse()) : real;
|
|
156
|
+
} catch {
|
|
157
|
+
const parent = path.dirname(current);
|
|
158
|
+
if (parent === current) return path.normalize(absolutePath);
|
|
159
|
+
tail.push(path.basename(current));
|
|
160
|
+
current = parent;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isWithin(root, candidate) {
|
|
166
|
+
return candidate === root || candidate.startsWith(root + path.sep);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Expands leading `~`, `~/`, `$HOME`, and `${HOME}` to the real home directory. */
|
|
170
|
+
function expandHome(value) {
|
|
171
|
+
if (value === "~") return os.homedir();
|
|
172
|
+
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
173
|
+
if (/^\$\{?HOME\}?($|\/)/.test(value)) return value.replace(/^\$\{?HOME\}?/, os.homedir());
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Pulls path-like references out of a shell command: absolute paths,
|
|
179
|
+
* home-anchored paths (`~...`, `$HOME/...`), and relative `..` traversals.
|
|
180
|
+
* Lexical by design — see the file header for why this is a guardrail, not
|
|
181
|
+
* a parser. URLs survive untouched (the char before `//` in `https://` is
|
|
182
|
+
* `:`, which is not a path delimiter here).
|
|
183
|
+
*/
|
|
184
|
+
function extractBashPathCandidates(command) {
|
|
185
|
+
const candidates = [];
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const add = (raw) => {
|
|
188
|
+
const cleaned = raw.replace(/[)\]}"'`,]+$/, "");
|
|
189
|
+
if (!cleaned || seen.has(cleaned)) return;
|
|
190
|
+
seen.add(cleaned);
|
|
191
|
+
candidates.push(cleaned);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const absoluteRe = /(?:^|[\s'"`=(<>;|&])(\/[^\s'"`<>;|&)]*)/g;
|
|
195
|
+
const homeRe = /(?:^|[\s'"`=(<>;|&])((?:~|\$\{?HOME\}?)[^\s'"`<>;|&)]*)/g;
|
|
196
|
+
let match;
|
|
197
|
+
while ((match = absoluteRe.exec(command))) add(match[1]);
|
|
198
|
+
while ((match = homeRe.exec(command))) add(match[1]);
|
|
199
|
+
|
|
200
|
+
for (const token of command.split(/[\s'"`;|&<>()]+/)) {
|
|
201
|
+
if (!token || token.startsWith("-") || token.startsWith("/") || token.startsWith("~")) continue;
|
|
202
|
+
if (token === ".." || token.startsWith("../") || token.includes("/../") || token.endsWith("/..")) {
|
|
203
|
+
add(token);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return candidates;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function denyMessage(toolName, rawPath, resolvedPath, config) {
|
|
210
|
+
const resolvedNote =
|
|
211
|
+
resolvedPath && resolvedPath !== rawPath ? ` (resolves to "${resolvedPath}")` : "";
|
|
212
|
+
return (
|
|
213
|
+
`Navarch worktree guard blocked this ${toolName} call: "${rawPath}"${resolvedNote} is outside ` +
|
|
214
|
+
`this session's workspace. This machine runs multiple isolated agent sessions; work only under: ` +
|
|
215
|
+
`${config.allowedRoots.join(", ")}. Standard system paths (/usr, /etc, /tmp, ...) stay available ` +
|
|
216
|
+
`to shell commands. Do not attempt to bypass this boundary — if the task genuinely requires that ` +
|
|
217
|
+
`path, state the limitation in your report instead.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Screens one Bash command. Returns {allowed} or {allowed:false, reason}. */
|
|
222
|
+
function evaluateBashCommand(command, cwd, config) {
|
|
223
|
+
// A bare `cd` (or `cd -`) jumps to $HOME / an unknowable previous
|
|
224
|
+
// directory — both outside the worktree by construction.
|
|
225
|
+
for (const segment of command.split(/[;&|]+/)) {
|
|
226
|
+
const trimmed = segment.trim();
|
|
227
|
+
if (trimmed === "cd" || trimmed === "cd -") {
|
|
228
|
+
return { allowed: false, reason: denyMessage("Bash", trimmed, os.homedir(), config) };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const raw of extractBashPathCandidates(command)) {
|
|
233
|
+
// `~otheruser/...` — another account's home; nothing there is in scope.
|
|
234
|
+
if (/^~[^/]/.test(raw)) {
|
|
235
|
+
return { allowed: false, reason: denyMessage("Bash", raw, "", config) };
|
|
236
|
+
}
|
|
237
|
+
const resolved = resolveWithRealpath(path.resolve(cwd, expandHome(raw)));
|
|
238
|
+
if (!checkPath(resolved, config, true)) {
|
|
239
|
+
return { allowed: false, reason: denyMessage("Bash", raw, resolved, config) };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { allowed: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Evaluates one PreToolUse payload. Guarded file tools and Bash are checked;
|
|
247
|
+
* every other tool (MCP platform tools, WebFetch, Task, ...) is allowed —
|
|
248
|
+
* the settings matcher (src/worktree-guard.cts) shouldn't even route them
|
|
249
|
+
* here, so this is belt-and-braces.
|
|
250
|
+
*/
|
|
251
|
+
function evaluateToolUse(input, config) {
|
|
252
|
+
const toolName = typeof input.tool_name === "string" ? input.tool_name : "";
|
|
253
|
+
const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
|
|
254
|
+
const toolInput = input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
|
|
255
|
+
|
|
256
|
+
const pathFields = FILE_TOOL_PATH_FIELDS[toolName];
|
|
257
|
+
if (pathFields) {
|
|
258
|
+
const candidates = [];
|
|
259
|
+
for (const field of pathFields) {
|
|
260
|
+
const value = toolInput[field];
|
|
261
|
+
if (typeof value === "string" && value) candidates.push(value);
|
|
262
|
+
}
|
|
263
|
+
// No explicit path (e.g. Glob/Grep default to cwd): judge cwd itself.
|
|
264
|
+
if (candidates.length === 0) candidates.push(cwd);
|
|
265
|
+
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
const resolved = resolveWithRealpath(path.resolve(cwd, expandHome(candidate)));
|
|
268
|
+
if (!checkPath(resolved, config, READ_ONLY_FILE_TOOLS.has(toolName))) {
|
|
269
|
+
return { allowed: false, reason: denyMessage(toolName, candidate, resolved, config) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { allowed: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (toolName === "Bash") {
|
|
276
|
+
const command = toolInput.command;
|
|
277
|
+
if (typeof command !== "string" || !command) return { allowed: true };
|
|
278
|
+
return evaluateBashCommand(command, cwd, config);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { allowed: true };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function main() {
|
|
285
|
+
try {
|
|
286
|
+
const configPath = process.argv[2];
|
|
287
|
+
if (!configPath) throw new Error("usage: worktree-guard-hook.cjs <guard-config.json>");
|
|
288
|
+
const config = loadGuardConfig(configPath);
|
|
289
|
+
const input = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
|
|
290
|
+
const verdict = evaluateToolUse(input, config);
|
|
291
|
+
if (verdict.allowed) process.exit(0);
|
|
292
|
+
process.stderr.write(verdict.reason);
|
|
293
|
+
process.exit(2);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// Fail closed: a guard that cannot evaluate must not wave the call through.
|
|
296
|
+
process.stderr.write(`Navarch worktree guard failed closed: ${String(err)}`);
|
|
297
|
+
process.exit(2);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (require.main === module) main();
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
DEFAULT_SYSTEM_PREFIXES,
|
|
305
|
+
evaluateBashCommand,
|
|
306
|
+
evaluateToolUse,
|
|
307
|
+
expandHome,
|
|
308
|
+
extractBashPathCandidates,
|
|
309
|
+
isWithin,
|
|
310
|
+
loadGuardConfig,
|
|
311
|
+
resolveWithRealpath,
|
|
312
|
+
};
|
package/dist/adapters/claude.cjs
CHANGED
|
@@ -4,6 +4,17 @@ exports.claudeCodeAdapter = void 0;
|
|
|
4
4
|
exports.runClaudeCodeAdapter = runClaudeCodeAdapter;
|
|
5
5
|
const node_child_process_1 = require("node:child_process");
|
|
6
6
|
const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
7
|
+
const NAVARCH_MCP_ALLOWED_TOOLS = [
|
|
8
|
+
"mcp__navarch__task_comment",
|
|
9
|
+
"mcp__navarch__record_decision",
|
|
10
|
+
"mcp__navarch__request_approval",
|
|
11
|
+
"mcp__navarch__register_resource",
|
|
12
|
+
"mcp__navarch__create_task",
|
|
13
|
+
"mcp__navarch__list_triage_tasks",
|
|
14
|
+
"mcp__navarch__groom_triage_task",
|
|
15
|
+
"mcp__navarch__defer_triage_task",
|
|
16
|
+
"mcp__navarch__file_pr_review",
|
|
17
|
+
];
|
|
7
18
|
/**
|
|
8
19
|
* Headless Claude Code adapter (project-plan.md §3.9 / implementation-plan.md
|
|
9
20
|
* WP-07): `claude -p "<context bundle>" --mcp-config platform-mcp.json
|
|
@@ -26,8 +37,43 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
26
37
|
*/
|
|
27
38
|
async function runClaudeCodeAdapter(options) {
|
|
28
39
|
const args = ["-p", options.prompt];
|
|
40
|
+
const hasExplicitPermissionPolicy = options.extraArgs.some((arg) => [
|
|
41
|
+
"--allowedTools",
|
|
42
|
+
"--allowed-tools",
|
|
43
|
+
"--disallowedTools",
|
|
44
|
+
"--disallowed-tools",
|
|
45
|
+
"--permission-mode",
|
|
46
|
+
"--permission-prompt-tool",
|
|
47
|
+
"--dangerously-skip-permissions",
|
|
48
|
+
].includes(arg));
|
|
49
|
+
// Navarch sessions are unattended: a permission prompt can never be
|
|
50
|
+
// answered and leaves Claude unable to edit the task's isolated worktree.
|
|
51
|
+
// The runtime owns that worktree (and, in Docker mode, the surrounding
|
|
52
|
+
// container), so make the non-interactive session capable of completing
|
|
53
|
+
// coding tasks by default. Operators can replace this with a narrower
|
|
54
|
+
// Claude permission policy through NAVARCH_CLAUDE_EXTRA_ARGS.
|
|
55
|
+
if (!hasExplicitPermissionPolicy) {
|
|
56
|
+
args.push("--dangerously-skip-permissions");
|
|
57
|
+
}
|
|
58
|
+
// The worktree-guard settings file (worktree-guard.cts) installs the
|
|
59
|
+
// PreToolUse boundary hook. Hooks run even under permission bypass, so this
|
|
60
|
+
// is what keeps an unattended session inside its own worktree on a machine
|
|
61
|
+
// running several agents. An operator-supplied --settings (via
|
|
62
|
+
// NAVARCH_CLAUDE_EXTRA_ARGS) wins — two --settings flags on one invocation
|
|
63
|
+
// would be ambiguous, and session.cts logs the guard as skipped.
|
|
64
|
+
if (options.settingsPath && !options.extraArgs.includes("--settings")) {
|
|
65
|
+
args.push("--settings", options.settingsPath);
|
|
66
|
+
}
|
|
29
67
|
if (options.mcpConfigPath) {
|
|
30
68
|
args.push("--mcp-config", options.mcpConfigPath);
|
|
69
|
+
// The Navarch server scopes every call to the authenticated lease and
|
|
70
|
+
// enforces task/project authorization. Keep its tools explicit for
|
|
71
|
+
// operator-supplied policies and for compatibility with Claude versions
|
|
72
|
+
// that still inspect allowedTools while permission bypass is active.
|
|
73
|
+
if (!options.extraArgs.includes("--allowedTools") &&
|
|
74
|
+
!options.extraArgs.includes("--allowed-tools")) {
|
|
75
|
+
args.push("--allowedTools", NAVARCH_MCP_ALLOWED_TOOLS.join(","));
|
|
76
|
+
}
|
|
31
77
|
}
|
|
32
78
|
// Only append the default when the caller hasn't already asked for a
|
|
33
79
|
// specific --output-format (extraArgs wins so an operator can opt back
|
package/dist/adapters/codex.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.codexAdapter = void 0;
|
|
4
4
|
exports.runCodexAdapter = runCodexAdapter;
|
|
5
|
+
exports.hasExplicitPermissionPolicy = hasExplicitPermissionPolicy;
|
|
5
6
|
const node_child_process_1 = require("node:child_process");
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
7
8
|
const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
@@ -29,16 +30,12 @@ const exit_conditions_cjs_1 = require("../exit-conditions.cjs");
|
|
|
29
30
|
* per-session JSON is translated into one-off `-c mcp_servers.*` overrides.
|
|
30
31
|
* Authentication and custom-header values are passed through environment
|
|
31
32
|
* variables so machine/lease credentials never appear in argv.
|
|
32
|
-
* -
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `--dangerously-bypass-approvals-and-sandbox` in published Codex CLI
|
|
39
|
-
* documentation) — deliberately NOT hardcoded here since getting an
|
|
40
|
-
* unverified flag wrong could silently change sandboxing behavior; left to
|
|
41
|
-
* be supplied via NAVARCH_CODEX_EXTRA_ARGS until confirmed.
|
|
33
|
+
* - Host-mode sessions use a generated native Codex permission profile that
|
|
34
|
+
* denies reads and writes outside the session worktree, shared Git dir,
|
|
35
|
+
* temp dirs, and operator-approved extra roots. It also uses `--ask-for-
|
|
36
|
+
* approval never`, so a boundary violation fails back to the unattended
|
|
37
|
+
* agent instead of waiting for input. Explicit permission-policy arguments
|
|
38
|
+
* in NAVARCH_CODEX_EXTRA_ARGS replace that generated profile.
|
|
42
39
|
* - `NAVARCH_CODEX_EXTRA_ARGS` (`extraArgs`) wins over the default `--json`
|
|
43
40
|
* exactly like the Claude adapter's `--output-format` opt-out, so an
|
|
44
41
|
* operator can fall back to plain-text output (or add the real
|
|
@@ -52,6 +49,9 @@ async function runCodexAdapter(options) {
|
|
|
52
49
|
if (!options.extraArgs.includes("--json")) {
|
|
53
50
|
args.push("--json");
|
|
54
51
|
}
|
|
52
|
+
if (options.codexGuardArgs && !hasExplicitPermissionPolicy(options.extraArgs)) {
|
|
53
|
+
args.push(...options.codexGuardArgs);
|
|
54
|
+
}
|
|
55
55
|
args.push(...options.extraArgs);
|
|
56
56
|
if (options.model)
|
|
57
57
|
args.push("--model", options.model);
|
|
@@ -64,6 +64,36 @@ async function runCodexAdapter(options) {
|
|
|
64
64
|
: await runOnHost(runOptions, args);
|
|
65
65
|
return attachUsage(raw);
|
|
66
66
|
}
|
|
67
|
+
/** Operator policy wins over Navarch's generated host-mode profile. */
|
|
68
|
+
function hasExplicitPermissionPolicy(args) {
|
|
69
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
70
|
+
const arg = args[index];
|
|
71
|
+
if ([
|
|
72
|
+
"--sandbox",
|
|
73
|
+
"-s",
|
|
74
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
75
|
+
"--profile",
|
|
76
|
+
"-p",
|
|
77
|
+
"--ignore-user-config",
|
|
78
|
+
].includes(arg) ||
|
|
79
|
+
arg.startsWith("--sandbox=") ||
|
|
80
|
+
arg.startsWith("-s=")) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (arg === "-c" || arg === "--config") {
|
|
84
|
+
const override = args[index + 1] ?? "";
|
|
85
|
+
if (/^(sandbox_mode|default_permissions|permissions)(\.|=)/.test(override))
|
|
86
|
+
return true;
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
else if (arg.startsWith("--config=")) {
|
|
90
|
+
const override = arg.slice("--config=".length);
|
|
91
|
+
if (/^(sandbox_mode|default_permissions|permissions)(\.|=)/.test(override))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
67
97
|
/** Convert Claude's per-session MCP JSON into Codex one-off TOML overrides. */
|
|
68
98
|
async function codexMcpArgs(path, env) {
|
|
69
99
|
const parsed = JSON.parse(await node_fs_1.promises.readFile(path, "utf8"));
|
package/dist/config.cjs
CHANGED
|
@@ -60,5 +60,12 @@ 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),
|
|
63
70
|
};
|
|
64
71
|
}
|
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 = {
|
|
@@ -183,6 +184,39 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
183
184
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
184
185
|
mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
|
|
185
186
|
}
|
|
187
|
+
// Worktree boundary guard (worktree-guard.cts): several sessions share this
|
|
188
|
+
// machine. Host-mode Claude gets a generated PreToolUse hook; host-mode
|
|
189
|
+
// Codex gets an OS-enforced native permission profile over the same roots.
|
|
190
|
+
// Docker mode already has a container boundary. NAVARCH_WORKTREE_GUARD=off
|
|
191
|
+
// opts out for either agent.
|
|
192
|
+
let claudeSettingsPath = null;
|
|
193
|
+
let codexGuardArgs;
|
|
194
|
+
if (config.sandboxMode === "host" && config.worktreeGuard) {
|
|
195
|
+
if (config.agentType === "claude-code") {
|
|
196
|
+
if (config.claudeExtraArgs.includes("--settings")) {
|
|
197
|
+
log.warn("NAVARCH_CLAUDE_EXTRA_ARGS supplies --settings; skipping the generated worktree-guard settings for this session.");
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
|
|
201
|
+
workDir,
|
|
202
|
+
worktreePath: gitWorktree.worktreePath,
|
|
203
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
204
|
+
workspaceRoot: config.workspaceRoot,
|
|
205
|
+
extraRoots: config.guardExtraRoots,
|
|
206
|
+
});
|
|
207
|
+
claudeSettingsPath = guard.settingsPath;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
|
|
212
|
+
workDir,
|
|
213
|
+
worktreePath: gitWorktree.worktreePath,
|
|
214
|
+
repositoryPath: gitWorktree.repositoryPath,
|
|
215
|
+
workspaceRoot: config.workspaceRoot,
|
|
216
|
+
extraRoots: config.guardExtraRoots,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
186
220
|
try {
|
|
187
221
|
await gitWorktree.prepare();
|
|
188
222
|
if (sandbox) {
|
|
@@ -220,6 +254,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
220
254
|
reasoningEffort: execution.reasoning_effort,
|
|
221
255
|
timeoutMs: config.sessionTimeoutMs,
|
|
222
256
|
env: toEnvMap(secrets),
|
|
257
|
+
settingsPath: claudeSettingsPath,
|
|
258
|
+
codexGuardArgs,
|
|
223
259
|
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
224
260
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
225
261
|
signal: activeAbortController.signal,
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
"--ask-for-approval",
|
|
92
|
+
"never",
|
|
93
|
+
"-c",
|
|
94
|
+
`projects.${tomlString(node_path_1.default.resolve(options.worktreePath))}.trust_level="untrusted"`,
|
|
95
|
+
"-c",
|
|
96
|
+
`default_permissions=${tomlString(CODEX_GUARD_PROFILE)}`,
|
|
97
|
+
"-c",
|
|
98
|
+
`permissions.${CODEX_GUARD_PROFILE}.filesystem=${tomlInlineTable(filesystem)}`,
|
|
99
|
+
// Navarch coding tasks must be able to fetch dependencies and push their
|
|
100
|
+
// branch. The profile still constrains filesystem access independently.
|
|
101
|
+
"-c",
|
|
102
|
+
`permissions.${CODEX_GUARD_PROFILE}.network.enabled=true`,
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
function isPathInside(candidate, root) {
|
|
106
|
+
const relative = node_path_1.default.relative(root, candidate);
|
|
107
|
+
return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
|
|
108
|
+
}
|
|
109
|
+
function tomlInlineTable(table) {
|
|
110
|
+
return `{${Object.entries(table)
|
|
111
|
+
.map(([key, value]) => typeof value === "string"
|
|
112
|
+
? `${tomlString(key)}=${tomlString(value)}`
|
|
113
|
+
: `${tomlString(key)}=${tomlInlineTable(value)}`)
|
|
114
|
+
.join(",")}}`;
|
|
115
|
+
}
|
|
116
|
+
function tomlString(value) {
|
|
117
|
+
return JSON.stringify(value);
|
|
118
|
+
}
|
|
119
|
+
function shellQuote(value) {
|
|
120
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|