@nexrall/code-core 1.4.30 → 1.4.32

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.
@@ -1,5 +1,7 @@
1
1
  import type { Message, ToolResult, AgentLoopOptions, EnvContext } from '../types';
2
2
  import { type AgentMemoryScope } from './agentTypes';
3
+ import { bashNeedsRepoLock } from '../permissions/bashClassify';
4
+ export { bashNeedsRepoLock };
3
5
  /**
4
6
  * Fingerprint one round's tool failures, for the repeated-failure runaway guard.
5
7
  *
@@ -85,12 +87,35 @@ export declare function stopReasonNotice(reason: StopReason, ctx?: {
85
87
  repeatError?: string | null;
86
88
  }): string | null;
87
89
  export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
90
+ /**
91
+ * Every path a tool call will touch, so all of them can be locked.
92
+ *
93
+ * `move_file`/`copy_file` carry `source` + `destination`, the rest carry `path`. Both
94
+ * `dest` and `destination` are accepted because the schema has used both spellings and
95
+ * silently missing the key would mean silently losing the lock — a failure that shows
96
+ * up as corrupted content rather than an error.
97
+ *
98
+ * Exported for tests: getting this wrong is invisible until two writes race.
99
+ */
100
+ export declare function lockPathsFor(name: string, input: Record<string, unknown>, workDir?: string): string[];
101
+ /**
102
+ * Resolve the fan-out limit: env → settings.json → default.
103
+ *
104
+ * Was env-ONLY, the same gap as the sub-task timeout: a project on a small machine that
105
+ * wanted 2, or a big one that wanted 6, had to set a shell variable, and nobody reading
106
+ * settings.json could see what the limit even was. Capped at 16 because this bounds real
107
+ * shared resources (CPU, the API rate limit, file handles) and a typo like 400 should
108
+ * degrade to "a lot" rather than fork-bomb the machine.
109
+ */
110
+ export declare function resolveMaxConcurrentSubtasks(settingsRaw?: Record<string, unknown>): number;
88
111
  /**
89
112
  * Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
90
113
  * the CLI ships as a single esbuild bundle with no node_modules, and this is a
91
114
  * dozen lines.
92
115
  */
93
116
  export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
