@nathapp/nax 0.42.6 → 0.42.7

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/dist/nax.js CHANGED
@@ -19075,6 +19075,7 @@ class SpawnAcpClient {
19075
19075
  model;
19076
19076
  cwd;
19077
19077
  timeoutSeconds;
19078
+ permissionMode;
19078
19079
  env;
19079
19080
  pidRegistry;
19080
19081
  constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
@@ -19088,6 +19089,7 @@ class SpawnAcpClient {
19088
19089
  this.agentName = lastToken;
19089
19090
  this.cwd = cwd || process.cwd();
19090
19091
  this.timeoutSeconds = timeoutSeconds || 1800;
19092
+ this.permissionMode = "approve-reads";
19091
19093
  this.env = buildAllowedEnv2();
19092
19094
  this.pidRegistry = pidRegistry;
19093
19095
  }
@@ -19126,7 +19128,7 @@ class SpawnAcpClient {
19126
19128
  cwd: this.cwd,
19127
19129
  model: this.model,
19128
19130
  timeoutSeconds: this.timeoutSeconds,
19129
- permissionMode: "approve-all",
19131
+ permissionMode: this.permissionMode,
19130
19132
  env: this.env,
19131
19133
  pidRegistry: this.pidRegistry
19132
19134
  });
@@ -20200,14 +20202,27 @@ var init_chain = __esm(() => {
20200
20202
  });
20201
20203
 
20202
20204
  // src/utils/path-security.ts
