@nexrall/code-core 1.4.31 → 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.
- package/dist/agent/loop.d.ts +2 -11
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +7 -59
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/permissions/bashClassify.d.ts +12 -0
- package/dist/permissions/bashClassify.d.ts.map +1 -0
- package/dist/permissions/bashClassify.js +73 -0
- package/dist/permissions/modePolicy.d.ts +55 -0
- package/dist/permissions/modePolicy.d.ts.map +1 -0
- package/dist/permissions/modePolicy.js +125 -0
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -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,17 +87,6 @@ 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;
|
|
88
|
-
/**
|
|
89
|
-
* Whether a bash command should take the repo-state lock.
|
|
90
|
-
*
|
|
91
|
-
* Exported for tests: the failure mode (two concurrent `git commit`) is timing-dependent
|
|
92
|
-
* and painful to reproduce through the real loop.
|
|
93
|
-
*
|
|
94
|
-
* Scans EVERY segment, not just the first word, because the mutating verb is routinely
|
|
95
|
-
* not leading: `cd packages/core && npm publish`, `FOO=1 git push`, `ls && git commit`.
|
|
96
|
-
* Checking only the first token would miss most real commands.
|
|
97
|
-
*/
|
|
98
|
-
export declare function bashNeedsRepoLock(command: string): boolean;
|
|
99
90
|
/**
|
|
100
91
|
* Every path a tool call will touch, so all of them can be locked.
|
|
101
92
|
*
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAKP,UAAU,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAkE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAKP,UAAU,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAkE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrH,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAiK7B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,MAAM,CAKR;AAED,+CAA+C;AAC/C,eAAO,MAAM,YAAY;;;CAAsC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAgB3B,CAAC;AAEX,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,SAAS,GACT,oBAAoB,GACpB,gBAAgB,GAChB,YAAY,GACZ,SAAS,GACT,gBAAgB,GAChB,QAAQ,GACR,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACzD,MAAM,GAAG,IAAI,CAgCf;AAWD,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA0ED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,EAAE,CAQV;AAwBD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAO9F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAgBlF;AA0BD,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C;AA4ID;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAOpF;AAMD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AA4BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,UAAO,GAAG,MAAM,CAYjF;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAc,GAAG,MAAM,CAKtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAgCpE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA6B5F;AAgbD,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAE1E;AA8BD,kHAAkH;AAClH,wBAAgB,oBAAoB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAEzE;AA6CD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CA8BrG;AAED,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D;;;;;;;OAOG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;;;;;;;;OAWG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CA2EN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAgC5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsKD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA48BpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAqCtE"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -33,12 +33,11 @@ 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.bashNeedsRepoLock = bashNeedsRepoLock;
|
|
42
41
|
exports.lockPathsFor = lockPathsFor;
|
|
43
42
|
exports.resolveMaxConcurrentSubtasks = resolveMaxConcurrentSubtasks;
|
|
44
43
|
exports.createLimiter = createLimiter;
|
|
@@ -68,6 +67,11 @@ const executor_1 = require("../tools/executor");
|
|
|
68
67
|
const agentTypes_1 = require("./agentTypes");
|
|
69
68
|
const skills_1 = require("./skills");
|
|
70
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; } });
|
|
71
75
|
const planMode_1 = require("./planMode");
|
|
72
76
|
const agentRegistry_1 = require("./agentRegistry");
|
|
73
77
|
const sandbox_1 = require("../tools/sandbox");
|
|
@@ -422,62 +426,6 @@ const WRITE_TOOLS = new Set([
|
|
|
422
426
|
* they serialise against each other while everything else stays parallel.
|
|
423
427
|
*/
|
|
424
428
|
const REPO_STATE_LOCK = '\u0000repo-state';
|
|
425
|
-
/**
|
|
426
|
-
* Commands that must not run concurrently with another instance of themselves.
|
|
427
|
-
*
|
|
428
|
-
* `bash` was excluded from the write-tool lock entirely, which was the largest hole in
|
|
429
|
-
* the concurrency story: it is the most mutating tool there is, and with 4 sub-agents
|
|
430
|
-
* fanned out two of them could run `git commit` simultaneously (one dies on
|
|
431
|
-
* `.git/index.lock`), or two `npm install` (corrupted node_modules / lockfile), or two
|
|
432
|
-
* `npm test` writing the same coverage and snapshot files (wrong results, not an error).
|
|
433
|
-
* The comment above withFileLock explains carefully why two edit_file calls must be
|
|
434
|
-
* sequenced, then a `bash -c 'echo x > file'` did exactly that with nothing holding it.
|
|
435
|
-
*
|
|
436
|
-
* Deliberately a VERB ALLOWLIST rather than serialising all of bash: an explorer's
|
|
437
|
-
* `grep`/`rg`/`ls`/`cat` are read-only and are precisely the calls that benefit most
|
|
438
|
-
* from running in parallel. Serialising those would trade a real correctness bug for a
|
|
439
|
-
* real performance regression. These verbs are the ones that take a global lock, write
|
|
440
|
-
* a lockfile, or mutate a shared build/dependency directory.
|
|
441
|
-
*/
|
|
442
|
-
const MUTATING_BASH_VERBS = new Set([
|
|
443
|
-
'git', 'npm', 'pnpm', 'yarn', 'bun', 'cargo', 'go', 'pip', 'pip3', 'poetry', 'uv',
|
|
444
|
-
'bundle', 'composer', 'gradle', 'mvn', 'make', 'terraform', 'helm', 'docker',
|
|
445
|
-
'vsce', 'tsc', 'pytest', 'dotnet', 'swift', 'gem',
|
|
446
|
-
]);
|
|
447
|
-
/**
|
|
448
|
-
* Whether a bash command should take the repo-state lock.
|
|
449
|
-
*
|
|
450
|
-
* Exported for tests: the failure mode (two concurrent `git commit`) is timing-dependent
|
|
451
|
-
* and painful to reproduce through the real loop.
|
|
452
|
-
*
|
|
453
|
-
* Scans EVERY segment, not just the first word, because the mutating verb is routinely
|
|
454
|
-
* not leading: `cd packages/core && npm publish`, `FOO=1 git push`, `ls && git commit`.
|
|
455
|
-
* Checking only the first token would miss most real commands.
|
|
456
|
-
*/
|
|
457
|
-
function bashNeedsRepoLock(command) {
|
|
458
|
-
if (!command)
|
|
459
|
-
return false;
|
|
460
|
-
// Split on shell separators, then take the first bare word of each segment, skipping
|
|
461
|
-
// env-var assignments (FOO=bar git push) and `sudo`/`time`-style prefixes.
|
|
462
|
-
const segments = command.split(/\|\||&&|[;|&\n()]|\$\(/);
|
|
463
|
-
for (const seg of segments) {
|
|
464
|
-
for (const tokenRaw of seg.trim().split(/\s+/)) {
|
|
465
|
-
const token = tokenRaw.replace(/^["']|["']$/g, '');
|
|
466
|
-
if (!token)
|
|
467
|
-
continue;
|
|
468
|
-
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
|
|
469
|
-
continue; // env assignment
|
|
470
|
-
if (token === 'sudo' || token === 'time' || token === 'nice' || token === 'env')
|
|
471
|
-
continue;
|
|
472
|
-
// Use the basename so `/usr/bin/git` and `./node_modules/.bin/tsc` still match.
|
|
473
|
-
const verb = token.split('/').pop() ?? token;
|
|
474
|
-
if (MUTATING_BASH_VERBS.has(verb))
|
|
475
|
-
return true;
|
|
476
|
-
break; // only the command word of each segment matters
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
481
429
|
/**
|
|
482
430
|
* Every path a tool call will touch, so all of them can be locked.
|
|
483
431
|
*
|
|
@@ -2650,7 +2598,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2650
2598
|
// This matters far more with sub-agents than without: up to 4 run
|
|
2651
2599
|
// concurrently, each issuing its own tool calls into this same process.
|
|
2652
2600
|
const locks = name === 'bash'
|
|
2653
|
-
? (bashNeedsRepoLock(String(input.command ?? '')) ? [REPO_STATE_LOCK] : [])
|
|
2601
|
+
? ((0, bashClassify_1.bashNeedsRepoLock)(String(input.command ?? '')) ? [REPO_STATE_LOCK] : [])
|
|
2654
2602
|
: lockPathsFor(name, input, options.workDir);
|
|
2655
2603
|
result = locks.length ? await withFileLocks(locks, run) : await run();
|
|
2656
2604
|
}
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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)",
|