117
+ /** Test-only: forget the memoised limiter so a new limit can take effect. */
118
+ export declare function _resetSubTaskLimiter(): void;
94
119
  /**
95
120
  * How long a sub-agent may make NO progress before it is stopped.
96
121
  *
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -33,12 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = void 0;
36
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports.bashNeedsRepoLock = void 0;
37
37
  exports.errorRoundSignature = errorRoundSignature;
38
38
  exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
39
39
  exports.stopReasonNotice = stopReasonNotice;
40
40
  exports.resolveMaxIterations = resolveMaxIterations;
41
+ exports.lockPathsFor = lockPathsFor;
42
+ exports.resolveMaxConcurrentSubtasks = resolveMaxConcurrentSubtasks;
41
43
  exports.createLimiter = createLimiter;
44
+ exports._resetSubTaskLimiter = _resetSubTaskLimiter;
42
45
  exports.resolveSubtaskTimeoutMs = resolveSubtaskTimeoutMs;
43
46
  exports.extractSubTaskText = extractSubTaskText;
44
47
  exports.capSubTaskText = capSubTaskText;
@@ -64,6 +67,11 @@ const executor_1 = require("../tools/executor");
64
67
  const agentTypes_1 = require("./agentTypes");
65
68
  const skills_1 = require("./skills");
66
69
  const rules_1 = require("../permissions/rules");
70
+ // bashNeedsRepoLock lives in permissions/bashClassify.ts, not here: the permission
71
+ // gate needs the same "does this command mutate shared state?" answer, and importing
72
+ // it from loop.ts would make permissions depend on the agent loop (a cycle).
73
+ const bashClassify_1 = require("../permissions/bashClassify");
74
+ Object.defineProperty(exports, "bashNeedsRepoLock", { enumerable: true, get: function () { return bashClassify_1.bashNeedsRepoLock; } });
67
75
  const planMode_1 = require("./planMode");
68
76
  const agentRegistry_1 = require("./agentRegistry");
69
77
  const sandbox_1 = require("../tools/sandbox");
@@ -387,6 +395,57 @@ async function withFileLock(absPath, fn) {
387
395
  _fileLocks.delete(absPath);
388
396
  }
389
397
  }
398
+ /**
399
+ * Take several locks at once, always in a globally consistent order.
400
+ *
401
+ * Needed because move_file/copy_file touch TWO paths. Locking only one of them (the
402
+ * old behaviour locked `source` and left `dest` unprotected) leaves exactly the race
403
+ * the lock exists to prevent: a `move_file{dest:'shared.ts'}` running concurrently with
404
+ * an `edit_file{path:'shared.ts'}` had nothing serialising them.
405
+ *
406
+ * The sort is load-bearing, not tidiness: two callers acquiring {A,B} and {B,A} at the
407
+ * same time would deadlock, each holding what the other waits for. Sorting means every
408
+ * caller in the process takes them in the same order, which makes that impossible.
409
+ */
410
+ async function withFileLocks(absPaths, fn) {
411
+ const unique = [...new Set(absPaths.filter(Boolean))].sort();
412
+ if (unique.length === 0)
413
+ return fn();
414
+ const [first, ...rest] = unique;
415
+ return withFileLock(first, () => (rest.length ? withFileLocks(rest, fn) : fn()));
416
+ }
417
+ /** Tools that mutate the filesystem and must be serialised per path. */
418
+ const WRITE_TOOLS = new Set([
419
+ 'write_file', 'edit_file', 'multi_edit', 'delete_file', 'move_file', 'copy_file', 'notebook_edit',
420
+ ]);
421
+ /**
422
+ * Shared lock key for bash commands that mutate repository-wide state.
423
+ *
424
+ * Not a path, because these commands do not declare one — `git commit` contends over
425
+ * `.git/index.lock`, `npm install` over `node_modules` and the lockfile. One key means
426
+ * they serialise against each other while everything else stays parallel.
427
+ */
428
+ const REPO_STATE_LOCK = '\u0000repo-state';
429
+ /**
430
+ * Every path a tool call will touch, so all of them can be locked.
431
+ *
432
+ * `move_file`/`copy_file` carry `source` + `destination`, the rest carry `path`. Both
433
+ * `dest` and `destination` are accepted because the schema has used both spellings and
434
+ * silently missing the key would mean silently losing the lock — a failure that shows
435
+ * up as corrupted content rather than an error.
436
+ *
437
+ * Exported for tests: getting this wrong is invisible until two writes race.
438
+ */
439
+ function lockPathsFor(name, input, workDir) {
440
+ if (!WRITE_TOOLS.has(name))
441
+ return [];
442
+ const raw = [input.path, input.source, input.destination, input.dest]
443
+ .filter((p) => typeof p === 'string' && p.length > 0);
444
+ const abs = raw.map((p) => (workDir ? path.resolve(workDir, p) : p));
445
+ // Fall back to the tool name so a malformed call still serialises against itself
446
+ // rather than escaping the lock entirely.
447
+ return abs.length ? abs : [name];
448
+ }
390
449
  // ─── Sub-agent fan-out limiter ────────────────────────────────────────────────
391
450
  //
392
451
  // Tool calls in one turn run via Promise.all with no ceiling. For ordinary tools
