@sagentlab/navarch-runtime 0.1.32 → 0.1.33

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
@@ -299,6 +299,8 @@ unchanged across the deployment.
299
299
  | `NAVARCH_WORKTREE_STALE_AFTER_MS` | `86400000` (24 hours) | Minimum inactivity age before an abandoned session worktree is removed. Active sessions are always protected. |
300
300
  | `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
301
301
  | `NAVARCH_SANDBOX_MODE` | `host` | `host` uses the resources already available to the agent process. Set `docker` explicitly for container isolation. |
302
+ | `NAVARCH_SANDBOX_PROFILE` | `trusted-development` | Named security profile for Docker sessions (`src/sandbox-profile.cts`): `trusted-development` (image-default user, uncapped, open egress), `untrusted-code` (non-root, 2 CPU / 4g / 512 PIDs, deny-by-default egress, read-only shared git), `elevated-verification` (non-root, 4 CPU / 8g / 2048 PIDs, egress limited to GitHub plus package registries). The default is the exact pre-profile flag set. |
303
+ | `NAVARCH_SANDBOX_EGRESS_NETWORK` | _(unset)_ | Docker network that enforces a profile's egress allowlist. Docker cannot filter by domain itself, so an allowlist profile without this fails closed to `--network=none` and records the denial. |
302
304
  | `NAVARCH_DOCKER_IMAGE` | `ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0` | Version-pinned per-session image with Node 20, git, GitHub CLI, ripgrep, jq, SSH, and Claude Code 2.1.218. Override with an image tag or digest you control. |
303
305
  | `NAVARCH_AGENT` | saved choice, then `claude-code` | Local choice of agent CLI: `claude-code`, `codex`, `gemini`, or `opencode`. Overrides the choice saved by `connect`/`register`; `start --agent` has highest priority. |
304
306
  | `NAVARCH_RUNTIMES` | selected `NAVARCH_AGENT` | Comma list of installed/authenticated adapters advertised to dispatch. The control plane chooses among these per project/task. |
package/dist/config.cjs CHANGED
@@ -8,6 +8,7 @@ exports.isRuntimeAgentType = isRuntimeAgentType;
8
8
  exports.loadRuntimeConfig = loadRuntimeConfig;
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_os_1 = __importDefault(require("node:os"));
11
+ const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
11
12
  /** Published image containing git, GitHub CLI, and the pinned Claude Code CLI. */
12
13
  exports.DEFAULT_SANDBOX_IMAGE = "ghcr.io/sagentlab/navarch-sandbox-agent:0.1.0";
13
14
  function isRuntimeAgentType(value) {
@@ -86,6 +87,13 @@ function loadRuntimeConfig(env = process.env) {
86
87
  gitAuthorEmail: env.NAVARCH_GIT_AUTHOR_EMAIL ?? "z@sagentlab.com",
87
88
  mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
88
89
  sandboxMode,
90
+ // An unrecognized profile name falls back to the default rather than
91
+ // failing startup: a typo must not silently drop a machine out of the
92
+ // dispatch pool, and the default is the posture sessions already had.
93
+ sandboxProfile: (0, sandbox_profile_cjs_1.isSandboxProfileId)(env.NAVARCH_SANDBOX_PROFILE)
94
+ ? env.NAVARCH_SANDBOX_PROFILE
95
+ : sandbox_profile_cjs_1.DEFAULT_SANDBOX_PROFILE_ID,
96
+ sandboxEgressNetwork: env.NAVARCH_SANDBOX_EGRESS_NETWORK?.trim() || null,
89
97
  dockerImage: env.NAVARCH_DOCKER_IMAGE ?? exports.DEFAULT_SANDBOX_IMAGE,
90
98
  // Multiple sessions share one machine; keeping each agent inside its own
91
99
  // worktree is the safe default, so disabling is the explicit opt-out.
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /**
3
+ * Named sandbox security profiles (issue #660).
4
+ *
5
+ * A profile is the *declared* security posture of one session: which user the
6
+ * agent runs as, what resources it may consume, which host paths it may see,
7
+ * and where it may reach on the network. `dockerRunFlags()` /
8
+ * `resolveMounts()` / `resolveNetwork()` turn that declaration into concrete
9
+ * `docker run` arguments, so the policy is written once and audited in one
10
+ * place rather than being spread across ad-hoc flags in sandbox.cts.
11
+ *
12
+ * NOTE ON NAMING: `SandboxPolicyDenial` here is a *security policy* denial (a
13
+ * mount outside the allowlist, an unenforceable egress allowlist). It is
14
+ * deliberately NOT the same concept as `SandboxDenialReason` in
15
+ * lib/navarch/sandbox.ts, which is free-tier billing/eligibility. Do not
16
+ * conflate the two.
17
+ *
18
+ * DEFAULT: `trusted-development` reproduces the pre-#660 Docker flags exactly
19
+ * — root user, no resource caps, Docker's default bridge network — so
20
+ * adopting profiles is a pure refactor for every existing session. The
21
+ * hardened postures are opt-in via NAVARCH_SANDBOX_PROFILE until the default
22
+ * flip is validated on real workloads.
23
+ */
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.DEFAULT_SANDBOX_PROFILE_ID = exports.GITHUB_EGRESS_ALLOWLIST = exports.SANDBOX_PROFILE_IDS = void 0;
29
+ exports.isSandboxProfileId = isSandboxProfileId;
30
+ exports.resolveSandboxProfile = resolveSandboxProfile;
31
+ exports.resolveMounts = resolveMounts;
32
+ exports.resolveNetwork = resolveNetwork;
33
+ exports.dockerRunFlags = dockerRunFlags;
34
+ const node_path_1 = __importDefault(require("node:path"));
35
+ exports.SANDBOX_PROFILE_IDS = [
36
+ "trusted-development",
37
+ "untrusted-code",
38
+ "elevated-verification",
39
+ ];
40
+ function isSandboxProfileId(value) {
41
+ return exports.SANDBOX_PROFILE_IDS.includes(value ?? "");
42
+ }
43
+ /** tmpfs mounts every profile shares: injected secrets must never hit a disk. */
44
+ const SECRET_TMPFS = ["/tmp", "/run"];
45
+ const BASE_SECRETS = {
46
+ allowEnvFlagInjection: false,
47
+ tmpfs: SECRET_TMPFS,
48
+ };
49
+ /**
50
+ * The GitHub egress worktree-guard.cts already grants host-mode Codex
51
+ * sessions (`**.github.com` / `**.githubusercontent.com`). Container profiles
52
+ * reuse the same set so a session cannot reach more from inside the sandbox
53
+ * than the host-mode guard would have allowed.
54
+ */
55
+ exports.GITHUB_EGRESS_ALLOWLIST = [
56
+ "**.github.com",
57
+ "**.githubusercontent.com",
58
+ ];
59
+ /** Registries an elevated verification run needs to install dependencies from. */
60
+ const PACKAGE_REGISTRY_ALLOWLIST = [
61
+ "registry.npmjs.org",
62
+ "**.pypi.org",
63
+ "**.crates.io",
64
+ ];
65
+ const PROFILES = {
66
+ // Byte-for-byte the pre-#660 flag set. Changing anything here changes the
67
+ // behavior of every existing dispatch — see sandbox.test.cts, which pins
68
+ // the resulting argv exactly.
69
+ "trusted-development": {
70
+ id: "trusted-development",
71
+ description: "First-party repositories run by the operator's own agents. Image-default user, no resource caps, unrestricted egress — the pre-#660 Docker posture.",
72
+ user: null,
73
+ memory: null,
74
+ cpus: null,
75
+ pidsLimit: null,
76
+ capDrop: ["ALL"],
77
+ securityOpt: ["no-new-privileges"],
78
+ network: { mode: "open", allow: [] },
79
+ mounts: { allow: ["workspace", "shared-git"], readOnly: [] },
80
+ secrets: BASE_SECRETS,
81
+ },
82
+ // Tightest posture: non-root, capped, GitHub-only egress that fails closed,
83
+ // and the shared bare repo mounted read-only so a hostile checkout cannot
84
+ // rewrite objects other sessions' worktrees depend on.
85
+ "untrusted-code": {
86
+ id: "untrusted-code",
87
+ description: "Third-party or unreviewed code. Non-root, hard CPU/memory/PID caps, deny-by-default egress with a GitHub allowlist, read-only shared git.",
88
+ user: "1000:1000",
89
+ memory: "4g",
90
+ cpus: "2",
91
+ pidsLimit: 512,
92
+ capDrop: ["ALL"],
93
+ securityOpt: ["no-new-privileges"],
94
+ network: { mode: "allowlist", allow: exports.GITHUB_EGRESS_ALLOWLIST },
95
+ mounts: { allow: ["workspace", "shared-git"], readOnly: ["shared-git"] },
96
+ secrets: BASE_SECRETS,
97
+ },
98
+ // Verification workloads (build + full test suite) need real resources and
99
+ // package registries, but stay non-root and stay off the open internet.
100
+ "elevated-verification": {
101
+ id: "elevated-verification",
102
+ description: "Build/test verification of reviewed code. Non-root with raised CPU/memory/PID caps and egress limited to GitHub plus package registries.",
103
+ user: "1000:1000",
104
+ memory: "8g",
105
+ cpus: "4",
106
+ pidsLimit: 2048,
107
+ capDrop: ["ALL"],
108
+ securityOpt: ["no-new-privileges"],
109
+ network: {
110
+ mode: "allowlist",
111
+ allow: [...exports.GITHUB_EGRESS_ALLOWLIST, ...PACKAGE_REGISTRY_ALLOWLIST],
112
+ },
113
+ mounts: { allow: ["workspace", "shared-git"], readOnly: [] },
114
+ secrets: BASE_SECRETS,
115
+ },
116
+ };
117
+ exports.DEFAULT_SANDBOX_PROFILE_ID = "trusted-development";
118
+ /**
119
+ * The profile for `id`, with the operator's egress network (if any) bound in.
120
+ * Returns a fresh object each call so callers cannot mutate the shared table.
121
+ */
122
+ function resolveSandboxProfile(id = exports.DEFAULT_SANDBOX_PROFILE_ID, options = {}) {
123
+ const base = PROFILES[id];
124
+ return {
125
+ ...base,
126
+ network: { ...base.network, egressNetwork: options.egressNetwork ?? null },
127
+ mounts: { ...base.mounts },
128
+ secrets: { ...base.secrets },
129
+ };
130
+ }
131
+ /**
132
+ * `-v` arguments for the requests this profile permits, plus a denial for
133
+ * each one it refuses. Denied mounts are dropped, not fatal: the session
134
+ * still starts with the mounts it is entitled to, and the refusal is recorded.
135
+ */
136
+ function resolveMounts(profile, requests) {
137
+ const args = [];
138
+ const denials = [];
139
+ for (const request of requests) {
140
+ if (!profile.mounts.allow.includes(request.kind)) {
141
+ denials.push({
142
+ policy: "mount",
143
+ code: "mount_kind_not_allowed",
144
+ detail: `profile ${profile.id} does not allow "${request.kind}" mounts (${request.hostPath})`,
145
+ });
146
+ continue;
147
+ }
148
+ if (!isInside(request.hostPath, request.allowedRoot)) {
149
+ denials.push({
150
+ policy: "mount",
151
+ code: "mount_outside_allowed_root",
152
+ detail: `${request.hostPath} is outside the allowed root ${request.allowedRoot} for "${request.kind}" mounts`,
153
+ });
154
+ continue;
155
+ }
156
+ const suffix = profile.mounts.readOnly.includes(request.kind) ? ":ro" : "";
157
+ args.push("-v", `${request.hostPath}:${request.containerPath}${suffix}`);
158
+ }
159
+ return { args, denials };
160
+ }
161
+ /** Same containment test worktree-guard.cts uses for its allowed roots. */
162
+ function isInside(candidate, root) {
163
+ const relative = node_path_1.default.relative(node_path_1.default.resolve(root), node_path_1.default.resolve(candidate));
164
+ return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
165
+ }
166
+ /**
167
+ * `--network` argument for this policy. An `allowlist` policy with no
168
+ * enforcing network degrades to `--network=none` (deny-by-default) and
169
+ * records the denial — it never degrades to open egress.
170
+ */
171
+ function resolveNetwork(policy) {
172
+ switch (policy.mode) {
173
+ case "open":
174
+ // No flag at all: Docker's default bridge, identical to pre-#660.
175
+ return { args: [], denials: [] };
176
+ case "isolated":
177
+ return { args: ["--network=none"], denials: [] };
178
+ case "allowlist": {
179
+ const network = policy.egressNetwork?.trim();
180
+ if (network)
181
+ return { args: ["--network", network], denials: [] };
182
+ return {
183
+ args: ["--network=none"],
184
+ denials: [
185
+ {
186
+ policy: "network",
187
+ code: "egress_allowlist_unenforceable",
188
+ detail: `no egress network configured (NAVARCH_SANDBOX_EGRESS_NETWORK); denying all egress instead of allowing ${policy.allow.join(", ")}`,
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * Hardening + resource + network flags for `docker run`, in a fixed order.
197
+ * Mounts are resolved separately (they need per-session paths) and appended
198
+ * by the backend.
199
+ *
200
+ * Order matters only for the equivalence test that pins the default profile's
201
+ * argv against the pre-#660 command line; Docker itself is order-insensitive
202
+ * among these.
203
+ */
204
+ function dockerRunFlags(profile) {
205
+ const args = [];
206
+ for (const cap of profile.capDrop)
207
+ args.push(`--cap-drop=${cap}`);
208
+ for (const opt of profile.securityOpt)
209
+ args.push(`--security-opt=${opt}`);
210
+ for (const mount of profile.secrets.tmpfs)
211
+ args.push("--tmpfs", mount);
212
+ if (profile.user)
213
+ args.push("--user", profile.user);
214
+ if (profile.memory)
215
+ args.push("--memory", profile.memory);
216
+ if (profile.cpus)
217
+ args.push("--cpus", profile.cpus);
218
+ if (profile.pidsLimit !== null)
219
+ args.push("--pids-limit", String(profile.pidsLimit));
220
+ const network = resolveNetwork(profile.network);
221
+ args.push(...network.args);
222
+ return { args, denials: network.denials };
223
+ }
package/dist/sandbox.cjs CHANGED
@@ -8,6 +8,7 @@ exports.isDockerAvailable = isDockerAvailable;
8
8
  const node_child_process_1 = require("node:child_process");
9
9
  const node_fs_1 = require("node:fs");
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
11
12
  class SandboxUnavailableError extends Error {
12
13
  }
13
14
  exports.SandboxUnavailableError = SandboxUnavailableError;
@@ -85,45 +86,92 @@ function containerName(sessionId) {
85
86
  * helper, so the literal secret value never appears in any argv the host's
86
87
  * `ps` can see and is never written to the container's persistent layer
87
88
  * (tmpfs only) — it disappears with the container on wipe().
89
+ *
90
+ * Since #660 every one of those flags comes from a named security profile
91
+ * (sandbox-profile.cts) rather than being hard-coded here, and this class is
92
+ * the `SandboxBackend` implementation for Docker rather than the only sandbox
93
+ * there can be. The default profile (`trusted-development`) emits exactly the
94
+ * flag set above, so nothing about an existing session changed.
88
95
  */
89
96
  class DockerSandbox {
97
+ id = "docker";
90
98
  name;
99
+ image;
100
+ profile;
91
101
  runner;
102
+ workspaceRoot;
92
103
  workDir;
93
- image;
94
104
  containerWorkDir;
95
105
  sharedGitDir;
106
+ appliedFlags = [];
107
+ denials = [];
96
108
  constructor(opts) {
97
109
  this.name = containerName(opts.sessionId);
98
110
  this.runner = opts.runner ?? exports.nodeCommandRunner;
111
+ this.workspaceRoot = opts.workspaceRoot;
99
112
  this.workDir = node_path_1.default.join(opts.workspaceRoot, opts.sessionId);
100
113
  this.image = opts.image;
101
114
  this.containerWorkDir = opts.containerWorkDir ?? null;
102
115
  this.sharedGitDir = opts.sharedGitDir ?? null;
116
+ this.profile = opts.profile ?? (0, sandbox_profile_cjs_1.resolveSandboxProfile)(sandbox_profile_cjs_1.DEFAULT_SANDBOX_PROFILE_ID);
117
+ }
118
+ /**
119
+ * Host paths this session wants mounted, each pinned to the root it must
120
+ * stay under so resolveMounts() can refuse anything else.
121
+ *
122
+ * - workspace ⊂ the sessions root this sandbox was constructed with, so a
123
+ * crafted session id containing `..` cannot mount a sibling session or
124
+ * escape the workspace entirely.
125
+ * - shared-git ⊂ the Navarch workspace root (the sessions root's parent).
126
+ * session.cts passes `<workspaceRoot>/sessions` here and GitWorktree puts
127
+ * the bare repo cache at the sibling `<workspaceRoot>/repositories/...`
128
+ * (or inside the session root for repo-local-token sessions), so both
129
+ * legitimate locations are covered while `~/.ssh` or `/` are not.
130
+ */
131
+ mountRequests() {
132
+ const sessionsRoot = node_path_1.default.resolve(this.workspaceRoot);
133
+ const navarchRoot = node_path_1.default.dirname(sessionsRoot);
134
+ if (!this.containerWorkDir) {
135
+ return [
136
+ {
137
+ kind: "workspace",
138
+ hostPath: this.workDir,
139
+ containerPath: "/workspace",
140
+ allowedRoot: sessionsRoot,
141
+ },
142
+ ];
143
+ }
144
+ const requests = [
145
+ {
146
+ kind: "workspace",
147
+ hostPath: this.workDir,
148
+ containerPath: this.workDir,
149
+ allowedRoot: sessionsRoot,
150
+ },
151
+ ];
152
+ if (this.sharedGitDir) {
153
+ requests.push({
154
+ kind: "shared-git",
155
+ hostPath: this.sharedGitDir,
156
+ containerPath: this.sharedGitDir,
157
+ allowedRoot: navarchRoot,
158
+ });
159
+ }
160
+ return requests;
103
161
  }
104
162
  async create() {
105
163
  await node_fs_1.promises.mkdir(this.workDir, { recursive: true });
106
- const mounts = this.containerWorkDir
107
- ? [
108
- "-v",
109
- `${this.workDir}:${this.workDir}`,
110
- ...(this.sharedGitDir ? ["-v", `${this.sharedGitDir}:${this.sharedGitDir}`] : []),
111
- "-w",
112
- this.containerWorkDir,
113
- ]
114
- : ["-v", `${this.workDir}:/workspace`, "-w", "/workspace"];
164
+ const policy = (0, sandbox_profile_cjs_1.dockerRunFlags)(this.profile);
165
+ const mounts = (0, sandbox_profile_cjs_1.resolveMounts)(this.profile, this.mountRequests());
166
+ this.denials.push(...policy.denials, ...mounts.denials);
167
+ const workdir = this.containerWorkDir ?? "/workspace";
168
+ this.appliedFlags = [...policy.args, ...mounts.args, "-w", workdir];
115
169
  const result = await this.runner.run("docker", [
116
170
  "run",
117
171
  "-d",
118
172
  "--name",
119
173
  this.name,
120
- "--cap-drop=ALL",
121
- "--security-opt=no-new-privileges",
122
- "--tmpfs",
123
- "/tmp",
124
- "--tmpfs",
125
- "/run",
126
- ...mounts,
174
+ ...this.appliedFlags,
127
175
  this.image,
128
176
  "tail",
129
177
  "-f",
@@ -164,6 +212,39 @@ class DockerSandbox {
164
212
  : `git clone --depth 1 ${shellQuote(url)} repo`;
165
213
  return this.exec(command);
166
214
  }
215
+ /**
216
+ * Copies one container path out to `<workDir>/artifacts/<artifactName>` and
217
+ * returns that host path, or null when it could not be retrieved.
218
+ * Best-effort by contract: evidence collection never fails a session, and
219
+ * the artifact root lives beside the workspace so wipe() removes it too.
220
+ */
221
+ async collectArtifact(containerPath, artifactName) {
222
+ const artifactRoot = node_path_1.default.join(this.workDir, "artifacts");
223
+ const destination = node_path_1.default.join(artifactRoot, artifactName);
224
+ try {
225
+ await node_fs_1.promises.mkdir(artifactRoot, { recursive: true });
226
+ const result = await this.runner.run("docker", [
227
+ "cp",
228
+ `${this.name}:${containerPath}`,
229
+ destination,
230
+ ]);
231
+ return result.code === 0 ? destination : null;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+ /** Profile/flag/denial metadata for run evidence and the sessions audit columns. */
238
+ describe() {
239
+ return {
240
+ backend: this.id,
241
+ profile: this.profile.id,
242
+ image: this.image,
243
+ name: this.name,
244
+ appliedFlags: [...this.appliedFlags],
245
+ denials: [...this.denials],
246
+ };
247
+ }
167
248
  /** Force-removes the container and the host-side workspace mount. Best-effort: never throws. */
168
249
  async wipe() {
169
250
  await this.stop();
package/dist/session.cjs CHANGED
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_fs_1 = require("node:fs");
10
10
  const api_cjs_1 = require("./api.cjs");
11
11
  const sandbox_cjs_1 = require("./sandbox.cjs");
12
+ const sandbox_profile_cjs_1 = require("./sandbox-profile.cjs");
12
13
  const index_cjs_1 = require("./adapters/index.cjs");
13
14
  const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
14
15
  const redact_cjs_1 = require("./redact.cjs");
@@ -109,10 +110,19 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
109
110
  : "best",
110
111
  reasoning_effort: "medium",
111
112
  };
113
+ // Isolation posture reported on every completion so `sessions` records what
114
+ // a run actually executed under (#660). Derived from config alone, so the
115
+ // pre-start failure paths below can stamp it too. Host mode records
116
+ // "host" — an honest answer to "what isolated this run?", not a profile id.
117
+ const sandboxReport = {
118
+ sandbox_profile: config.sandboxMode === "docker" ? config.sandboxProfile : "host",
119
+ ...(config.sandboxMode === "docker" ? { sandbox_image: config.dockerImage } : {}),
120
+ };
112
121
  const executionReport = {
113
122
  model: execution.model,
114
123
  execution_profile: execution.profile,
115
124
  reasoning_effort: execution.reasoning_effort,
125
+ ...sandboxReport,
116
126
  };
117
127
  // The session's identity is the pre-allocated session id sent at claim time
118
128
  // (recorded on the lease by the dispatcher). Lease-scoped API calls
@@ -252,6 +262,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
252
262
  image: config.dockerImage,
253
263
  containerWorkDir: gitWorktree.worktreePath,
254
264
  sharedGitDir: gitWorktree.repositoryPath,
265
+ profile: (0, sandbox_profile_cjs_1.resolveSandboxProfile)(config.sandboxProfile, {
266
+ egressNetwork: config.sandboxEgressNetwork,
267
+ }),
255
268
  })
256
269
  : null;
257
270
  // Platform MCP config (implementation-plan.md WP-07: "--mcp-config
@@ -333,6 +346,12 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
333
346
  await gitWorktree.prepare();
334
347
  if (sandbox) {
335
348
  await sandbox.create();
349
+ // Security-policy refusals (a mount outside the allowlist, an egress
350
+ // allowlist with no network able to enforce it). Unrelated to the
351
+ // billing-tier SandboxDenialReason in lib/navarch/sandbox.ts.
352
+ for (const denial of sandbox.describe().denials) {
353
+ log.warn(`sandbox policy denial [${denial.policy}/${denial.code}]: ${denial.detail}`);
354
+ }
336
355
  await sandbox.injectEnv(sessionEnv);
337
356
  }
338
357
  // The control plane returns the agent type selected for this worker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, Gemini, or OpenCode.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",