@sagentlab/navarch-runtime 0.1.83 → 0.1.85

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 CHANGED
@@ -549,6 +549,11 @@ mount, avoiding bubblewrap startup failures with linked Git worktrees. Its
549
549
  the profile does not grant all of `/tmp`. Extra/tool roots that contain the
550
550
  workspace are omitted on Linux so they cannot expose sibling sessions or
551
551
  session metadata. This also applies when the workspace itself lives under `/tmp`.
552
+ Because bubblewrap starts from an empty `/` and Codex re-executes its own binary
553
+ inside it, the profile also grants read access to the Codex executable's
554
+ install directory (resolved from `NAVARCH_CODEX_BIN` or `PATH`, following
555
+ symlinks) plus mise and nvm tool installs, so a Codex or Node installed under
556
+ `$HOME` still starts. `$HOME` and `CODEX_HOME` themselves are never granted.
552
557
  To verify the linked-worktree boundary on a Linux host with Codex installed,
553
558
  run `NAVARCH_TEST_REAL_CODEX=1 npm test -- tests/codex-linux-sandbox.test.cts`
554
559
  from `runtime/`. The smoke test uses local shell commands and makes no model calls.
@@ -19,12 +19,15 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
19
19
  *
20
20
  * Verified against codex-cli 0.144.1 on 2026-07-18:
21
21
  *
22
- * codex exec "<prompt>" --json [-c mcp_servers.<name>.<key>=<value>] [...extraArgs]
22
+ * codex exec - --json [-c mcp_servers.<name>.<key>=<value>] [...extraArgs] < prompt
23
23
  *
24
24
  * - `exec <prompt>` — ASSUMED to be Codex CLI's non-interactive/headless
25
25
  * subcommand (the `codex exec` "automation mode" analog of `claude -p`):
26
26
  * runs the prompt to completion without the interactive TUI and exits,
27
- * printing its result to stdout.
27
+ * printing its result to stdout. On the host the prompt is `-` and arrives
28
+ * on stdin: Linux rejects any single argv string over 128 KiB
29
+ * (MAX_ARG_STRLEN), so a large review prompt failed `spawn E2BIG` before
30
+ * Codex started (tobi-agent-5571, 2026-10-06).
28
31
  * - `--json` emits top-level `thread.started`, `turn.started`,
29
32
  * `item.completed`, and `turn.completed` JSONL events. See
30
33
  * exit-conditions.cts for the tolerant parser.
@@ -45,7 +48,7 @@ const kill_agent_cjs_1 = require("../kill-agent.cjs");
45
48
  * approval-bypass flag) without an adapter code change.
46
49
  */
47
50
  async function runCodexAdapter(options) {
48
- const args = ["exec", options.prompt];
51
+ const args = ["exec", options.dockerExec ? options.prompt : "-"];
49
52
  const env = { ...options.env };
50
53
  if (options.mcpConfigPath)
51
54
  args.push(...(await codexMcpArgs(options.mcpConfigPath, env)));
@@ -173,10 +176,11 @@ async function runOnHost(options, args) {
173
176
  cwd: options.cwd,
174
177
  env: { ...process.env, ...options.env },
175
178
  });
176
- // `codex exec` appends piped stdin to the prompt. The spawned process gets
177
- // a pipe by default, so close it immediately or it can wait forever for
178
- // input even though the full prompt was supplied as an argument.
179
- child.stdin?.end();
179
+ // `codex exec -` reads the whole prompt from stdin and starts at EOF, so
180
+ // end the pipe after writing it. A child that exits before reading
181
+ // surfaces through `close`; ignore the resulting EPIPE here.
182
+ child.stdin?.on("error", () => undefined);
183
+ child.stdin?.end(options.prompt);
180
184
  options.onProcessStarted?.(child.pid);