20203
- import { isAbsolute, join as join5, normalize, resolve } from "path";
20205
+ import { realpathSync } from "fs";
20206
+ import { dirname, isAbsolute, join as join5, normalize, resolve } from "path";
20207
+ function safeRealpath(p) {
20208
+ try {
20209
+ return realpathSync(p);
20210
+ } catch {
20211
+ try {
20212
+ const parent = realpathSync(dirname(p));
20213
+ return join5(parent, p.split("/").pop() ?? "");
20214
+ } catch {
20215
+ return p;
20216
+ }
20217
+ }
20218
+ }
20204
20219
  function validateModulePath(modulePath, allowedRoots) {
20205
20220
  if (!modulePath) {
20206
20221
  return { valid: false, error: "Module path is empty" };
20207
20222
  }
20208
- const normalizedRoots = allowedRoots.map((r) => resolve(r));
20223
+ const normalizedRoots = allowedRoots.map((r) => safeRealpath(resolve(r)));
20209
20224
  if (isAbsolute(modulePath)) {
20210
- const absoluteTarget = normalize(modulePath);
20225
+ const absoluteTarget = safeRealpath(normalize(modulePath));
20211
20226
  const isWithin = normalizedRoots.some((root) => {
20212
20227
  return absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root;
20213
20228
  });
@@ -20216,7 +20231,7 @@ function validateModulePath(modulePath, allowedRoots) {
20216
20231
  }
20217
20232
  } else {
20218
20233
  for (const root of normalizedRoots) {
20219
- const absoluteTarget = resolve(join5(root, modulePath));
20234
+ const absoluteTarget = safeRealpath(resolve(join5(root, modulePath)));
20220
20235
  if (absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root) {
20221
20236
  return { valid: true, absolutePath: absoluteTarget };
20222
20237
  }
@@ -20496,7 +20511,7 @@ function isPlainObject2(value) {
20496
20511
  }
20497
20512
 
20498
20513
  // src/config/path-security.ts
20499
- import { existsSync as existsSync4, lstatSync, realpathSync } from "fs";
20514
+ import { existsSync as existsSync4, lstatSync, realpathSync as realpathSync2 } from "fs";
20500
20515
  import { isAbsolute as isAbsolute2, normalize as normalize2, resolve as resolve3 } from "path";
20501
20516
  function validateDirectory(dirPath, baseDir) {
20502
20517
  const resolved = resolve3(dirPath);
@@ -20505,7 +20520,7 @@ function validateDirectory(dirPath, baseDir) {
20505
20520
  }
20506
20521
  let realPath;
20507
20522
  try {
20508
- realPath = realpathSync(resolved);
20523
+ realPath = realpathSync2(resolved);
20509
20524
  } catch (error48) {
20510
20525
  throw new Error(`Failed to resolve path: ${dirPath} (${error48.message})`);
20511
20526
  }
@@ -20519,7 +20534,7 @@ function validateDirectory(dirPath, baseDir) {
20519
20534
  }
20520
20535
  if (baseDir) {
20521
20536
  const resolvedBase = resolve3(baseDir);
20522
- const realBase = existsSync4(resolvedBase) ? realpathSync(resolvedBase) : resolvedBase;
20537
+ const realBase = existsSync4(resolvedBase) ? realpathSync2(resolvedBase) : resolvedBase;
20523
20538
  if (!isWithinDirectory(realPath, realBase)) {
20524
20539
  throw new Error(`Path is outside allowed directory: ${dirPath} (resolved to ${realPath}, base: ${realBase})`);
20525
20540
  }
@@ -20543,19 +20558,19 @@ function validateFilePath(filePath, baseDir) {
20543
20558
  if (!existsSync4(resolved)) {
20544
20559
  const parent = resolve3(resolved, "..");
20545
20560
  if (existsSync4(parent)) {
20546
- const realParent = realpathSync(parent);
20561
+ const realParent = realpathSync2(parent);
20547
20562
  realPath = resolve3(realParent, filePath.split("/").pop() || "");
20548
20563
  } else {
20549
20564
  realPath = resolved;
20550
20565
  }
20551
20566
  } else {
20552
- realPath = realpathSync(resolved);
20567
+ realPath = realpathSync2(resolved);
20553
20568
  }
20554
20569
  } catch (error48) {
20555
20570
  throw new Error(`Failed to resolve path: ${filePath} (${error48.message})`);
20556
20571
  }
20557
20572
  const resolvedBase = resolve3(baseDir);
20558
- const realBase = existsSync4(resolvedBase) ? realpathSync(resolvedBase) : resolvedBase;
20573
+ const realBase = existsSync4(resolvedBase) ? realpathSync2(resolvedBase) : resolvedBase;
20559
20574
  if (!isWithinDirectory(realPath, realBase)) {
20560
20575
  throw new Error(`Path is outside allowed directory: ${filePath} (resolved to ${realPath}, base: ${realBase})`);
20561
20576
  }
@@ -21899,7 +21914,7 @@ var package_default;
21899
21914
  var init_package = __esm(() => {
21900
21915
  package_default = {
21901
21916
  name: "@nathapp/nax",
21902
- version: "0.42.6",
21917
+ version: "0.42.7",
21903
21918
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
21904
21919
  type: "module",
21905
21920
  bin: {
@@ -21972,8 +21987,8 @@ var init_version = __esm(() => {
21972
21987
  NAX_VERSION = package_default.version;
21973
21988
  NAX_COMMIT = (() => {
21974
21989
  try {
21975
- if (/^[0-9a-f]{6,10}$/.test("deb8333"))
21976
- return "deb8333";
21990
+ if (/^[0-9a-f]{6,10}$/.test("e8a2a25"))
21991
+ return "e8a2a25";
21977
21992
  } catch {}
21978
21993
  try {
21979
21994
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24452,7 +24467,7 @@ var init_constitution = __esm(() => {
24452
24467
  });
24453
24468
 
24454
24469
  // src/pipeline/stages/constitution.ts
24455
- import { dirname } from "path";
24470
+ import { dirname as dirname2 } from "path";
24456
24471
  var constitutionStage;
24457
24472
  var init_constitution2 = __esm(() => {
24458
24473
  init_constitution();
@@ -24462,7 +24477,7 @@ var init_constitution2 = __esm(() => {
24462
24477
  enabled: (ctx) => ctx.config.constitution.enabled,
24463
24478
  async execute(ctx) {
24464
24479
  const logger = getLogger();
24465
- const ngentDir = ctx.featureDir ? dirname(dirname(ctx.featureDir)) : `${ctx.workdir}/nax`;
24480
+ const ngentDir = ctx.featureDir ? dirname2(dirname2(ctx.featureDir)) : `${ctx.workdir}/nax`;
24466
24481
  const result = await loadConstitution(ngentDir, ctx.config.constitution);
24467
24482
  if (result) {
24468
24483
  ctx.constitution = result;
@@ -25259,6 +25274,7 @@ var init_story_context = __esm(() => {
25259
25274
  });
25260
25275
 
25261
25276
  // src/execution/lock.ts
25277
+ import { unlink } from "fs/promises";
25262
25278
  import path8 from "path";
25263
25279
  function getSafeLogger3() {
25264
25280
  try {
@@ -25292,7 +25308,7 @@ async function acquireLock(workdir) {
25292
25308
  });
25293
25309
  const fs2 = await import("fs/promises");
25294
25310
  await fs2.unlink(lockPath).catch(() => {});
25295
- lockData2 = undefined;
25311
+ lockData2 = null;
25296
25312
  }
25297
25313
  if (lockData2) {
25298
25314
  const lockPid = lockData2.pid;
@@ -25330,18 +25346,14 @@ async function acquireLock(workdir) {
25330
25346
  async function releaseLock(workdir) {
25331
25347
  const lockPath = path8.join(workdir, "nax.lock");
25332
25348
  try {
25333
- const file2 = Bun.file(lockPath);
25334
- const exists = await file2.exists();
25335
- if (exists) {
25336
- const proc = Bun.spawn(["rm", lockPath], { stdout: "pipe" });
25337
- await proc.exited;
25338
- await Bun.sleep(10);
25339
- }
25349
+ await unlink(lockPath);
25340
25350
  } catch (error48) {
25341
- const logger = getSafeLogger3();
25342
- logger?.warn("execution", "Failed to release lock", {
25343
- error: error48.message
25344
- });
25351
+ if (error48.code !== "ENOENT") {
25352
+ const logger = getSafeLogger3();
25353
+ logger?.warn("execution", "Failed to release lock", {
25354
+ error: error48.message
25355
+ });
25356
+ }
25345
25357
  }
25346
25358
  }
25347
25359
  var init_lock = __esm(() => {
@@ -25684,17 +25696,17 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
25684
25696
  });
25685
25697
  const gitRoot = (await new Response(topLevelProc.stdout).text()).trim();
25686
25698
  await topLevelProc.exited;
25687
- const { realpathSync: realpathSync3 } = await import("fs");
25699
+ const { realpathSync: realpathSync4 } = await import("fs");
25688
25700
  const realWorkdir = (() => {
25689
25701
  try {
25690
- return realpathSync3(workdir);
25702
+ return realpathSync4(workdir);
25691
25703
  } catch {
25692
25704
  return workdir;
25693
25705
  }
25694
25706
  })();
25695
25707
  const realGitRoot = (() => {
25696
25708
  try {
25697
- return realpathSync3(gitRoot);
25709
+ return realpathSync4(gitRoot);
25698
25710
  } catch {
25699
25711
  return gitRoot;
25700
25712
  }
@@ -26986,7 +26998,7 @@ var init_session_runner = __esm(() => {
26986
26998
  });
26987
26999
 
26988
27000
  // src/tdd/verdict-reader.ts
26989
- import { unlink } from "fs/promises";
27001
+ import { unlink as unlink2 } from "fs/promises";
26990
27002
  import path9 from "path";
26991
27003
  function isValidVerdict(obj) {
26992
27004
  if (!obj || typeof obj !== "object")
@@ -27186,7 +27198,7 @@ async function readVerdict(workdir) {
27186
27198
  async function cleanupVerdict(workdir) {
27187
27199
  const verdictPath = path9.join(workdir, VERDICT_FILE);
27188
27200
  try {
27189
- await unlink(verdictPath);
27201
+ await unlink2(verdictPath);
27190
27202
  } catch {}
27191
27203
  }
27192
27204
  var VERDICT_FILE = ".nax-verifier-verdict.json";
@@ -30912,14 +30924,15 @@ function installSignalHandlers(ctx) {
30912
30924
  process.on("SIGINT", sigintHandler);
30913
30925
  process.on("SIGHUP", sighupHandler);
30914
30926
  process.on("uncaughtException", uncaughtExceptionHandler);
30915
- process.on("unhandledRejection", (reason) => unhandledRejectionHandler(reason));
30927
+ const rejectionWrapper = (reason) => unhandledRejectionHandler(reason);
30928
+ process.on("unhandledRejection", rejectionWrapper);
30916
30929
  logger?.debug("crash-recovery", "Signal handlers installed");
30917
30930
  return () => {
30918
30931
  process.removeListener("SIGTERM", sigtermHandler);
30919
30932
  process.removeListener("SIGINT", sigintHandler);
30920
30933
  process.removeListener("SIGHUP", sighupHandler);
30921
30934
  process.removeListener("uncaughtException", uncaughtExceptionHandler);
30922
- process.removeListener("unhandledRejection", (reason) => unhandledRejectionHandler(reason));
30935
+ process.removeListener("unhandledRejection", rejectionWrapper);
30923
30936
  logger?.debug("crash-recovery", "Signal handlers unregistered");
30924
30937
  };
30925
30938
  }
@@ -33695,7 +33708,7 @@ var init_sequential_executor = __esm(() => {
33695
33708
  });
33696
33709
 
33697
33710
  // src/execution/status-file.ts
33698
- import { rename, unlink as unlink2 } from "fs/promises";
33711
+ import { rename, unlink as unlink3 } from "fs/promises";
33699
33712
  import { resolve as resolve8 } from "path";
33700
33713
  function countProgress(prd) {
33701
33714
  const stories = prd.userStories;
@@ -33744,7 +33757,7 @@ async function writeStatusFile(filePath, status) {
33744
33757
  }
33745
33758
  const tmpPath = `${resolvedPath}.tmp`;
33746
33759
  try {
33747
- await unlink2(tmpPath);
33760
+ await unlink3(tmpPath);
33748
33761
  } catch {}
33749
33762
  await Bun.write(tmpPath, JSON.stringify(status, null, 2));
33750
33763
  await rename(tmpPath, resolvedPath);
@@ -66140,7 +66153,7 @@ import { join as join13 } from "path";
66140
66153
  // src/commands/common.ts
66141
66154
  init_path_security2();
66142
66155
  init_errors3();
66143
- import { existsSync as existsSync10, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
66156
+ import { existsSync as existsSync10, readdirSync as readdirSync2, realpathSync as realpathSync3 } from "fs";
66144
66157
  import { join as join11, resolve as resolve6 } from "path";
66145
66158
  function resolveProject(options = {}) {
66146
66159
  const { dir, feature } = options;
@@ -66148,7 +66161,7 @@ function resolveProject(options = {}) {
66148
66161
  let naxDir;
66149
66162
  let configPath;
66150
66163
  if (dir) {
66151
- projectRoot = realpathSync2(resolve6(dir));
66164
+ projectRoot = realpathSync3(resolve6(dir));
66152
66165
  naxDir = join11(projectRoot, "nax");
66153
66166
  if (!existsSync10(naxDir)) {
66154
66167
  throw new NaxError(`Directory does not contain a nax project: ${projectRoot}
@@ -66207,7 +66220,7 @@ function findProjectRoot(startDir) {
66207
66220
  const naxDir = join11(current, "nax");
66208
66221
  const configPath = join11(naxDir, "config.json");
66209
66222
  if (existsSync10(configPath)) {
66210
- return realpathSync2(current);
66223
+ return realpathSync3(current);
66211
66224
  }
66212
66225
  const parent = join11(current, "..");
66213
66226
  if (parent === current) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.42.6",
3
+ "version": "0.42.7",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -254,6 +254,7 @@ export class SpawnAcpClient implements AcpClient {
254
254
  private readonly model: string;
255
255
  private readonly cwd: string;
256
256
  private readonly timeoutSeconds: number;
257
+ private readonly permissionMode: string;
257
258
  private readonly env: Record<string, string | undefined>;
258
259
  private readonly pidRegistry?: PidRegistry;
259
260
 
@@ -270,6 +271,7 @@ export class SpawnAcpClient implements AcpClient {
270
271
  this.agentName = lastToken;
271
272
  this.cwd = cwd || process.cwd();
272
273
  this.timeoutSeconds = timeoutSeconds || 1800;
274
+ this.permissionMode = "approve-reads";
273
275
  this.env = buildAllowedEnv();
274
276
  this.pidRegistry = pidRegistry;
275
277
  }
@@ -326,7 +328,7 @@ export class SpawnAcpClient implements AcpClient {
326
328
  cwd: this.cwd,
327
329
  model: this.model,
328
330
  timeoutSeconds: this.timeoutSeconds,
329
- permissionMode: "approve-all", // Default for resumed sessions
331
+ permissionMode: this.permissionMode,
330
332
  env: this.env,
331
333
  pidRegistry: this.pidRegistry,
332
334
  });
@@ -134,7 +134,8 @@ export function installSignalHandlers(ctx: SignalHandlerContext): () => void {
134
134
  process.on("SIGINT", sigintHandler);
135
135
  process.on("SIGHUP", sighupHandler);
136
136
  process.on("uncaughtException", uncaughtExceptionHandler);
137
- process.on("unhandledRejection", (reason) => unhandledRejectionHandler(reason));
137
+ const rejectionWrapper = (reason: unknown) => unhandledRejectionHandler(reason);
138
+ process.on("unhandledRejection", rejectionWrapper);
138
139
 
139
140
  logger?.debug("crash-recovery", "Signal handlers installed");
140
141
 
@@ -143,7 +144,7 @@ export function installSignalHandlers(ctx: SignalHandlerContext): () => void {
143
144
  process.removeListener("SIGINT", sigintHandler);
144
145
  process.removeListener("SIGHUP", sighupHandler);
145
146
  process.removeListener("uncaughtException", uncaughtExceptionHandler);
146
- process.removeListener("unhandledRejection", (reason) => unhandledRejectionHandler(reason));
147
+ process.removeListener("unhandledRejection", rejectionWrapper);
147
148
  logger?.debug("crash-recovery", "Signal handlers unregistered");
148
149
  };
149
150
  }
@@ -5,6 +5,7 @@
5
5
  * Prevents concurrent runs in the same directory.
6
6
  */
7
7
 
8
+ import { unlink } from "node:fs/promises";
8
9
  import path from "node:path";
9
10
  import { getLogger } from "../logger";
10
11
 
@@ -49,7 +50,7 @@ export async function acquireLock(workdir: string): Promise<boolean> {
49
50
  if (exists) {
50
51
  // Read lock data
51
52
  const lockContent = await lockFile.text();
52
- let lockData: { pid: number };
53
+ let lockData: { pid: number } | null;
53
54
  try {
54
55
  lockData = JSON.parse(lockContent);
55
56
  } catch {
@@ -61,7 +62,7 @@ export async function acquireLock(workdir: string): Promise<boolean> {
61
62
  const fs = await import("node:fs/promises");
62
63
  await fs.unlink(lockPath).catch(() => {});
63
64
  // Fall through to create a new lock
64
- lockData = undefined as unknown as { pid: number };
65
+ lockData = null;
65
66
  }
66
67
 
67
68
  if (lockData) {
@@ -88,6 +89,7 @@ export async function acquireLock(workdir: string): Promise<boolean> {
88
89
  pid: process.pid,
89
90
  timestamp: Date.now(),
90
91
  };
92
+ // NOTE: Node.js fs used intentionally — Bun.file()/Bun.write() lacks O_CREAT|O_EXCL atomic exclusive create
91
93
  const fs = await import("node:fs");
92
94
  const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o644);
93
95
  fs.writeSync(fd, JSON.stringify(lockData));
@@ -114,18 +116,14 @@ export async function acquireLock(workdir: string): Promise<boolean> {
114
116
  export async function releaseLock(workdir: string): Promise<void> {
115
117
  const lockPath = path.join(workdir, "nax.lock");
116
118
  try {
117
- const file = Bun.file(lockPath);
118
- const exists = await file.exists();
119
- if (exists) {
120
- const proc = Bun.spawn(["rm", lockPath], { stdout: "pipe" });
121
- await proc.exited;
122
- // Wait a bit for filesystem to sync (prevents race in tests)
123
- await Bun.sleep(10);
124
- }
119
+ await unlink(lockPath);
125
120
  } catch (error) {
126
- const logger = getSafeLogger();
127
- logger?.warn("execution", "Failed to release lock", {
128
- error: (error as Error).message,
129
- });
121
+ // Ignore ENOENT (already gone), log others
122
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
123
+ const logger = getSafeLogger();
124
+ logger?.warn("execution", "Failed to release lock", {
125
+ error: (error as Error).message,
126
+ });
127
+ }
130
128
  }
131
129
  }
@@ -2,7 +2,8 @@
2
2
  * Path security utilities for nax (SEC-1, SEC-2).
3
3
  */
4
4
 
5
- import { isAbsolute, join, normalize, resolve } from "node:path";
5
+ import { realpathSync } from "node:fs";
6
+ import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
6
7
 
7
8
  /**
8
9
  * Result of a path validation.
@@ -23,16 +24,32 @@ export interface PathValidationResult {
23
24
  * @param allowedRoots - Array of absolute paths that are allowed as roots
24
25
  * @returns Validation result
25
26
  */
27
+ /** Resolve symlinks for a path that may not exist yet (fall back to parent dir). */
28
+ function safeRealpath(p: string): string {
29
+ try {
30
+ return realpathSync(p);
31
+ } catch {
32
+ // Path doesn't exist — resolve the parent directory instead
33
+ try {
34
+ const parent = realpathSync(dirname(p));
35
+ return join(parent, p.split("/").pop() ?? "");
36
+ } catch {
37
+ return p;
38
+ }
39
+ }
40
+ }
41
+
26
42
  export function validateModulePath(modulePath: string, allowedRoots: string[]): PathValidationResult {
27
43
  if (!modulePath) {
28
44
  return { valid: false, error: "Module path is empty" };
29
45
  }
30
46
 
31
- const normalizedRoots = allowedRoots.map((r) => resolve(r));
47
+ // Resolve symlinks in each root
48
+ const normalizedRoots = allowedRoots.map((r) => safeRealpath(resolve(r)));
32
49
 
33
50
  // If absolute, just check against roots
34
51
  if (isAbsolute(modulePath)) {
35
- const absoluteTarget = normalize(modulePath);
52
+ const absoluteTarget = safeRealpath(normalize(modulePath));
36
53
  const isWithin = normalizedRoots.some((root) => {
37
54
  return absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root;
38
55
  });
@@ -42,7 +59,7 @@ export function validateModulePath(modulePath: string, allowedRoots: string[]):
42
59
  } else {
43
60
  // If relative, check if it's within any root when resolved relative to that root
44
61
  for (const root of normalizedRoots) {
45
- const absoluteTarget = resolve(join(root, modulePath));
62
+ const absoluteTarget = safeRealpath(resolve(join(root, modulePath)));
46
63
  if (absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root) {
47
64
  return { valid: true, absolutePath: absoluteTarget };
48
65
  }