@nathapp/nax-agent 0.91.4 → 0.91.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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to `@nathapp/nax-agent` are recorded here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). While the version is `0.x`, a minor
5
5
  release may change the public API.
6
6
 
7
+ ## [0.91.6] - 2026-10-24
8
+
9
+ - No changes in this package. Released in lockstep with the other nax packages.
10
+
11
+ ## [0.91.5] - 2026-10-23
12
+
13
+ - No changes in this package. Released in lockstep with the other nax packages.
14
+
7
15
  ## [0.91.4] - 2026-10-23
8
16
 
9
17
  - No changes in this package. Released in lockstep with the other nax packages.
@@ -41,7 +41,8 @@ export const SandboxConfigSchema = z.object({
41
41
  /**
42
42
  * US-002 (#2538): let a sandboxed command reach loopback ports.
43
43
  *
44
- * Off when absent, and it only changes anything alongside an
44
+ * Absent: on when `allowedDomains` is set (see `effectiveAllowLocalBinding`),
45
+ * off otherwise. It only changes anything alongside an
45
46
  * `allowedDomains` list: with an open network srt's macOS profile already
46
47
  * allows all traffic. srt reads it once per process, from the config given
47
48
  * to `SandboxManager.initialize`.
@@ -1,9 +1,28 @@
1
- /** Node glob normalized to the agent contract and Bun's symlink traversal. */
1
+ /**
2
+ * Node glob normalized to the agent contract and Bun's symlink traversal.
3
+ *
4
+ * Every dot segment hides a result (ruling D21), with one exemption: a pattern
5
+ * whose first segment is literally `.nax` may surface results whose first
6
+ * result-relative segment is `.nax` (nax's own artifacts live there). Dot
7
+ * segments below `.nax/` stay hidden, and so does every other hidden path.
8
+ */
2
9
  import { globSync, lstatSync, statSync } from "node:fs";
3
10
  import { glob } from "node:fs/promises";
4
11
  import { relative, resolve, sep } from "node:path";
12
+ const NAX_DIR = ".nax";
13
+ /** True when the pattern's first `/` segment is exactly `.nax`: no glob metacharacter, no escape. */
14
+ function exemptsNaxDir(pattern) {
15
+ return pattern.replace(/^(?:\.\/)+/, "").split("/")[0] === NAX_DIR;
16
+ }
17
+ /** Whether a result-relative path has a hidden dot segment, ignoring a leading `.nax` the pattern asked for. */
18
+ function hasHiddenSegment(path, naxExempt) {
19
+ return path
20
+ .split(sep)
21
+ .some((segment, index) => segment.startsWith(".") && !(naxExempt && index === 0 && segment === NAX_DIR));
22
+ }
5
23
  function scanOptions(pattern, opts) {
6
24
  const cwd = resolve(opts.cwd);
25
+ const naxExempt = exemptsNaxDir(pattern);
7
26
  if (!statSync(cwd).isDirectory()) {
8
27
  throw Object.assign(new Error(`ENOTDIR: not a directory, glob '${cwd}'`), { code: "ENOTDIR", path: cwd });
9
28
  }
@@ -24,7 +43,7 @@ function scanOptions(pattern, opts) {
24
43
  };
25
44
  const exclude = (entry) => {
26
45
  const path = relative(cwd, resolve(cwd, entry.parentPath, entry.name));
27
- if (path.split(sep).some((segment) => segment.startsWith(".")))
46
+ if (hasHiddenSegment(path, naxExempt))
28
47
  return true;
29
48
  // Node can optimize brace/class segments without reporting their symlink
30
49
  // entries. Check lexical ancestors too, allowing only the literal prefix.
@@ -45,7 +64,7 @@ function filePath(entry, cwd, absolute, pattern) {
45
64
  statSync(full, { throwIfNoEntry: false })?.isFile()))
46
65
  return null;
47
66
  const path = relative(cwd, full);
48
- if (path.split(sep).some((segment) => segment.startsWith(".")))
67
+ if (hasHiddenSegment(path, exemptsNaxDir(pattern)))
49
68
  return null;
50
69
  const dotPrefix = pattern.match(/^(?:\.\/)+/)?.[0] ?? "";
51
70
  return absolute ? resolve(full) : `${dotPrefix}${path}`;
@@ -0,0 +1,3 @@
1
+ import type { SandboxConfig } from "../config/schemas-sandbox.ts";
2
+ /** Explicit loopback settings win; an allow-list (including []) defaults on. */
3
+ export declare function effectiveAllowLocalBinding(network: SandboxConfig["network"]): boolean;
@@ -0,0 +1,6 @@
1
+ /** Explicit loopback settings win; an allow-list (including []) defaults on. */
2
+ export function effectiveAllowLocalBinding(network) {
3
+ return typeof network.allowLocalBinding === "boolean"
4
+ ? network.allowLocalBinding
5
+ : network.allowedDomains !== undefined;
6
+ }
@@ -13,6 +13,7 @@ import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
13
13
  import { CLAUDE_SETTINGS_FILES } from "./claude-settings.js";
14
14
  import { BUILTIN_CACHE_WRITE_ROOTS, BUILTIN_CREDENTIAL_READ_DENIES, MACOS_CACHE_WRITE_ROOT, SRT_MACOS_TMPDIR, SRT_MACOS_TMPDIR_DENIES, } from "./defaults.js";
15
15
  import { WORKTREE_COMMON_WRITE_DIRS, WORKTREE_CONFIG_FILE } from "./git-guards.js";
16
+ import { effectiveAllowLocalBinding } from "./network.js";
16
17
  function expandHome(p, home) {
17
18
  if (p === "~")
18
19
  return home;
@@ -162,10 +163,8 @@ export function buildSandboxPolicy(input) {
162
163
  ...config.filesystem.denyRead.map((p) => resolve(root, expandHome(p, home))),
163
164
  ]);
164
165
  const allowed = config.network.allowedDomains;
165
- // US-002: the flag is carried only when the config asks for it, so an absent
166
- // -- or explicitly false -- config leaves `network` without the key. The type
167
- // only admits `true` (see SandboxNetworkPolicy), and srt treats absent as off.
168
- const loopback = config.network.allowLocalBinding === true;
166
+ // Resolve the config default before constructing a true-or-absent policy.
167
+ const loopback = effectiveAllowLocalBinding(config.network);
169
168
  return {
170
169
  writeRoots,
171
170
  denyWrite,
@@ -4,6 +4,7 @@
4
4
  * serves one project, so one network config per process holds.
5
5
  */
6
6
  import { getSafeLogger } from "#src/infra/index";
7
+ import { effectiveAllowLocalBinding } from "./network.js";
7
8
  import { probeSandbox } from "./probe.js";
8
9
  import { createSrtBackend } from "./srt-backend.js";
9
10
  export const _sandboxRegistryDeps = {
@@ -14,7 +15,10 @@ let backend;
14
15
  let probed;
15
16
  let warnedUnavailable = false;
16
17
  export function sandboxBackendFor(config) {
17
- backend ??= _sandboxRegistryDeps.createBackend(config.network);
18
+ backend ??= _sandboxRegistryDeps.createBackend({
19
+ ...config.network,
20
+ allowLocalBinding: effectiveAllowLocalBinding(config.network),
21
+ });
18
22
  return backend;
19
23
  }
20
24
  export function probeSandboxOnce(target, storyId = "_sandbox") {
@@ -1,3 +1,2 @@
1
- /** nax#2500: a launcher's sandbox as plain data for a backend that runs its own shell. */
2
1
  import type { CommandLauncher, SessionSandbox } from "./types.ts";
3
- export declare function sessionSandboxFrom(launcher: CommandLauncher | undefined, root: string): Promise<SessionSandbox | undefined>;
2
+ export declare function sessionSandboxFrom(launcher: CommandLauncher | undefined, root: string, writeExemption?: string): Promise<SessionSandbox | undefined>;
@@ -1,9 +1,15 @@
1
- export async function sessionSandboxFrom(launcher, root) {
1
+ /** nax#2500: a launcher's sandbox as plain data for a backend that runs its own shell. */
2
+ import { resolve } from "node:path";
3
+ export async function sessionSandboxFrom(launcher, root, writeExemption) {
2
4
  if (launcher === undefined || launcher.state.kind === "disabled")
3
5
  return undefined;
4
6
  if (launcher.state.kind === "unavailable")
5
7
  return { kind: "unavailable", reason: launcher.state.reason };
6
8
  if (launcher.policyAt === undefined)
7
9
  return { kind: "unavailable", reason: "the launcher exposes no policy" };
8
- return { kind: "policy", policy: await launcher.policyAt(root) };
10
+ return {
11
+ kind: "policy",
12
+ policy: await launcher.policyAt(root),
13
+ ...(writeExemption === undefined ? {} : { writeExemption: resolve(root, writeExemption) }),
14
+ };
9
15
  }
@@ -14,7 +14,8 @@ export interface SandboxNetworkPolicy {
14
14
  * US-002: srt may let a command bind to -- and reach -- loopback ports, so a
15
15
  * DB-backed test run sees a local server that is up (#2538).
16
16
  *
17
- * `true` only. srt's own default is off, so absent and `false` are the same
17
+ * The config default is resolved by `effectiveAllowLocalBinding` before
18
+ * policy construction. `true` only. srt's own default is off, so absent and `false` are the same
18
19
  * thing to it, and a policy that cannot express "off" cannot contradict the
19
20
  * config it was built from.
20
21
  */
@@ -34,6 +35,8 @@ export interface SandboxPolicy {
34
35
  export type SessionSandbox = {
35
36
  readonly kind: "policy";
36
37
  readonly policy: SandboxPolicy;
38
+ /** Absolute path of the session's single writable output. */
39
+ readonly writeExemption?: string;
37
40
  } | {
38
41
  readonly kind: "unavailable";
39
42
  readonly reason: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax-agent",
3
- "version": "0.91.4",
3
+ "version": "0.91.6",
4
4
  "description": "nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.",
5
5
  "license": "MIT",
6
6
  "author": "William Khoo",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@anthropic-ai/sandbox-runtime": "0.0.77",
49
49
  "@modelcontextprotocol/sdk": "^1.32.1",
50
- "@nathapp/nax-ai": "0.91.4",
50
+ "@nathapp/nax-ai": "0.91.6",
51
51
  "zod": "^4.3.6"
52
52
  },
53
53
  "publishConfig": {