@sagentlab/navarch-runtime 0.1.81 → 0.1.83

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
@@ -543,6 +543,15 @@ The Claude hook is a strong guardrail rather than a hard security boundary
543
543
  because shell paths are screened lexically. Codex's permission profile is
544
544
  enforced by its OS sandbox. For container-grade whole-process isolation use
545
545
  `NAVARCH_SANDBOX_MODE=docker`; neither host guard is installed in Docker mode.
546
+ On Linux, Codex uses an explicit allowlist without a denied workspace-parent
547
+ mount, avoiding bubblewrap startup failures with linked Git worktrees. Its
548
+ `TMPDIR`, `TMP`, and `TEMP` point to a private `tmp` directory inside the session;
549
+ the profile does not grant all of `/tmp`. Extra/tool roots that contain the
550
+ workspace are omitted on Linux so they cannot expose sibling sessions or
551
+ session metadata. This also applies when the workspace itself lives under `/tmp`.
552
+ To verify the linked-worktree boundary on a Linux host with Codex installed,
553
+ run `NAVARCH_TEST_REAL_CODEX=1 npm test -- tests/codex-linux-sandbox.test.cts`
554
+ from `runtime/`. The smoke test uses local shell commands and makes no model calls.
546
555
  An operator-supplied `--settings` in `NAVARCH_CLAUDE_EXTRA_ARGS`, or an
547
556
  explicit Codex permission/sandbox option in `NAVARCH_CODEX_EXTRA_ARGS`, takes
548
557
  precedence over the generated policy.
@@ -39,6 +39,7 @@ function parseClaudeJsonResult(stdout) {
39
39
  const isUsableResult = (value) => typeof value === "object" &&
40
40
  value !== null &&
41
41
  !Array.isArray(value) &&
42
+ (!("result" in value) || typeof value.result === "string") &&
42
43
  ("result" in value || "usage" in value || "cost_usd" in value || "total_cost_usd" in value);
43
44
  try {
44
45
  const parsed = JSON.parse(trimmed);
package/dist/session.cjs CHANGED
@@ -378,6 +378,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
378
378
  }
379
379
  }
380
380
  else if (runtime === "codex") {
381
+ if (process.platform === "linux") {
382
+ const tmpDir = (0, worktree_guard_cjs_1.sessionCodexTmpDir)(workDir);
383
+ await node_fs_1.promises.mkdir(tmpDir, { recursive: true, mode: 0o700 });
384
+ Object.assign(sessionEnv, { TMPDIR: tmpDir, TMP: tmpDir, TEMP: tmpDir });
385
+ }
381
386
  codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
382
387
  workDir,
383
388
  worktreePath: gitWorktree.worktreePath,
@@ -9,6 +9,7 @@ exports.prepareWorktreeGuard = prepareWorktreeGuard;
9
9
  exports.codexToolReadRoots = codexToolReadRoots;
10
10
  exports.macosDeveloperWriteRoots = macosDeveloperWriteRoots;
11
11
  exports.sessionGhConfigDir = sessionGhConfigDir;
12
+ exports.sessionCodexTmpDir = sessionCodexTmpDir;
12
13
  exports.codexWorktreeGuardArgs = codexWorktreeGuardArgs;
13
14
  exports.geminiSandboxMounts = geminiSandboxMounts;
14
15
  const node_path_1 = __importDefault(require("node:path"));
@@ -172,6 +173,9 @@ function macosDeveloperWriteRoots(env = process.env, platform = process.platform
172
173
  function sessionGhConfigDir(workDir) {
173
174
  return node_path_1.default.join(workDir, SESSION_GH_CONFIG_DIRNAME);
174
175
  }
176
+ function sessionCodexTmpDir(workDir) {
177
+ return node_path_1.default.resolve(workDir, "tmp");
178
+ }
175
179
  /**
176
180
  * Builds one-off Codex permission-profile arguments for a host session.
177
181
  *
@@ -180,33 +184,46 @@ function sessionGhConfigDir(workDir) {
180
184
  * this boundary. Authentication still comes from CODEX_HOME. The profile
181
185
  * grants only the runtime paths common tools need, the session worktree, the
182
186
  * shared Git directory, temp space, and operator-approved extra roots. The
183
- * surrounding Navarch workspace is denied, then the more-specific current
184
- * worktree/repository grants reopen only this session's paths. The worktree
187
+ * surrounding Navarch workspace is denied (implicitly on Linux), with only
188
+ * this session's paths granted access. The worktree
185
189
  * is marked untrusted for configuration purposes so a checked-in legacy
186
190
  * sandbox_mode cannot disable the generated permission profile; repository
187
191
  * instructions such as AGENTS.md still load normally.
188
192
  */
189
- function codexWorktreeGuardArgs(options) {
193
+ function codexWorktreeGuardArgs(options, platform = process.platform) {
194
+ const workspaceRoot = node_path_1.default.resolve(options.workspaceRoot);
195
+ const linux = platform === "linux";
196
+ // On Linux, a denied ancestor is an empty read-only bwrap mount. Codex
197
+ // can then try to protect a linked worktree's gitdir before reopening the
198
+ // bare repository, failing to mkdir repository.git/worktrees/repo beneath
199
+ // that mount. Use restricted-read's implicit deny instead. Broad temp or
200
+ // ancestor grants would expose the workspace without the explicit mask,
201
+ // so Linux gets only a session temp root and non-overlapping extra roots.
190
202
  const filesystem = {
191
203
  ":minimal": "read",
192
- ":tmpdir": "write",
193
- ":slash_tmp": "write",
194
- [node_path_1.default.resolve(options.workspaceRoot)]: "deny",
204
+ ...(linux
205
+ ? { [sessionCodexTmpDir(options.workDir)]: "write" }
206
+ : {
207
+ ":tmpdir": "write",
208
+ ":slash_tmp": "write",
209
+ [workspaceRoot]: "deny",
210
+ }),
195
211
  [node_path_1.default.resolve(options.worktreePath)]: "write",
196
212
  [node_path_1.default.resolve(options.repositoryPath)]: "write",
197
213
  [sessionGhConfigDir(options.workDir)]: "read",
198
214
  };
199
- for (const root of codexToolReadRoots()) {
215
+ const overlapsWorkspace = (root) => isPathInside(root, workspaceRoot) || (linux && isPathInside(workspaceRoot, root));
216
+ for (const root of codexToolReadRoots(process.env, platform)) {
200
217
  const resolved = node_path_1.default.resolve(root);
201
- if (isPathInside(resolved, node_path_1.default.resolve(options.workspaceRoot)))
218
+ if (overlapsWorkspace(resolved))
202
219
  continue;
203
220
  filesystem[resolved] = "read";
204
221
  }
205
- for (const root of [...macosDeveloperWriteRoots(), ...(options.extraRoots ?? [])]) {
222
+ for (const root of [...macosDeveloperWriteRoots(process.env, platform), ...(options.extraRoots ?? [])]) {
206
223
  const resolved = node_path_1.default.resolve(root);
207
224
  // Match the Claude hook's denied-root precedence: an extra root cannot
208
225
  // reopen sibling sessions or metadata inside the Navarch workspace.
209
- if (isPathInside(resolved, node_path_1.default.resolve(options.workspaceRoot)))
226
+ if (overlapsWorkspace(resolved))
210
227
  continue;
211
228
  filesystem[resolved] = "write";
212
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
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",