181
185
  const timer = options.timeoutMs > 0 ? setTimeout(() => {
182
186
  timedOut = true;
@@ -155,6 +155,7 @@ function codexSandboxProbeArgs(config, roots) {
155
155
  repositoryPath: node_path_1.default.join(workspaceRoot, "repo"),
156
156
  workspaceRoot,
157
157
  extraRoots: [...config.guardExtraRoots, ...roots.readRoots],
158
+ codexBin: config.codexBin,
158
159
  }).filter((arg) => arg !== "--ignore-user-config");
159
160
  }
160
161
  const PROBE_SCRIPT = `
@@ -200,9 +201,15 @@ async function probeCodexSandbox(config, deps) {
200
201
  return null;
201
202
  const output = `${result.stderr}\n${result.stdout}`;
202
203
  if (!result.stdout.includes(PROBE_START_MARKER)) {
204
+ // bwrap could not find the program it was asked to start inside the
205
+ // sandbox view: a path outside the granted read roots, not an old CLI.
206
+ const hidden = /bwrap: execvp .*No such file or directory/.test(output);
203
207
  return ("Could not verify a browser inside the Codex sandbox: `codex sandbox` did not run the probe " +
204
208
  `(${result.timedOut ? "timed out" : output.trim() ? summarizeLaunchFailure(output) : `exit ${result.code}`}). ` +
205
- "Update the Codex CLI, or run browser tasks with another agent.");
209
+ (hidden
210
+ ? "The sandbox cannot see that executable; add its install directory to NAVARCH_GUARD_EXTRA_ROOTS, " +
211
+ "or run browser tasks with another agent."
212
+ : "Update the Codex CLI, or run browser tasks with another agent."));
206
213
  }
207
214
  const detail = result.timedOut ? "the launch timed out." : summarizeLaunchFailure(output);
208
215
  const machPort = /bootstrap_check_in|MachPortRendezvous|mach_port_rendezvous/i.test(output);
package/dist/session.cjs CHANGED
@@ -389,6 +389,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
389
389
  repositoryPath: gitWorktree.repositoryPath,
390
390
  workspaceRoot: config.workspaceRoot,
391
391
  extraRoots: config.guardExtraRoots,
392
+ codexBin: config.codexBin,
392
393
  });
393
394
  }
394
395
  else if (runtime === "gemini") {
@@ -7,6 +7,7 @@ exports.guardHookScriptPath = guardHookScriptPath;
7
7
  exports.readClaudeApiKeyHelper = readClaudeApiKeyHelper;
8
8
  exports.prepareWorktreeGuard = prepareWorktreeGuard;
9
9
  exports.codexToolReadRoots = codexToolReadRoots;
10
+ exports.codexExecutableReadRoots = codexExecutableReadRoots;
10
11
  exports.macosDeveloperWriteRoots = macosDeveloperWriteRoots;
11
12
  exports.sessionGhConfigDir = sessionGhConfigDir;
12
13
  exports.sessionCodexTmpDir = sessionCodexTmpDir;
@@ -131,9 +132,75 @@ function codexToolReadRoots(env = process.env, platform = process.platform) {
131
132
  "/System/Library/CoreServices/SystemAppearance.bundle",
132
133
  node_path_1.default.join(homeDir, "Library", "Caches", "ms-playwright"),
133
134
  node_path_1.default.join(homeDir, ".cache", "ms-playwright"),
135
+ // Per-user toolchain installs (node/npm, Codex itself). Shell commands
136
+ // under the Linux sandbox see only granted roots, so a mise- or
137
+ // nvm-managed `node` on PATH is otherwise "command not found".
138
+ node_path_1.default.join(miseDataDir(env, homeDir), "installs"),
139
+ node_path_1.default.join(env.NVM_DIR?.trim() || node_path_1.default.join(homeDir, ".nvm"), "versions"),
134
140
  ...(platform === "darwin" ? MACOS_TOOLCHAIN_READ_ROOTS : []),
135
141
  ];
136
142
  }
143
+ function miseDataDir(env, homeDir) {
144
+ const explicit = env.MISE_DATA_DIR?.trim();
145
+ if (explicit)
146
+ return explicit;
147
+ return node_path_1.default.join(env.XDG_DATA_HOME?.trim() || node_path_1.default.join(homeDir, ".local", "share"), "mise");
148
+ }
149
+ /**
150
+ * Directories holding the Codex executable itself.
151
+ *
152
+ * On Linux a restricted-read profile starts bwrap from an empty `/` with only
153
+ * granted roots mounted, and Codex re-executes its own binary inside that view
154
+ * to install seccomp. A CLI installed outside `:minimal` (mise, nvm, an npm
155
+ * prefix under $HOME) then fails every sandboxed command with
156
+ * `bwrap: execvp <codex>: No such file or directory`. Grants the PATH entry,
157
+ * its symlink target's directory, and an npm package root (whose vendored
158
+ * native binary is what actually re-executes). Never returns $HOME, CODEX_HOME,
159
+ * or an ancestor of either: those would expose credentials read-only.
160
+ */
161
+ function codexExecutableReadRoots(bin, env = process.env) {
162
+ const resolved = resolveExecutable(bin, env);
163
+ if (!resolved)
164
+ return [];
165
+ let real = resolved;
166
+ try {
167
+ real = (0, node_fs_1.realpathSync)(resolved);
168
+ }
169
+ catch {
170
+ // Keep the logical path; bwrap mounts it as given.
171
+ }
172
+ const roots = [node_path_1.default.dirname(resolved), node_path_1.default.dirname(real)];
173
+ const packageRoot = /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//.exec(real)?.[1];
174
+ if (packageRoot)
175
+ roots.push(packageRoot);
176
+ const homeDir = env.HOME?.trim() || node_os_1.default.homedir();
177
+ const sensitive = [homeDir, env.CODEX_HOME?.trim() || node_path_1.default.join(homeDir, ".codex")].flatMap((dir) => {
178
+ try {
179
+ return [node_path_1.default.resolve(dir), (0, node_fs_1.realpathSync)(dir)];
180
+ }
181
+ catch {
182
+ return [node_path_1.default.resolve(dir)];
183
+ }
184
+ });
185
+ return [...new Set(roots.map((root) => node_path_1.default.resolve(root)))].filter((root) => !sensitive.some((target) => isPathInside(target, root)));
186
+ }
187
+ function resolveExecutable(bin, env) {
188
+ const candidates = bin.includes("/")
189
+ ? [node_path_1.default.resolve(bin)]
190
+ : (env.PATH ?? "").split(node_path_1.default.delimiter).filter(Boolean).map((dir) => node_path_1.default.join(dir, bin));
191
+ for (const candidate of candidates) {
192
+ try {
193
+ if (!(0, node_fs_1.statSync)(candidate).isFile())
194
+ continue;
195
+ (0, node_fs_1.accessSync)(candidate, node_fs_1.constants.X_OK);
196
+ return node_path_1.default.resolve(candidate);
197
+ }
198
+ catch {
199
+ // Not here; keep searching PATH.
200
+ }
201
+ }
202
+ return undefined;
203
+ }
137
204
  /**
138
205
  * World-readable macOS toolchain locations. `xcrun simctl` / `xcodebuild`
139
206
  * load /Library/Developer/PrivateFrameworks/CoreSimulator.framework and the
@@ -213,7 +280,11 @@ function codexWorktreeGuardArgs(options, platform = process.platform) {
213
280
  [sessionGhConfigDir(options.workDir)]: "read",
214
281
  };
215
282
  const overlapsWorkspace = (root) => isPathInside(root, workspaceRoot) || (linux && isPathInside(workspaceRoot, root));
216
- for (const root of codexToolReadRoots(process.env, platform)) {
283
+ const readRoots = [
284
+ ...codexToolReadRoots(process.env, platform),
285
+ ...(linux && options.codexBin ? codexExecutableReadRoots(options.codexBin) : []),
286
+ ];
287
+ for (const root of readRoots) {
217
288
  const resolved = node_path_1.default.resolve(root);
218
289
  if (overlapsWorkspace(resolved))
219
290
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",