@nexrall/code-core 1.4.63 → 1.4.66

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.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/dist/agent/agentRegistry.d.ts +14 -3
  3. package/dist/agent/agentRegistry.d.ts.map +1 -1
  4. package/dist/agent/agentRegistry.js +136 -6
  5. package/dist/agent/agentTypes.d.ts +53 -3
  6. package/dist/agent/agentTypes.d.ts.map +1 -1
  7. package/dist/agent/agentTypes.js +200 -153
  8. package/dist/agent/backgroundAgents.d.ts +66 -0
  9. package/dist/agent/backgroundAgents.d.ts.map +1 -0
  10. package/dist/agent/backgroundAgents.js +145 -0
  11. package/dist/agent/loop.d.ts +44 -7
  12. package/dist/agent/loop.d.ts.map +1 -1
  13. package/dist/agent/loop.js +946 -263
  14. package/dist/agent/memory.d.ts +11 -0
  15. package/dist/agent/memory.d.ts.map +1 -1
  16. package/dist/agent/memory.js +23 -3
  17. package/dist/agent/modelCatalogue.d.ts +15 -0
  18. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  19. package/dist/agent/modelCatalogue.js +46 -0
  20. package/dist/agent/planMode.d.ts +14 -0
  21. package/dist/agent/planMode.d.ts.map +1 -1
  22. package/dist/agent/planMode.js +145 -17
  23. package/dist/agent/readDedupe.d.ts +15 -0
  24. package/dist/agent/readDedupe.d.ts.map +1 -0
  25. package/dist/agent/readDedupe.js +146 -0
  26. package/dist/agent/securityLint.js +2 -2
  27. package/dist/agent/toolPrefetch.d.ts +44 -0
  28. package/dist/agent/toolPrefetch.d.ts.map +1 -0
  29. package/dist/agent/toolPrefetch.js +101 -0
  30. package/dist/agent/trust.d.ts +0 -5
  31. package/dist/agent/trust.d.ts.map +1 -1
  32. package/dist/agent/trust.js +41 -0
  33. package/dist/api/client.d.ts +15 -1
  34. package/dist/api/client.d.ts.map +1 -1
  35. package/dist/api/client.js +88 -8
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/permissions/destructive.d.ts +9 -7
  40. package/dist/permissions/destructive.d.ts.map +1 -1
  41. package/dist/permissions/destructive.js +55 -8
  42. package/dist/permissions/destructiveTokens.d.ts +29 -0
  43. package/dist/permissions/destructiveTokens.d.ts.map +1 -0
  44. package/dist/permissions/destructiveTokens.js +469 -0
  45. package/dist/permissions/modePolicy.d.ts +22 -8
  46. package/dist/permissions/modePolicy.d.ts.map +1 -1
  47. package/dist/permissions/modePolicy.js +25 -10
  48. package/dist/tools/executor.d.ts +36 -0
  49. package/dist/tools/executor.d.ts.map +1 -1
  50. package/dist/tools/executor.js +408 -85
  51. package/dist/tools/tsLangService.d.ts.map +1 -1
  52. package/dist/tools/tsLangService.js +107 -21
  53. package/dist/types.d.ts +101 -7
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +12 -1
  56. package/dist/util/miniYaml.d.ts +10 -0
  57. package/dist/util/miniYaml.d.ts.map +1 -0
  58. package/dist/util/miniYaml.js +149 -0
  59. package/package.json +2 -10