@@ -407,10 +466,26 @@ async function withFileLock(absPath, fn) {
407
466
  //
408
467
  // This is a QUEUE, not a rejection: every sub-task still runs, just at most N at a
409
468
  // time. Failing the excess would be worse than serialising it.
410
- const MAX_CONCURRENT_SUBTASKS = (() => {
411
- const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
412
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
413
- })();
469
+ const DEFAULT_MAX_CONCURRENT_SUBTASKS = 4;
470
+ /**
471
+ * Resolve the fan-out limit: env → settings.json default.
472
+ *
473
+ * Was env-ONLY, the same gap as the sub-task timeout: a project on a small machine that
474
+ * wanted 2, or a big one that wanted 6, had to set a shell variable, and nobody reading
475
+ * settings.json could see what the limit even was. Capped at 16 because this bounds real
476
+ * shared resources (CPU, the API rate limit, file handles) and a typo like 400 should
477
+ * degrade to "a lot" rather than fork-bomb the machine.
478
+ */
479
+ function resolveMaxConcurrentSubtasks(settingsRaw = {}) {
480
+ const clamp = (n) => Math.min(Math.floor(n), 16);
481
+ const fromEnv = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
482
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
483
+ return clamp(fromEnv);
484
+ const fromSettings = Number(settingsRaw.maxConcurrentSubtasks);
485
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
486
+ return clamp(fromSettings);
487
+ return DEFAULT_MAX_CONCURRENT_SUBTASKS;
488
+ }
414
489
  /**
415
490
  * Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
416
491
  * the CLI ships as a single esbuild bundle with no node_modules, and this is a
@@ -438,7 +513,28 @@ function createLimiter(max) {
438
513
  // Process-wide, deliberately: the limit exists to protect shared resources (CPU,
439
514
  // the API rate limit, file handles), and those are shared across every concurrent
440
515
  // turn in this process, not just the tool calls of one message.
441
- const _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
516
+ //
517
+ // Built LAZILY on first use rather than at module load, because the limit can now come
518
+ // from settings.json and the workspace is not known when this module is imported.
519
+ // Once created it is reused for the process lifetime — rebuilding it per turn would
520
+ // reset `active` and let the ceiling be exceeded, which is worse than not honouring a
521
+ // mid-session settings change.
522
+ let _subTaskLimitInstance = null;
523
+ let _subTaskLimitMax = 0;
524
+ /** Sub-tasks dispatched but not yet finished — used only to detect queueing. */
525
+ let _inFlightSubTasks = 0;
526
+ function subTaskLimiter(workDir) {
527
+ if (!_subTaskLimitInstance) {
528
+ _subTaskLimitMax = resolveMaxConcurrentSubtasks(workDir ? (0, rules_1.loadSettings)(workDir).raw : {});
529
+ _subTaskLimitInstance = createLimiter(_subTaskLimitMax);
530
+ }
531
+ return { run: _subTaskLimitInstance, max: _subTaskLimitMax };
532
+ }
533
+ /** Test-only: forget the memoised limiter so a new limit can take effect. */
534
+ function _resetSubTaskLimiter() {
535
+ _subTaskLimitInstance = null;
536
+ _subTaskLimitMax = 0;
537
+ }
442
538
  // ─── Human-readable tool descriptions ────────────────────────────────────────
443
539
  function humanDescription(name, input) {
444
540
  switch (name) {
@@ -2426,9 +2522,30 @@ async function runAgentLoop(initialMessages, options) {
2426
2522
  // waiting on a child that can never be scheduled, the run would wedge
2427
2523
  // permanently. Keeping the limiter acyclic (a holder never re-enters it)
2428
2524
  // means raising MAX_TASK_DEPTH later can't silently reintroduce that.
2429
- result = depth === 0
2430
- ? await _subTaskLimit(() => runSubTask(input, options, agentTypes))
2431
- : await runSubTask(input, options, agentTypes);
2525
+ if (depth === 0) {
2526
+ const { run: limitRun, max: limitMax } = subTaskLimiter(options.workDir);
2527
+ // Tell the user when a sub-task is WAITING rather than working.
2528
+ //
2529
+ // With a burst of 8 and a ceiling of 4, the last four sat silently in the
2530
+ // queue. From the outside they looked started, so a long wait read as a
2531
+ // hang — and the fix for a hang (Ctrl+C and retry) is exactly wrong here,
2532
+ // since the work was about to run. `_inFlightSubTasks` is only ever
2533
+ // touched from this single dispatch point, so the count is exact.
2534
+ if (_inFlightSubTasks >= limitMax) {
2535
+ (options.onNotice ?? options.onText)(`\u23f3 Queued: ${limitMax} sub-agents are already running, so this one starts when a ` +
2536
+ `slot frees up (raise "maxConcurrentSubtasks" in .nexrall/settings.json to widen it).`);
2537
+ }
2538
+ _inFlightSubTasks++;
2539
+ try {
2540
+ result = await limitRun(() => runSubTask(input, options, agentTypes));
2541
+ }
2542
+ finally {
2543
+ _inFlightSubTasks--;
2544
+ }
2545
+ }
2546
+ else {
2547
+ result = await runSubTask(input, options, agentTypes);
2548
+ }
2432
2549
  }
2433
2550
  else {
2434
2551
  const pre = runToolHooks(hooks.PreToolUse, 'PreToolUse', name, input, options.workDir);
@@ -2462,22 +2579,28 @@ async function runAgentLoop(initialMessages, options) {
2462
2579
  else {
2463
2580
  // Snapshot pre-mutation state so the user can /rewind this turn.
2464
2581
  options.checkpointManager?.recordBeforeMutation(name, input);
2465
- // Serialise writes to the same file: if the model emits two
2466
- // edit_file/write_file calls for the same path in one turn
2467
- // (executed via Promise.all), the second would overwrite the
2468
- // first. withFileLock queues them so they apply sequentially.
2469
- const WRITE_TOOLS = new Set(['write_file', 'edit_file', 'multi_edit', 'delete_file', 'move_file', 'copy_file', 'notebook_edit']);
2470
2582
  const onStream = options.onToolStreamChunk
2471
2583
  ? (chunk) => options.onToolStreamChunk(name, chunk)
2472
2584
  : undefined;
2473
- if (WRITE_TOOLS.has(name)) {
2474
- const targetPath = (typeof input.path === 'string' ? input.path : typeof input.source === 'string' ? input.source : '');
2475
- const absTarget = targetPath && options.workDir ? path.resolve(options.workDir, targetPath) : targetPath;
2476
- result = await withFileLock(absTarget || name, () => (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope, onStream));
2477
- }
2478
- else {
2479
- result = await (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope, onStream);
2480
- }
2585
+ const run = () => (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope, onStream);
2586
+ // Serialise anything that would otherwise race. Two sources of races:
2587
+ //
2588
+ // • Writes to the SAME PATH the model emits multiple tool_use blocks
2589
+ // per turn and they run via Promise.all, so two edit_file calls on
2590
+ // one file would both read the original and the second would discard
2591
+ // the first. Now ALL paths a call touches are locked (move_file and
2592
+ // copy_file have two), in a globally consistent order.
2593
+ // • Repo-wide state via bash — `git commit`, `npm install`, `npm test`.
2594
+ // These declare no path, so they take one shared lock. Read-only
2595
+ // commands (grep, ls, cat) are excluded and stay parallel, which is
2596
+ // what an explorer agent spends nearly all its calls on.
2597
+ //
2598
+ // This matters far more with sub-agents than without: up to 4 run
2599
+ // concurrently, each issuing its own tool calls into this same process.
2600
+ const locks = name === 'bash'
2601
+ ? ((0, bashClassify_1.bashNeedsRepoLock)(String(input.command ?? '')) ? [REPO_STATE_LOCK] : [])
2602
+ : lockPathsFor(name, input, options.workDir);
2603
+ result = locks.length ? await withFileLocks(locks, run) : await run();
2481
2604
  }
2482
2605
  }
2483
2606
  catch (err) {
package/dist/index.d.ts CHANGED
@@ -20,6 +20,8 @@ export * from './agent/agentTypes';
20
20
  export * from './agent/planMode';
21
21
  export * from './agent/agentRegistry';
22
22
  export * from './permissions/rules';
23
+ export * from './permissions/modePolicy';
24
+ export * from './permissions/bashClassify';
23
25
  export * from './permissions/destructive';
24
26
  export * from './plugins/index';
25
27
  export * from './plugins/installer';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ __exportStar(require("./agent/agentTypes"), exports);
36
36
  __exportStar(require("./agent/planMode"), exports);
37
37
  __exportStar(require("./agent/agentRegistry"), exports);
38
38
  __exportStar(require("./permissions/rules"), exports);
39
+ __exportStar(require("./permissions/modePolicy"), exports);
40
+ __exportStar(require("./permissions/bashClassify"), exports);
39
41
  __exportStar(require("./permissions/destructive"), exports);
40
42
  __exportStar(require("./plugins/index"), exports);
41
43
  __exportStar(require("./plugins/installer"), exports);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether a bash command should take the repo-state lock.
3
+ *
4
+ * Exported for tests: the failure mode (two concurrent `git commit`) is timing-dependent
5
+ * and painful to reproduce through the real loop.
6
+ *
7
+ * Scans EVERY segment, not just the first word, because the mutating verb is routinely
8
+ * not leading: `cd packages/core && npm publish`, `FOO=1 git push`, `ls && git commit`.
9
+ * Checking only the first token would miss most real commands.
10
+ */
11
+ export declare function bashNeedsRepoLock(command: string): boolean;
12
+ //# sourceMappingURL=bashClassify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bashClassify.d.ts","sourceRoot":"","sources":["../../src/permissions/bashClassify.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkB1D"}
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // ─── Classifying shell commands ───────────────────────────────────────────────
3
+ //
4
+ // Lives in permissions/ rather than agent/loop.ts because TWO subsystems need the same
5
+ // answer to "does this command mutate shared state?":
6
+ //
7
+ // • the concurrency locks — two `git commit` must not interleave;
8
+ // • the permission gate — `auto` mode runs `npm test` without asking but must still
9
+ // prompt for anything with wider side effects.
10
+ //
11
+ // Keeping one classifier means those can never disagree. Two copies would be a second
12
+ // source of truth that has to stay in sync forever, which is the exact class of bug this
13
+ // codebase keeps turning up (a prompt promising what the gate didn't enforce; a mode the
14
+ // gate never read).
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.bashNeedsRepoLock = bashNeedsRepoLock;
17
+ /**
18
+ * Commands that must not run concurrently with another instance of themselves.
19
+ *
20
+ * `bash` was excluded from the write-tool lock entirely, which was the largest hole in
21
+ * the concurrency story: it is the most mutating tool there is, and with 4 sub-agents
22
+ * fanned out two of them could run `git commit` simultaneously (one dies on
23
+ * `.git/index.lock`), or two `npm install` (corrupted node_modules / lockfile), or two
24
+ * `npm test` writing the same coverage and snapshot files (wrong results, not an error).
25
+ * The comment above withFileLock explains carefully why two edit_file calls must be
26
+ * sequenced, then a `bash -c 'echo x > file'` did exactly that with nothing holding it.
27
+ *
28
+ * Deliberately a VERB ALLOWLIST rather than serialising all of bash: an explorer's
29
+ * `grep`/`rg`/`ls`/`cat` are read-only and are precisely the calls that benefit most
30
+ * from running in parallel. Serialising those would trade a real correctness bug for a
31
+ * real performance regression. These verbs are the ones that take a global lock, write
32
+ * a lockfile, or mutate a shared build/dependency directory.
33
+ */
34
+ const MUTATING_BASH_VERBS = new Set([
35
+ 'git', 'npm', 'pnpm', 'yarn', 'bun', 'cargo', 'go', 'pip', 'pip3', 'poetry', 'uv',
36
+ 'bundle', 'composer', 'gradle', 'mvn', 'make', 'terraform', 'helm', 'docker',
37
+ 'vsce', 'tsc', 'pytest', 'dotnet', 'swift', 'gem',
38
+ ]);
39
+ /**
40
+ * Whether a bash command should take the repo-state lock.
41
+ *
42
+ * Exported for tests: the failure mode (two concurrent `git commit`) is timing-dependent
43
+ * and painful to reproduce through the real loop.
44
+ *
45
+ * Scans EVERY segment, not just the first word, because the mutating verb is routinely
46
+ * not leading: `cd packages/core && npm publish`, `FOO=1 git push`, `ls && git commit`.
47
+ * Checking only the first token would miss most real commands.
48
+ */
49
+ function bashNeedsRepoLock(command) {
50
+ if (!command)
51
+ return false;
52
+ // Split on shell separators, then take the first bare word of each segment, skipping
53
+ // env-var assignments (FOO=bar git push) and `sudo`/`time`-style prefixes.
54
+ const segments = command.split(/\|\||&&|[;|&\n()]|\$\(/);
55
+ for (const seg of segments) {
56
+ for (const tokenRaw of seg.trim().split(/\s+/)) {
57
+ const token = tokenRaw.replace(/^["']|["']$/g, '');
58
+ if (!token)
59
+ continue;
60
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
61
+ continue; // env assignment
62
+ if (token === 'sudo' || token === 'time' || token === 'nice' || token === 'env')
63
+ continue;
64
+ // Use the basename so `/usr/bin/git` and `./node_modules/.bin/tsc` still match.
65
+ const verb = token.split('/').pop() ?? token;
66
+ if (MUTATING_BASH_VERBS.has(verb))
67
+ return true;
68
+ break; // only the command word of each segment matters
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+ //# sourceMappingURL=bashClassify.js.map
@@ -0,0 +1,55 @@
1
+ /** Permission modes, in increasing order of autonomy. */
2
+ export type PermissionMode = 'plan' | 'ask' | 'edit' | 'auto';
3
+ /** What the gate should do with one tool call. */
4
+ export type PolicyDecision = 'allow' | 'ask' | 'deny' | 'confirm';
5
+ export interface PolicyInput {
6
+ tool: string;
7
+ input: Record<string, unknown>;
8
+ mode: PermissionMode;
9
+ /** True when the user has already opted into blanket approval (--yolo / bypass). */
10
+ bypass?: boolean;
11
+ /** True when this call is irreversible (DB drop, force-push, rm -rf …). */
12
+ destructive?: boolean;
13
+ }
14
+ /**
15
+ * Tools that only ever READ. Always allowed, in every mode including plan.
16
+ *
17
+ * Kept here rather than in each client so "read-only" cannot mean two different things
18
+ * in two windows. Note `memory_write` is NOT here even though the CLI's old list had it
19
+ * — it mutates durable cross-session state, and listing it under "read-only / safe"
20
+ * meant it silently bypassed plan mode's read-only guarantee.
21
+ */
22
+ export declare const READ_ONLY_TOOLS: Set<string>;
23
+ /** Tools that write to the filesystem. */
24
+ export declare const FILE_WRITE_TOOLS: Set<string>;
25
+ /** Tools that move/delete files — riskier than an in-place edit. */
26
+ export declare const FILE_MUTATE_TOOLS: Set<string>;
27
+ /** Tools that run a shell command. */
28
+ export declare const BASH_TOOLS: Set<string>;
29
+ /**
30
+ * Is this bash command read-only?
31
+ *
32
+ * Deliberately delegates to `bashNeedsRepoLock`, the classifier written for the
33
+ * concurrency work: it already distinguishes commands that mutate shared state
34
+ * (`git`, `npm`, `cargo`, `make` …) from ones that only read (`grep`, `ls`, `cat`,
35
+ * `rg`), scanning every shell segment so `cd x && npm publish` is caught.
36
+ *
37
+ * Reusing it is the point. Writing a second classifier here would create two
38
+ * definitions of "mutating" that must agree forever — the exact failure this whole
39
+ * audit keeps finding. If the lock treats a command as mutating, so does the gate.
40
+ */
41
+ export declare function isReadOnlyBash(command: string): boolean;
42
+ /**
43
+ * The single source of truth for "should we prompt?".
44
+ *
45
+ * Order matters and mirrors Claude Code's: destructive actions are checked BEFORE any
46
+ * blanket approval, so an irreversible command can never ride `--yolo` or `auto`
47
+ * straight to execution. That ordering has a body count in the industry (dropped
48
+ * production databases), and it is the one rule no convenience setting may override.
49
+ */
50
+ export declare function decide(req: PolicyInput): PolicyDecision;
51
+ /** Human-readable one-liner for the footer / status bar. */
52
+ export declare function describeMode(mode: PermissionMode): string;
53
+ /** Normalise arbitrary input to a known mode, defaulting to the safe middle. */
54
+ export declare function parseMode(raw: unknown): PermissionMode;
55
+ //# sourceMappingURL=modePolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modePolicy.d.ts","sourceRoot":"","sources":["../../src/permissions/modePolicy.ts"],"names":[],"mappings":"AAsBA,yDAAyD;AACzD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,kDAAkD;AAClD,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,KAAK,GACL,MAAM,GACN,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,cAAc,CAAC;IACrB,oFAAoF;IACpF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,aAU1B,CAAC;AAEH,0CAA0C;AAC1C,eAAO,MAAM,gBAAgB,aAE3B,CAAC;AAEH,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,aAE5B,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,UAAU,aAAsD,CAAC;AAE9E;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,cAAc,CAmDvD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAOzD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAGtD"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASH_TOOLS = exports.FILE_MUTATE_TOOLS = exports.FILE_WRITE_TOOLS = exports.READ_ONLY_TOOLS = void 0;
4
+ exports.isReadOnlyBash = isReadOnlyBash;
5
+ exports.decide = decide;
6
+ exports.describeMode = describeMode;
7
+ exports.parseMode = parseMode;
8
+ const bashClassify_1 = require("./bashClassify");
9
+ /**
10
+ * Tools that only ever READ. Always allowed, in every mode including plan.
11
+ *
12
+ * Kept here rather than in each client so "read-only" cannot mean two different things
13
+ * in two windows. Note `memory_write` is NOT here even though the CLI's old list had it
14
+ * — it mutates durable cross-session state, and listing it under "read-only / safe"
15
+ * meant it silently bypassed plan mode's read-only guarantee.
16
+ */
17
+ exports.READ_ONLY_TOOLS = new Set([
18
+ 'read_file', 'list_directory', 'search_files', 'glob', 'grep',
19
+ 'fetch_url', 'web_search',
20
+ 'bash_output', 'kill_shell',
21
+ 'todo_read', 'todo_write',
22
+ 'memory_read',
23
+ 'use_skill',
24
+ 'notebook_read',
25
+ 'get_diagnostics', 'go_to_definition', 'find_references',
26
+ 'get_symbols', 'get_workspace_symbols', 'get_hover',
27
+ ]);
28
+ /** Tools that write to the filesystem. */
29
+ exports.FILE_WRITE_TOOLS = new Set([
30
+ 'write_file', 'create_file', 'edit_file', 'multi_edit', 'notebook_edit',
31
+ ]);
32
+ /** Tools that move/delete files — riskier than an in-place edit. */
33
+ exports.FILE_MUTATE_TOOLS = new Set([
34
+ 'delete_file', 'move_file', 'copy_file',
35
+ ]);
36
+ /** Tools that run a shell command. */
37
+ exports.BASH_TOOLS = new Set(['bash', 'run_command', 'execute_command']);
38
+ /**
39
+ * Is this bash command read-only?
40
+ *
41
+ * Deliberately delegates to `bashNeedsRepoLock`, the classifier written for the
42
+ * concurrency work: it already distinguishes commands that mutate shared state
43
+ * (`git`, `npm`, `cargo`, `make` …) from ones that only read (`grep`, `ls`, `cat`,
44
+ * `rg`), scanning every shell segment so `cd x && npm publish` is caught.
45
+ *
46
+ * Reusing it is the point. Writing a second classifier here would create two
47
+ * definitions of "mutating" that must agree forever — the exact failure this whole
48
+ * audit keeps finding. If the lock treats a command as mutating, so does the gate.
49
+ */
50
+ function isReadOnlyBash(command) {
51
+ return !(0, bashClassify_1.bashNeedsRepoLock)(command);
52
+ }
53
+ /**
54
+ * The single source of truth for "should we prompt?".
55
+ *
56
+ * Order matters and mirrors Claude Code's: destructive actions are checked BEFORE any
57
+ * blanket approval, so an irreversible command can never ride `--yolo` or `auto`
58
+ * straight to execution. That ordering has a body count in the industry (dropped
59
+ * production databases), and it is the one rule no convenience setting may override.
60
+ */
61
+ function decide(req) {
62
+ const { tool, mode } = req;
63
+ const command = String(req.input.command ?? req.input.cmd ?? '');
64
+ // 1. Reads are always fine — an agent that cannot read cannot help.
65
+ // Checked before plan mode too: plan mode is read-ONLY, not read-nothing.
66
+ if (exports.READ_ONLY_TOOLS.has(tool))
67
+ return 'allow';
68
+ if (exports.BASH_TOOLS.has(tool) && isReadOnlyBash(command) && !req.destructive)
69
+ return 'allow';
70
+ // 2. Irreversible actions ALWAYS need an explicit human "yes".
71
+ // Before bypass, before mode. `rm -rf`, `git push --force`, `DROP TABLE` are not
72
+ // things any mode should silently perform, and the strong confirm treatment (type
73
+ // the word "yes", no "approve all") is what makes it a real speed bump.
74
+ if (req.destructive)
75
+ return 'confirm';
76
+ // 3. Plan mode: refuse every remaining (i.e. mutating) tool outright.
77
+ // A refusal, not a prompt — the guarantee is "nothing changes until I approve the
78
+ // plan", and a prompt would let one distracted keypress break it.
79
+ if (mode === 'plan')
80
+ return 'deny';
81
+ // 4. Explicit blanket approval (--yolo / bypassPermissions). Deliberately after the
82
+ // destructive check above.
83
+ if (req.bypass)
84
+ return 'allow';
85
+ // 5. Mode policy for the mutating tools that remain.
86
+ if (exports.FILE_WRITE_TOOLS.has(tool) || exports.FILE_MUTATE_TOOLS.has(tool)) {
87
+ // `edit` and `auto` both pre-authorise ordinary file edits — that is the whole
88
+ // reason to leave `ask`.
89
+ return mode === 'ask' ? 'ask' : 'allow';
90
+ }
91
+ if (exports.BASH_TOOLS.has(tool)) {
92
+ // A mutating-but-recoverable command: `npm test`, `npm install`, `git commit`,
93
+ // `cargo build`. `auto` runs these, which is the behaviour its own prompt block
94
+ // already advertises. `edit` still asks, matching Claude Code's acceptEdits —
95
+ // "file changes flow, anything with side effects beyond the working directory
96
+ // still prompts" — because shell commands reach the network, global caches and
97
+ // other checkouts, not just this repo.
98
+ return mode === 'auto' ? 'allow' : 'ask';
99
+ }
100
+ // 6. Anything unrecognised — a newly added tool, an MCP tool, a plugin tool — asks,
101
+ // in EVERY mode including `auto`.
102
+ //
103
+ // Fail-CLOSED on permission, deliberately: a tool nobody has classified must not
104
+ // inherit autonomy merely because the user chose a permissive mode. An MCP server
105
+ // is third-party code whose blast radius this policy cannot see, so `auto` meaning
106
+ // "run anything, including things I've never heard of" would make the mode
107
+ // unauditable. Same principle as forcing an unparseable agent definition to
108
+ // read-only: the less we know, the less it gets.
109
+ return 'ask';
110
+ }
111
+ /** Human-readable one-liner for the footer / status bar. */
112
+ function describeMode(mode) {
113
+ switch (mode) {
114
+ case 'plan': return 'plan — read-only, produces a plan for approval';
115
+ case 'ask': return 'ask — approves reads; asks before edits and commands';
116
+ case 'edit': return 'edit — edits files freely; asks before shell commands';
117
+ case 'auto': return 'auto — edits and runs commands; still confirms irreversible actions';
118
+ }
119
+ }
120
+ /** Normalise arbitrary input to a known mode, defaulting to the safe middle. */
121
+ function parseMode(raw) {
122
+ const s = String(raw ?? '').trim().toLowerCase();
123
+ return s === 'plan' || s === 'ask' || s === 'edit' || s === 'auto' ? s : 'ask';
124
+ }
125
+ //# sourceMappingURL=modePolicy.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.30",
3
+ "version": "1.4.32",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",