@sagentlab/navarch-runtime 0.1.83 → 0.1.84

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.
@@ -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.84",
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",