@@ -0,0 +1,469 @@
1
+ "use strict";
2
+ /**
3
+ * Token-level half of the destructive-command classifier.
4
+ *
5
+ * destructive.ts started life as a pile of regexes run over the command STRING. That
6
+ * missed most real-world spellings, because a regex does not understand shell:
7
+ * - global options between the tool and its verb: git -C repo reset --hard
8
+ * - combined short flags: git push -fu origin main
9
+ * - refspec force syntax: git push origin +main
10
+ * - wrappers that hide the real command: sudo / env X=1 / xargs / nohup
11
+ * - interpreters that take the command as a string: bash -c "…", eval "…", ssh host "…"
12
+ * - containers that run it elsewhere: docker exec db psql … / kubectl exec -- …
13
+ * and it could not express "rm -rf node_modules is fine, rm -rf src is not".
14
+ *
15
+ * So this module splits the command into simple commands the way a shell would (honouring
16
+ * quotes, so a commit message that MENTIONS `git push --force` is one argument of
17
+ * `git commit`, never a command), peels wrappers off, and judges (binary, args).
18
+ *
19
+ * It is a heuristic, not a shell: no variable expansion, no aliases, no functions. When it
20
+ * cannot tell (an `rm -rf $DIR`), it errs toward asking — the cost of a false positive is
21
+ * one confirmation, the cost of a false negative is unrecoverable.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.tokenize = tokenize;
25
+ exports.classifyTokens = classifyTokens;
26
+ const hit = (category, reason) => ({ category, reason });
27
+ /** Split `src` into simple commands, each a list of unquoted words. */
28
+ function tokenize(src) {
29
+ const cmds = [];
30
+ let cur = [];
31
+ let word = '';
32
+ let has = false;
33
+ let q = null;
34
+ const pushWord = () => { if (has)
35
+ cur.push(word); word = ''; has = false; };
36
+ const pushCmd = () => { pushWord(); if (cur.length)
37
+ cmds.push(cur); cur = []; };
38
+ for (let i = 0; i < src.length; i++) {
39
+ const ch = src[i];
40
+ if (q === "'") {
41
+ if (ch === "'")
42
+ q = null;
43
+ else
44
+ word += ch;
45
+ continue;
46
+ }
47
+ if (q === '"') {
48
+ if (ch === '\\' && i + 1 < src.length) {
49
+ word += src[++i];
50
+ continue;
51
+ }
52
+ if (ch === '"') {
53
+ q = null;
54
+ continue;
55
+ }
56
+ word += ch;
57
+ continue;
58
+ }
59
+ if (ch === "'" || ch === '"') {
60
+ q = ch;
61
+ has = true;
62
+ continue;
63
+ }
64
+ if (ch === '\\') {
65
+ if (i + 1 < src.length) {
66
+ word += src[++i];
67
+ has = true;
68
+ }
69
+ continue;
70
+ }
71
+ // Command separators. `(` `)` and backtick make $(…) / `…` bodies their own commands.
72
+ if (ch === ';' || ch === '&' || ch === '|' || ch === '\n' || ch === '(' || ch === ')' || ch === '`') {
73
+ pushCmd();
74
+ continue;
75
+ }
76
+ if (ch === ' ' || ch === '\t') {
77
+ pushWord();
78
+ continue;
79
+ }
80
+ word += ch;
81
+ has = true;
82
+ }
83
+ pushCmd();
84
+ return cmds;
85
+ }
86
+ const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
87
+ const CONTROL_WORDS = new Set(['{', '}', '!', 'if', 'then', 'else', 'elif', 'do', 'while', 'until', 'fi', 'done']);
88
+ const SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh']);
89
+ const base = (w) => {
90
+ const b = w.includes('/') ? w.slice(w.lastIndexOf('/') + 1) : w;
91
+ return b;
92
+ };
93
+ /** Drop leading options from `args`, consuming a value for those named in `withArg`. */
94
+ function dropOpts(args, withArg = new Set()) {
95
+ let i = 0;
96
+ while (i < args.length && args[i].startsWith('-') && args[i] !== '--') {
97
+ i += withArg.has(args[i]) ? 2 : 1;
98
+ }
99
+ if (args[i] === '--')
100
+ i++;
101
+ return args.slice(i);
102
+ }
103
+ /** True if `args` carries the long flag (or `--flag=value`) or a short-flag bundle containing `short`. */
104
+ function has(args, long, short) {
105
+ return args.some((a) => (long !== null && (a === long || a.startsWith(long + '='))) ||
106
+ (short !== null && /^-[A-Za-z]+$/.test(a) && a.includes(short)));
107
+ }
108
+ const BROAD = new Set(['.', './', '*', './*', ':/', ':/', '..', '../', '/', '~', '~/', '$HOME', '${HOME}', '.*']);
109
+ /** Directories that are regenerated by a build/install and are routine to delete. */
110
+ const DISPOSABLE = new Set([
111
+ 'node_modules', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit', '.turbo', '.cache', '.parcel-cache',
112
+ 'coverage', '.nyc_output', 'target', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox',
113
+ 'venv', '.venv', '.gradle', '.dart_tool', 'tmp', '.tmp', '.vite', '.angular', '.expo', 'DerivedData',
114
+ ]);
115
+ const SCRATCH_PREFIXES = ['/tmp/', '/private/tmp/', '/var/folders/', '/private/var/folders/'];
116
+ function isDisposableTarget(raw) {
117
+ let t = raw;
118
+ if (t === '')
119
+ return false;
120
+ // Anything we cannot resolve statically is treated as unsafe.
121
+ if (/[$~`]/.test(t) || t.includes('..'))
122
+ return false;
123
+ if (t.startsWith('/'))
124
+ return SCRATCH_PREFIXES.some((p) => t.startsWith(p) && t.length > p.length);
125
+ t = t.replace(/^(?:\.\/)+/, '').replace(/\/+$/, '');
126
+ if (t === '' || t === '.')
127
+ return false;
128
+ const segs = t.split('/').filter(Boolean);
129
+ if (segs.length === 0)
130
+ return false;
131
+ return DISPOSABLE.has(segs[0]) || DISPOSABLE.has(segs[segs.length - 1]);
132
+ }
133
+ function checkGit(argsIn) {
134
+ // Skip git's GLOBAL options so `git -C repo reset --hard` is seen as `reset --hard`.
135
+ let i = 0;
136
+ while (i < argsIn.length && argsIn[i].startsWith('-')) {
137
+ const a = argsIn[i];
138
+ i += ['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path'].includes(a) ? 2 : 1;
139
+ }
140
+ const sub = argsIn[i];
141
+ const args = argsIn.slice(i + 1);
142
+ const operands = args.filter((a) => !a.startsWith('-'));
143
+ switch (sub) {
144
+ case 'push': {
145
+ if (has(args, '--force', 'f') || operands.some((a) => a.startsWith('+'))) {
146
+ return hit('git-history', 'force-pushes (can overwrite/destroy remote history)');
147
+ }
148
+ if (has(args, '--delete', 'd') || operands.some((a) => a.startsWith(':'))) {
149
+ return hit('git-history', 'deletes a remote branch or tag');
150
+ }
151
+ if (has(args, '--mirror', null)) {
152
+ return hit('git-history', 'mirror-pushes (overwrites and deletes remote refs to match local)');
153
+ }
154
+ return null;
155
+ }
156
+ case 'reset':
157
+ if (has(args, '--hard', null) || has(args, '--keep', null) || has(args, '--merge', null)) {
158
+ return hit('git-history', 'hard-resets the working tree (discards uncommitted work)');
159
+ }
160
+ return null;
161
+ case 'clean':
162
+ if (has(args, '--force', 'f') && !has(args, '--dry-run', 'n')) {
163
+ return hit('git-history', 'deletes untracked files (git clean)');
164
+ }
165
+ return null;
166
+ case 'branch':
167
+ if (args.some((a) => /^-[A-Za-z]*D/.test(a)) ||
168
+ ((has(args, '--delete', 'd')) && has(args, '--force', 'f'))) {
169
+ return hit('git-history', 'force-deletes a git branch');
170
+ }
171
+ return null;
172
+ case 'checkout':
173
+ if (has(args, '--force', 'f'))
174
+ return hit('git-history', 'git checkout --force discards local changes');
175
+ if (args.some((a) => BROAD.has(a)))
176
+ return hit('git-history', 'discards every uncommitted change in the working tree');
177
+ return null;
178
+ case 'restore': {
179
+ const stagedOnly = has(args, '--staged', 'S') && !has(args, '--worktree', 'W');
180
+ if (!stagedOnly && args.some((a) => BROAD.has(a))) {
181
+ return hit('git-history', 'discards every uncommitted change in the working tree');
182
+ }
183
+ return null;
184
+ }
185
+ case 'stash':
186
+ if (args[0] === 'drop' || args[0] === 'clear')
187
+ return hit('git-history', 'drops stashed work (not recoverable without the commit hash)');
188
+ return null;
189
+ case 'update-ref':
190
+ if (has(args, '-d', 'd') || has(args, '--delete', null))
191
+ return hit('git-history', 'deletes a git ref');
192
+ return null;
193
+ case 'reflog':
194
+ if (args[0] === 'delete' || args[0] === 'expire')
195
+ return hit('git-history', 'reflog expire / gc prune (permanently drops recoverable commits)');
196
+ return null;
197
+ case 'gc':
198
+ if (args.some((a) => /^--prune=(?:now|all)$/.test(a)))
199
+ return hit('git-history', 'reflog expire / gc prune (permanently drops recoverable commits)');
200
+ return null;
201
+ case 'worktree':
202
+ if (args[0] === 'remove' && has(args, '--force', 'f'))
203
+ return hit('git-history', 'force-removes a worktree (discards its uncommitted work)');
204
+ return null;
205
+ case 'filter-branch':
206
+ case 'filter-repo':
207
+ return hit('git-history', 'rewrites git history (filter-branch/filter-repo)');
208
+ case 'rm':
209
+ // `git rm -r <dir>` is routine and recoverable from HEAD; only a bare `.`/`*` is a wipe.
210
+ if (operands.some((a) => BROAD.has(a))) {
211
+ return hit('git-history', 'removes every tracked file from the working tree and index');
212
+ }
213
+ return null;
214
+ default:
215
+ return null;
216
+ }
217
+ }
218
+ function checkRm(args) {
219
+ const targets = [];
220
+ let recursive = false;
221
+ let endOpts = false;
222
+ for (const a of args) {
223
+ if (!endOpts && a === '--') {
224
+ endOpts = true;
225
+ continue;
226
+ }
227
+ if (!endOpts && a.startsWith('-')) {
228
+ if (a === '--recursive' || /^-[A-Za-z]*[rR]/.test(a))
229
+ recursive = true;
230
+ continue;
231
+ }
232
+ targets.push(a);
233
+ }
234
+ if (targets.some((t) => BROAD.has(t)))
235
+ return hit('filesystem', 'deletes the current directory, home, or everything matching a bare glob');
236
+ if (recursive && targets.some((t) => !isDisposableTarget(t))) {
237
+ return hit('filesystem', 'recursively deletes files or directories that are not a known build artefact');
238
+ }
239
+ return null;
240
+ }
241
+ function checkFind(args) {
242
+ if (args.includes('-delete'))
243
+ return hit('filesystem', 'find -delete removes every match');
244
+ for (let i = 0; i < args.length - 1; i++) {
245
+ if ((args[i] === '-exec' || args[i] === '-execdir' || args[i] === '-ok') && ['rm', 'shred', 'unlink'].includes(base(args[i + 1]))) {
246
+ return hit('filesystem', 'find -exec rm removes every match');
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+ /** Judge one already-unwrapped command. `recurse` re-enters the string classifier. */
252
+ function judge(bin, args) {
253
+ switch (bin) {
254
+ case 'git': return checkGit(args);
255
+ case 'rm':
256
+ case 'unlink': return checkRm(args);
257
+ case 'find': return checkFind(args);
258
+ case 'rsync':
259
+ if (args.some((a) => a === '--del' || /^--delete(?:-[a-z]+)?$/.test(a)))
260
+ return hit('filesystem', 'rsync --delete removes destination files that are absent from the source');
261
+ return null;
262
+ case 'truncate': return hit('filesystem', 'truncate cuts a file (contents lost)');
263
+ case 'shred': return hit('filesystem', 'shred irrecoverably overwrites a file');
264
+ case 'dd':
265
+ if (args.some((a) => /^of=\/dev\//.test(a)))
266
+ return hit('filesystem', 'dd writing to a device (overwrites the disk)');
267
+ return null;
268
+ case 'diskutil':
269
+ if (/^(?:erase|secureErase|zeroDisk|randomDisk|partitionDisk|reformat)/i.test(args[0] ?? ''))
270
+ return hit('filesystem', 'diskutil erases or repartitions a disk');
271
+ return null;
272
+ case 'chmod': {
273
+ if (!has(args, '--recursive', 'R'))
274
+ return null;
275
+ const operands = args.filter((a) => !a.startsWith('-'));
276
+ if (operands.slice(1).some((t) => BROAD.has(t)))
277
+ return hit('filesystem', 'recursive chmod over the current directory / home / root');
278
+ return null;
279
+ }
280
+ case 'terraform':
281
+ case 'tofu':
282
+ case 'terragrunt':
283
+ if (args.includes('destroy') || (args.includes('apply') && (args.includes('-destroy') || args.includes('-auto-approve')))) {
284
+ return hit('infra', 'destroys or applies Terraform-managed infrastructure without a plan review');
285
+ }
286
+ return null;
287
+ case 'kubectl':
288
+ case 'oc': {
289
+ const a = dropOpts(args, new Set(['-n', '--namespace', '--context', '--cluster', '--kubeconfig', '-s', '--server', '--user']));
290
+ if (a[0] === 'delete')
291
+ return hit('infra', 'deletes Kubernetes resources');
292
+ if (a[0] === 'drain')
293
+ return hit('infra', 'drains a node (evicts every pod)');
294
+ if (a[0] === 'replace' && has(a, '--force', null))
295
+ return hit('infra', 'kubectl replace --force deletes and recreates the resource');
296
+ return null;
297
+ }
298
+ case 'helm': {
299
+ const a = dropOpts(args, new Set(['-n', '--namespace', '--kube-context', '--kubeconfig']));
300
+ if (a[0] === 'uninstall' || a[0] === 'delete')
301
+ return hit('infra', 'helm uninstall / delete (removes a release)');
302
+ return null;
303
+ }
304
+ case 'aws':
305
+ if (args[0] === 's3' && args[1] === 'sync' && has(args, '--delete', null))
306
+ return hit('cloud', 'aws s3 sync --delete removes bucket objects missing locally');
307
+ return null;
308
+ case 'vercel':
309
+ if (args[0] === 'remove' || args[0] === 'rm')
310
+ return hit('cloud', 'removes a Vercel project or deployment');
311
+ return null;
312
+ case 'fly':
313
+ case 'flyctl':
314
+ if (args.includes('destroy'))
315
+ return hit('cloud', 'destroys a Fly.io app, volume or machine');
316
+ return null;
317
+ case 'heroku':
318
+ if (args.some((a) => /:destroy$/.test(a) || a === 'pg:reset'))
319
+ return hit('cloud', 'destroys a Heroku app or resets its database');
320
+ return null;
321
+ case 'supabase':
322
+ if ((args[0] === 'db' && args[1] === 'reset') || (args[0] === 'projects' && args[1] === 'delete'))
323
+ return hit('cloud', 'resets a Supabase database or deletes a project');
324
+ return null;
325
+ case 'firebase':
326
+ if (args.some((a) => /:delete$/.test(a)))
327
+ return hit('cloud', 'deletes Firebase data or a project');
328
+ return null;
329
+ case 'netlify':
330
+ if (args.some((a) => /:delete$/.test(a)))
331
+ return hit('cloud', 'deletes a Netlify site');
332
+ return null;
333
+ case 'npm':
334
+ case 'pnpm':
335
+ case 'yarn':
336
+ if (args[0] === 'unpublish')
337
+ return hit('publish', 'unpublishes a package version');
338
+ if (args[0] === 'dist-tag' && (args[1] === 'rm' || args[1] === 'remove'))
339
+ return hit('publish', 'removes an npm dist-tag (can break `npm i pkg@latest`)');
340
+ return null;
341
+ case 'gh': {
342
+ if ((args[0] === 'repo' || args[0] === 'release') && args[1] === 'delete')
343
+ return hit('cloud', 'deletes a GitHub repository or release');
344
+ if (args[0] === 'api') {
345
+ const m = args.findIndex((a) => a === '-X' || a === '--method');
346
+ const inline = args.find((a) => /^(?:-X|--method=)DELETE$/i.test(a));
347
+ if ((m >= 0 && /^delete$/i.test(args[m + 1] ?? '')) || inline)
348
+ return hit('cloud', 'sends an HTTP DELETE to the GitHub API');
349
+ }
350
+ return null;
351
+ }
352
+ case 'docker':
353
+ case 'docker-compose':
354
+ case 'podman': {
355
+ const a = bin === 'docker-compose' ? ['compose', ...args] : dropOpts(args, new Set(['-H', '--host', '--context', '-c']));
356
+ const verb = a[0] === 'container' ? a[1] : a[0];
357
+ const rest = a.slice(a[0] === 'container' ? 2 : 1);
358
+ if (verb === 'rm' && has(rest, '--force', 'f') && rest.some((x) => x.startsWith('$')))
359
+ return hit('container', 'force-removes many containers at once');
360
+ if (a[0] === 'compose' && rest.includes('down') && (has(rest, '--volumes', 'v')))
361
+ return hit('container', 'docker compose down with -v (deletes named volumes)');
362
+ return null;
363
+ }
364
+ case 'dropdb':
365
+ case 'dropuser': return hit('database', 'drops a Postgres database or role');
366
+ default:
367
+ if (bin.startsWith('mkfs'))
368
+ return hit('filesystem', 'mkfs formats a filesystem (destroys its contents)');
369
+ return null;
370
+ }
371
+ }
372
+ /**
373
+ * Peel wrappers off one simple command and judge what is really being run.
374
+ * `recurse` classifies a nested command STRING (bash -c "…", ssh host "…", eval …).
375
+ */
376
+ const DB_BINS = new Set([
377
+ 'psql', 'mysql', 'mariadb', 'mongo', 'mongosh', 'sqlite3', 'cockroach', 'clickhouse-client', 'pgcli', 'mycli', 'usql', 'sqlcmd', 'redis-cli',
378
+ ]);
379
+ function classifyTokens(tokens, recurse, rawSql) {
380
+ let t = tokens.slice();
381
+ for (let guard = 0; guard < 12; guard++) {
382
+ while (t.length && (ENV_ASSIGN.test(t[0]) || CONTROL_WORDS.has(t[0])))
383
+ t = t.slice(1);
384
+ if (t.length === 0)
385
+ return null;
386
+ // A bare redirect with no command (`> file`, `: > file`, `true > file`) truncates the file.
387
+ {
388
+ const r = /^>(?![>&])/.test(t[0]) ? t : ((t[0] === ':' || t[0] === 'true') && /^>(?![>&])/.test(t[1] ?? '')) ? t.slice(1) : null;
389
+ if (r) {
390
+ const target = r[0].length > 1 ? r[0].slice(1) : (r[1] ?? '');
391
+ if (!target.startsWith('/dev/'))
392
+ return hit('filesystem', 'a bare `>` redirect truncates the file (contents lost)');
393
+ return null;
394
+ }
395
+ }
396
+ const bin = base(t[0]);
397
+ const args = t.slice(1);
398
+ // Real SQL lives inside quotes, which the tokenizer has already unwrapped; hand it back
399
+ // to the quote-preserving SQL rules as a normal `client "arg" "arg"` string.
400
+ if (DB_BINS.has(bin)) {
401
+ return rawSql(bin + ' ' + args.map((a) => JSON.stringify(a)).join(' '));
402
+ }
403
+ if (bin === 'sudo' || bin === 'doas') {
404
+ t = dropOpts(args, new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']));
405
+ continue;
406
+ }
407
+ if (bin === 'env') {
408
+ t = dropOpts(args, new Set(['-u', '-C', '-S']));
409
+ continue;
410
+ }
411
+ if (bin === 'nice') {
412
+ t = dropOpts(args, new Set(['-n']));
413
+ continue;
414
+ }
415
+ if (bin === 'ionice') {
416
+ t = dropOpts(args, new Set(['-c', '-n', '-p']));
417
+ continue;
418
+ }
419
+ if (bin === 'timeout') {
420
+ t = dropOpts(args, new Set(['-s', '-k'])).slice(1);
421
+ continue;
422
+ }
423
+ if (bin === 'xargs') {
424
+ t = dropOpts(args, new Set(['-I', '-n', '-P', '-L', '-s', '-d', '-E', '-a', '-J']));
425
+ continue;
426
+ }
427
+ if (['command', 'builtin', 'nohup', 'exec', 'time', 'setsid', 'caffeinate', 'stdbuf', 'unbuffer'].includes(bin)) {
428
+ t = dropOpts(args);
429
+ continue;
430
+ }
431
+ if (SHELLS.has(bin)) {
432
+ const i = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
433
+ if (i >= 0 && args[i + 1] !== undefined)
434
+ return recurse(args[i + 1]);
435
+ return null;
436
+ }
437
+ if (bin === 'eval')
438
+ return recurse(args.join(' '));
439
+ if (bin === 'ssh') {
440
+ const a = dropOpts(args, new Set(['-p', '-i', '-l', '-o', '-F', '-J', '-L', '-R', '-D', '-b', '-c', '-E', '-e', '-I', '-m', '-O', '-Q', '-S', '-w', '-W']));
441
+ return a.length > 1 ? recurse(a.slice(1).join(' ')) : null;
442
+ }
443
+ if (bin === 'docker' || bin === 'podman' || bin === 'docker-compose') {
444
+ const a0 = bin === 'docker-compose' ? ['compose', ...args] : dropOpts(args, new Set(['-H', '--host', '--context', '-c']));
445
+ const isExec = a0[0] === 'exec' || (a0[0] === 'compose' && dropOpts(a0.slice(1), new Set(['-f', '-p', '--project-name', '--file', '--profile']))[0] === 'exec');
446
+ if (isExec) {
447
+ const afterVerb = a0[0] === 'exec'
448
+ ? a0.slice(1)
449
+ : dropOpts(a0.slice(1), new Set(['-f', '-p', '--project-name', '--file', '--profile'])).slice(1);
450
+ const inner = dropOpts(afterVerb, new Set(['-e', '-u', '-w', '--env', '--user', '--workdir', '--env-file', '--detach-keys', '--index']));
451
+ if (inner.length > 1) {
452
+ t = inner.slice(1);
453
+ continue;
454
+ }
455
+ return null;
456
+ }
457
+ }
458
+ if (bin === 'kubectl' || bin === 'oc') {
459
+ const i = args.indexOf('--');
460
+ if (i >= 0 && args.includes('exec') && i + 1 < args.length) {
461
+ t = args.slice(i + 1);
462
+ continue;
463
+ }
464
+ }
465
+ return judge(bin, args);
466
+ }
467
+ return null;
468
+ }
469
+ //# sourceMappingURL=destructiveTokens.js.map
@@ -18,6 +18,16 @@ export interface PolicyInput {
18
18
  * in two windows. Note `memory_write` is NOT here even though the CLI's old list had it
19
19
  * — it mutates durable cross-session state, and listing it under "read-only / safe"
20
20
  * meant it silently bypassed plan mode's read-only guarantee.
21
+ *
22
+ * `task` IS here on purpose, to stay in lockstep with `planMode.ts`'s own
23
+ * READ_ONLY_TOOLS: that module already lets `task` through its plan-mode lock
24
+ * because delegating research to a sub-agent is safe — `runAgentLoop` passes
25
+ * `planMode` down, so the sub-agent is under the identical read-only lock and
26
+ * its own write tools are refused one level deeper. Without `task` here too,
27
+ * step 3 of `decide()` (`mode === 'plan' → deny`) would silently re-block a
28
+ * call that `checkPlanMode()` had already deliberately allowed through — the
29
+ * exact "two copies that drift" failure this module exists to prevent, just
30
+ * hitting `task` specifically instead of the read-only tools it was written for.
21
31
  */
22
32
  export declare const READ_ONLY_TOOLS: Set<string>;
23
33
  /** Tools that write to the filesystem. */
@@ -27,16 +37,20 @@ export declare const FILE_MUTATE_TOOLS: Set<string>;
27
37
  /** Tools that run a shell command. */
28
38
  export declare const BASH_TOOLS: Set<string>;
29
39
  /**
30
- * Is this bash command read-only?
40
+ * Is this bash command read-only — i.e. safe to run WITHOUT asking?
31
41
  *
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.
42
+ * History: this used to be `!bashNeedsRepoLock(command)`. That classifier answers a
43
+ * different question — "does this need the repo-state LOCK?" — by listing the ~30
44
+ * build/VCS verbs that take one (git, npm, make …). Everything NOT on that list was
45
+ * therefore "read-only", so `rm file`, `mv`, `echo x > f`, `sed -i`, `kill` and
46
+ * `curl … | sh` all ran with no prompt in `ask` and `edit` mode. Measured through the
47
+ * real `requestPermission`: prompted=0 for each. A denylist of mutators can never be
48
+ * complete; the gate needs an ALLOWLIST of readers, which plan mode already had.
36
49
  *
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.
50
+ * It now delegates to `isReadOnlyPipeline` (agent/planMode.ts), the same "provably
51
+ * read-only" classifier the plan-mode lock uses, so "read-only" means one thing in
52
+ * both places. `bashNeedsRepoLock` is unchanged and still correct for what it is for:
53
+ * deciding which calls must not run concurrently.
40
54
  */
41
55
  export declare function isReadOnlyBash(command: string): boolean;
42
56
  /**
@@ -1 +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,aAG3B,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,CAwDvD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAOzD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAGtD"}
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;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,eAAe,aAW1B,CAAC;AAEH,0CAA0C;AAC1C,eAAO,MAAM,gBAAgB,aAG3B,CAAC;AAEH,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,aAE5B,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,UAAU,aAAsD,CAAC;AAE9E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,cAAc,CAwDvD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAOzD;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAGtD"}
@@ -5,7 +5,7 @@ exports.isReadOnlyBash = isReadOnlyBash;
5
5
  exports.decide = decide;
6
6
  exports.describeMode = describeMode;
7
7
  exports.parseMode = parseMode;
8
- const bashClassify_1 = require("./bashClassify");
8
+ const planMode_1 = require("../agent/planMode");
9
9
  /**
10
10
  * Tools that only ever READ. Always allowed, in every mode including plan.
11
11
  *
@@ -13,6 +13,16 @@ const bashClassify_1 = require("./bashClassify");
13
13
  * in two windows. Note `memory_write` is NOT here even though the CLI's old list had it
14
14
  * — it mutates durable cross-session state, and listing it under "read-only / safe"
15
15
  * meant it silently bypassed plan mode's read-only guarantee.
16
+ *
17
+ * `task` IS here on purpose, to stay in lockstep with `planMode.ts`'s own
18
+ * READ_ONLY_TOOLS: that module already lets `task` through its plan-mode lock
19
+ * because delegating research to a sub-agent is safe — `runAgentLoop` passes
20
+ * `planMode` down, so the sub-agent is under the identical read-only lock and
21
+ * its own write tools are refused one level deeper. Without `task` here too,
22
+ * step 3 of `decide()` (`mode === 'plan' → deny`) would silently re-block a
23
+ * call that `checkPlanMode()` had already deliberately allowed through — the
24
+ * exact "two copies that drift" failure this module exists to prevent, just
25
+ * hitting `task` specifically instead of the read-only tools it was written for.
16
26
  */
17
27
  exports.READ_ONLY_TOOLS = new Set([
18
28
  'read_file', 'list_directory', 'search_files', 'glob', 'grep',
@@ -24,6 +34,7 @@ exports.READ_ONLY_TOOLS = new Set([
24
34
  'notebook_read',
25
35
  'get_diagnostics', 'go_to_definition', 'find_references',
26
36
  'get_symbols', 'get_workspace_symbols', 'get_hover',
37
+ 'task',
27
38
  ]);
28
39
  /** Tools that write to the filesystem. */
29
40
  exports.FILE_WRITE_TOOLS = new Set([
@@ -37,19 +48,23 @@ exports.FILE_MUTATE_TOOLS = new Set([
37
48
  /** Tools that run a shell command. */
38
49
  exports.BASH_TOOLS = new Set(['bash', 'run_command', 'execute_command']);
39
50
  /**
40
- * Is this bash command read-only?
51
+ * Is this bash command read-only — i.e. safe to run WITHOUT asking?
41
52
  *
42
- * Deliberately delegates to `bashNeedsRepoLock`, the classifier written for the
43
- * concurrency work: it already distinguishes commands that mutate shared state
44
- * (`git`, `npm`, `cargo`, `make` …) from ones that only read (`grep`, `ls`, `cat`,
45
- * `rg`), scanning every shell segment so `cd x && npm publish` is caught.
53
+ * History: this used to be `!bashNeedsRepoLock(command)`. That classifier answers a
54
+ * different question — "does this need the repo-state LOCK?" — by listing the ~30
55
+ * build/VCS verbs that take one (git, npm, make …). Everything NOT on that list was
56
+ * therefore "read-only", so `rm file`, `mv`, `echo x > f`, `sed -i`, `kill` and
57
+ * `curl … | sh` all ran with no prompt in `ask` and `edit` mode. Measured through the
58
+ * real `requestPermission`: prompted=0 for each. A denylist of mutators can never be
59
+ * complete; the gate needs an ALLOWLIST of readers, which plan mode already had.
46
60
  *
47
- * Reusing it is the point. Writing a second classifier here would create two
48
- * definitions of "mutating" that must agree forever — the exact failure this whole
49
- * audit keeps finding. If the lock treats a command as mutating, so does the gate.
61
+ * It now delegates to `isReadOnlyPipeline` (agent/planMode.ts), the same "provably
62
+ * read-only" classifier the plan-mode lock uses, so "read-only" means one thing in
63
+ * both places. `bashNeedsRepoLock` is unchanged and still correct for what it is for:
64
+ * deciding which calls must not run concurrently.
50
65
  */
51
66
  function isReadOnlyBash(command) {
52
- return !(0, bashClassify_1.bashNeedsRepoLock)(command);
67
+ return (0, planMode_1.isReadOnlyPipeline)(command);
53
68
  }
54
69
  /**
55
70
  * The single source of truth for "should we prompt?".
@@ -1,5 +1,41 @@
1
1
  import type { ToolResult } from '../types';
2
2
  import { type SandboxConfig } from './sandbox';
3
+ /**
4
+ * Cap output from a tool we do not control (MCP servers). Built-in tools all cap their
5
+ * own output, but an MCP result went into the conversation verbatim: one call to a
6
+ * DB/browser/log server could inline megabytes, which is then re-sent (and re-billed as
7
+ * cache writes) on every later round until compaction. Keep HEAD+TAIL inline and spill
8
+ * the full text to a temp file the model can read_file with offset/limit — the same
9
+ * contract as bash's GAP B spill.
10
+ */
11
+ export declare function capExternalOutput(output: string, label: string, maxChars?: number): string;
12
+ /**
13
+ * Async drop-in for the subset of `spawnSync` the search tools use. spawnSync blocked
14
+ * the Node event loop for the whole search: several search_files calls issued in one
15
+ * parallel tool batch ran strictly one after another, and a search started by the
16
+ * mid-stream prefetcher froze SSE parsing and UI rendering until it finished. Result
17
+ * shape mirrors spawnSync's (`status`, `signal`, `error` with code ETIMEDOUT / ENOBUFS)
18
+ * so the callers' existing failure handling is unchanged.
19
+ */
20
+ export interface CaptureResult {
21
+ stdout: string;
22
+ stderr: string;
23
+ status: number | null;
24
+ signal: NodeJS.Signals | null;
25
+ error?: NodeJS.ErrnoException;
26
+ }
27
+ export declare function runCapture(cmd: string, args: string[], opts: {
28
+ cwd?: string;
29
+ timeout: number;
30
+ maxBuffer: number;
31
+ }): Promise<CaptureResult>;
32
+ /**
33
+ * Glob → anchored RegExp. Scans the pattern once, so the `*` / `?` rules can never
34
+ * rewrite the regex emitted for `**` (the old chained .replace() calls turned
35
+ * `**\/` into `([^/]:[^/]+/)` — every `**` pattern silently matched nothing).
36
+ * Supports `**`, `*`, `?`, `{a,b}` and `[...]` classes.
37
+ */
38
+ export declare function globToRegex(pattern: string): RegExp;
3
39
  export declare function executeTool(name: string, input: Record<string, unknown>, abortSignal?: {
4
40
  aborted: boolean;
5
41
  }, sandbox?: SandboxConfig, workDir?: string, agentScope?: string, onStream?: (chunk: string) => void, selfPeer?: {
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAqxFtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAMlC,QAAQ,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,UAAU,CAAC,CAkCrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAokCtE;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAmB,GAAG,MAAM,CAcpG;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B;AACD,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAsoCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqCnD;AAwwBD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAMlC,QAAQ,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,UAAU,CAAC,CAkCrB"}