evolcore 0.0.9 → 0.0.11

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 (64) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/contact-book-store.js +1 -1
  20. package/dist/config/gateway-config.js +26 -10
  21. package/dist/core/agent-reload-coordinator.js +53 -0
  22. package/dist/core/auth/operation-authorizer.js +32 -147
  23. package/dist/core/auth/operation-catalog.js +80 -0
  24. package/dist/core/bootstrap-messages.js +50 -0
  25. package/dist/core/bootstrap-service.js +85 -10
  26. package/dist/core/channel-loader.js +23 -6
  27. package/dist/core/command/agent-control.js +14 -11
  28. package/dist/core/command/menu-handler.js +67 -76
  29. package/dist/core/command/slash-handler.js +4 -4
  30. package/dist/core/data-migration.js +79 -27
  31. package/dist/core/evolagent-registry.js +125 -35
  32. package/dist/core/evolagent.js +8 -3
  33. package/dist/core/inference/text-inference.js +38 -4
  34. package/dist/core/message/message-bridge.js +1 -1
  35. package/dist/core/message/message-log.js +22 -0
  36. package/dist/core/message/message-queue.js +19 -4
  37. package/dist/core/model/model-catalog.js +143 -24
  38. package/dist/core/model/model-diagnostics.js +28 -10
  39. package/dist/core/permission/index.js +1 -0
  40. package/dist/core/permission/readonly-shell-query.js +532 -0
  41. package/dist/core/permission/shell-environment.js +46 -0
  42. package/dist/core/permission/tool-policy.js +231 -93
  43. package/dist/core/protected-paths.js +10 -7
  44. package/dist/core/runner-reload-transaction.js +57 -0
  45. package/dist/index.js +262 -84
  46. package/dist/ipc.js +29 -11
  47. package/dist/utils/aid-bind.js +3 -8
  48. package/dist/utils/log-writer.js +6 -10
  49. package/dist/utils/logger.js +5 -5
  50. package/kits/docs/evolcore/msg.md +13 -0
  51. package/kits/rules/01-overview.md +9 -0
  52. package/kits/schemas/agent-config.schema.3.json +1 -1
  53. package/kits/schemas/agent-config.schema.4.json +1 -1
  54. package/kits/schemas/relation-config.schema.2.json +1 -1
  55. package/kits/schemas/role-config.schema.1.json +1 -1
  56. package/kits/templates/roles/admin.json +5 -0
  57. package/kits/templates/roles/member.json +17 -0
  58. package/kits/templates/roles/visitor.json +8 -0
  59. package/kits/templates/system-fragments/bootstrap.md +12 -6
  60. package/kits/templates/system-fragments/channel.md +6 -0
  61. package/kits/templates/system-fragments/session.md +2 -0
  62. package/package.json +2 -1
  63. package/skills/eclink/SKILL.md +15 -3
  64. package/skills/eclink/agents/openai.yaml +3 -3
@@ -0,0 +1,532 @@
1
+ import path from 'path';
2
+ import { parseLiteralShellArgv } from './ec-command-parser.js';
3
+ const MAX_QUERY_LISTS = 8;
4
+ const MAX_PIPELINE_STAGES = 3;
5
+ const SUPPORTED_EXECUTABLES = new Set([
6
+ 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort',
7
+ ]);
8
+ const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
9
+ const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
10
+ // The fd number must be adjacent to `>` in real shell grammar. Accepting
11
+ // `2 > /dev/null` would hide a literal file operand named `2` while actually
12
+ // redirecting stdout rather than stderr.
13
+ const STDERR_DEV_NULL_RE = /^(.*?)[ \t]+2>[ \t]*\/dev\/null[ \t]*$/;
14
+ function normalizeExecutable(value) {
15
+ const normalized = value.replace(/\\/g, '/');
16
+ const executable = path.posix.basename(normalized);
17
+ if (!SUPPORTED_EXECUTABLES.has(executable))
18
+ return undefined;
19
+ if (normalized === executable || normalized === `/bin/${executable}` || normalized === `/usr/bin/${executable}`) {
20
+ return executable;
21
+ }
22
+ return undefined;
23
+ }
24
+ function splitQueryStructure(command) {
25
+ const pipelines = [];
26
+ let pipeline = [];
27
+ let segment = '';
28
+ let quote = null;
29
+ let escaped = false;
30
+ const finishStage = () => {
31
+ const trimmed = segment.trim();
32
+ if (!trimmed)
33
+ return false;
34
+ pipeline.push(trimmed);
35
+ segment = '';
36
+ return pipeline.length <= MAX_PIPELINE_STAGES;
37
+ };
38
+ const finishPipeline = () => {
39
+ if (!finishStage())
40
+ return false;
41
+ pipelines.push(pipeline);
42
+ pipeline = [];
43
+ return pipelines.length <= MAX_QUERY_LISTS;
44
+ };
45
+ for (let index = 0; index < command.length; index++) {
46
+ const char = command[index];
47
+ if (escaped) {
48
+ segment += char;
49
+ escaped = false;
50
+ continue;
51
+ }
52
+ if (char === '\\') {
53
+ segment += char;
54
+ escaped = true;
55
+ continue;
56
+ }
57
+ if (quote === 'single') {
58
+ segment += char;
59
+ if (char === "'")
60
+ quote = null;
61
+ continue;
62
+ }
63
+ if (quote === 'double') {
64
+ segment += char;
65
+ if (char === '"')
66
+ quote = null;
67
+ continue;
68
+ }
69
+ if (char === "'" || char === '"') {
70
+ quote = char === "'" ? 'single' : 'double';
71
+ segment += char;
72
+ continue;
73
+ }
74
+ if (char === '\0' || char === '\r' || char === '\n' || char === '&' || char === '<')
75
+ return null;
76
+ if (char === '|') {
77
+ if (command[index + 1] === '|' || !finishStage())
78
+ return null;
79
+ continue;
80
+ }
81
+ if (char === ';') {
82
+ if (!finishPipeline())
83
+ return null;
84
+ continue;
85
+ }
86
+ segment += char;
87
+ }
88
+ if (escaped || quote || !finishPipeline())
89
+ return null;
90
+ return pipelines;
91
+ }
92
+ function parseStageArgv(segment) {
93
+ const redirectMatch = segment.match(STDERR_DEV_NULL_RE);
94
+ const withoutRedirect = redirectMatch?.[1] ?? segment;
95
+ if (withoutRedirect.includes('>'))
96
+ return null;
97
+ return parseLiteralShellArgv(withoutRedirect.trim());
98
+ }
99
+ function exact(value) {
100
+ return { value, access: 'exact' };
101
+ }
102
+ function recursive(value) {
103
+ return { value, access: 'recursive' };
104
+ }
105
+ function parseOptionsAndPaths(args, valueOptions, recursiveOptions = new Set()) {
106
+ const paths = [];
107
+ let optionsEnded = false;
108
+ let recursiveAccess = false;
109
+ for (let index = 0; index < args.length; index++) {
110
+ const argument = args[index];
111
+ if (!optionsEnded && argument === '--') {
112
+ optionsEnded = true;
113
+ continue;
114
+ }
115
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
116
+ const option = argument.split('=', 1)[0];
117
+ if (recursiveOptions.has(option) || [...recursiveOptions].some(entry => entry.length === 2 && argument.startsWith('-') && !argument.startsWith('--') && argument.slice(1).includes(entry[1]))) {
118
+ recursiveAccess = true;
119
+ }
120
+ if (!argument.includes('=') && valueOptions.has(option)) {
121
+ if (++index >= args.length)
122
+ return null;
123
+ }
124
+ continue;
125
+ }
126
+ if (argument !== '-')
127
+ paths.push(argument);
128
+ }
129
+ return { paths, recursive: recursiveAccess };
130
+ }
131
+ function analyzeLs(args) {
132
+ if (args.some(argument => argument === '--dereference'
133
+ || (!argument.startsWith('--') && /^-[^-]*L/.test(argument))))
134
+ return null;
135
+ const parsed = parseOptionsAndPaths(args, new Set([
136
+ '-I', '-T', '-w', '--block-size', '--color', '--format', '--hide', '--ignore',
137
+ '--indicator-style', '--quoting-style', '--sort', '--tabsize', '--time', '--time-style',
138
+ ]), new Set(['-R', '--recursive']));
139
+ if (!parsed)
140
+ return null;
141
+ const values = parsed.paths.length > 0 ? parsed.paths : ['.'];
142
+ return values.map(value => parsed.recursive ? recursive(value) : exact(value));
143
+ }
144
+ function analyzeCat(args) {
145
+ const parsed = parseOptionsAndPaths(args, new Set());
146
+ return parsed?.paths.map(exact) ?? null;
147
+ }
148
+ function consumeOptionValue(args, index, inline) {
149
+ if (inline)
150
+ return index;
151
+ return index + 1 < args.length ? index + 1 : null;
152
+ }
153
+ function analyzeGrep(args) {
154
+ const paths = [];
155
+ let optionsEnded = false;
156
+ let hasExplicitPattern = false;
157
+ let implicitPatternSeen = false;
158
+ let inputOperandSeen = false;
159
+ let recursiveAccess = false;
160
+ const textValueOptions = new Set([
161
+ '-A', '-B', '-C', '-m', '--after-context', '--before-context', '--binary-files',
162
+ '--color', '--context', '--directories', '--exclude', '--exclude-dir', '--group-separator',
163
+ '--include', '--label', '--max-count',
164
+ ]);
165
+ const patternFileOptions = new Set(['-f', '--file']);
166
+ const auxiliaryFileOptions = new Set(['--exclude-from']);
167
+ for (let index = 0; index < args.length; index++) {
168
+ const argument = args[index];
169
+ if (!optionsEnded && argument === '--') {
170
+ optionsEnded = true;
171
+ continue;
172
+ }
173
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
174
+ const shortBody = argument.startsWith('--') ? '' : argument.slice(1);
175
+ const inlineValueOption = shortBody.length > 1 && ['d', 'e', 'f'].includes(shortBody[0]);
176
+ if (!inlineValueOption && /[def]/.test(shortBody.slice(1)))
177
+ return null;
178
+ if (argument === '-R' || argument === '--dereference-recursive'
179
+ || (!inlineValueOption && shortBody.includes('R')))
180
+ return null;
181
+ if (argument === '-r' || argument === '--recursive'
182
+ || (!inlineValueOption && shortBody.includes('r'))) {
183
+ recursiveAccess = true;
184
+ }
185
+ const option = argument.split('=', 1)[0];
186
+ if (option === '--directories' || argument === '-d' || (!argument.startsWith('--') && argument.startsWith('-d'))) {
187
+ const inline = argument.includes('=') || (!argument.startsWith('--') && argument.length > 2);
188
+ const consumed = consumeOptionValue(args, index, inline);
189
+ if (consumed === null)
190
+ return null;
191
+ const value = argument.includes('=')
192
+ ? argument.slice(argument.indexOf('=') + 1)
193
+ : inline
194
+ ? argument.slice(2)
195
+ : args[consumed];
196
+ if (value === 'recurse')
197
+ recursiveAccess = true;
198
+ index = consumed;
199
+ continue;
200
+ }
201
+ const inline = argument.includes('=') || (!argument.startsWith('--') && argument.length > 2
202
+ && (argument[1] === 'e' || argument[1] === 'f'));
203
+ if (option === '-e' || option === '--regexp' || (!argument.startsWith('--') && argument.startsWith('-e'))) {
204
+ const consumed = consumeOptionValue(args, index, inline);
205
+ if (consumed === null)
206
+ return null;
207
+ index = consumed;
208
+ hasExplicitPattern = true;
209
+ continue;
210
+ }
211
+ const patternFileOption = patternFileOptions.has(option)
212
+ || (!argument.startsWith('--') && argument.startsWith('-f'));
213
+ const pathOption = patternFileOption || auxiliaryFileOptions.has(option);
214
+ if (pathOption) {
215
+ const consumed = consumeOptionValue(args, index, inline);
216
+ if (consumed === null)
217
+ return null;
218
+ if (inline) {
219
+ const value = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : argument.slice(2);
220
+ if (value)
221
+ paths.push(exact(value));
222
+ }
223
+ else {
224
+ paths.push(exact(args[consumed]));
225
+ }
226
+ index = consumed;
227
+ if (patternFileOption)
228
+ hasExplicitPattern = true;
229
+ continue;
230
+ }
231
+ if (textValueOptions.has(option)) {
232
+ const consumed = consumeOptionValue(args, index, argument.includes('='));
233
+ if (consumed === null)
234
+ return null;
235
+ index = consumed;
236
+ }
237
+ continue;
238
+ }
239
+ if (!hasExplicitPattern && !implicitPatternSeen) {
240
+ implicitPatternSeen = true;
241
+ continue;
242
+ }
243
+ inputOperandSeen = true;
244
+ if (argument !== '-')
245
+ paths.push(recursiveAccess ? recursive(argument) : exact(argument));
246
+ }
247
+ if (!hasExplicitPattern && !implicitPatternSeen)
248
+ return null;
249
+ if (recursiveAccess && !inputOperandSeen)
250
+ paths.push(recursive('.'));
251
+ return paths;
252
+ }
253
+ function analyzeRg(args) {
254
+ if (args.some(argument => RG_PROCESS_SPAWNING_OPTION_RE.test(argument)
255
+ || argument === '--follow'
256
+ // `-z` is the short form of `--search-zip`; it may also appear in a
257
+ // short-option cluster such as `-nz`. Once -e/-f/-g starts an attached
258
+ // value, later letters belong to that value rather than the option set.
259
+ || (argument.startsWith('-') && !argument.startsWith('--') && (() => {
260
+ const body = argument.slice(1);
261
+ if (body.length > 1 && ['e', 'f', 'g'].includes(body[0]))
262
+ return false;
263
+ if (/[efg]/.test(body.slice(1)))
264
+ return true;
265
+ return /[Lz]/.test(body);
266
+ })())))
267
+ return null;
268
+ const paths = [];
269
+ let optionsEnded = false;
270
+ let hasExplicitPattern = false;
271
+ let implicitPatternSeen = false;
272
+ let filesMode = false;
273
+ let inputOperandSeen = false;
274
+ const textValueOptions = new Set([
275
+ '-A', '-B', '-C', '-E', '-M', '-g', '-j', '-m', '-r', '-t', '-T',
276
+ '--after-context', '--before-context', '--color', '--colors', '--context', '--context-separator',
277
+ '--dfa-size-limit', '--encoding', '--engine', '--field-context-separator', '--field-match-separator',
278
+ '--glob', '--iglob', '--max-columns', '--max-count', '--max-depth', '--max-filesize', '--path-separator',
279
+ '--regex-size-limit', '--replace', '--sort', '--sortr', '--type', '--type-add', '--type-clear', '--type-not',
280
+ ]);
281
+ const patternFileOptions = new Set(['-f', '--file']);
282
+ const auxiliaryFileOptions = new Set(['--ignore-file']);
283
+ for (let index = 0; index < args.length; index++) {
284
+ const argument = args[index];
285
+ if (!optionsEnded && argument === '--') {
286
+ optionsEnded = true;
287
+ continue;
288
+ }
289
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
290
+ const option = argument.split('=', 1)[0];
291
+ if (['--files', '--files-with-matches', '--files-without-match', '--type-list'].includes(option))
292
+ filesMode = true;
293
+ const inlineShort = !argument.startsWith('--') && argument.length > 2
294
+ && (argument[1] === 'e' || argument[1] === 'f' || argument[1] === 'g');
295
+ if (option === '-e' || option === '--regexp' || (!argument.startsWith('--') && argument.startsWith('-e'))) {
296
+ const consumed = consumeOptionValue(args, index, argument.includes('=') || inlineShort);
297
+ if (consumed === null)
298
+ return null;
299
+ index = consumed;
300
+ hasExplicitPattern = true;
301
+ continue;
302
+ }
303
+ const patternFileOption = patternFileOptions.has(option)
304
+ || (!argument.startsWith('--') && argument.startsWith('-f'));
305
+ const pathOption = patternFileOption || auxiliaryFileOptions.has(option);
306
+ if (pathOption) {
307
+ const inline = argument.includes('=') || inlineShort;
308
+ const consumed = consumeOptionValue(args, index, inline);
309
+ if (consumed === null)
310
+ return null;
311
+ if (inline) {
312
+ const value = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : argument.slice(2);
313
+ if (value)
314
+ paths.push(exact(value));
315
+ }
316
+ else {
317
+ paths.push(exact(args[consumed]));
318
+ }
319
+ index = consumed;
320
+ if (patternFileOption)
321
+ hasExplicitPattern = true;
322
+ continue;
323
+ }
324
+ if (textValueOptions.has(option) || (!argument.startsWith('--') && argument.startsWith('-g'))) {
325
+ const consumed = consumeOptionValue(args, index, argument.includes('=') || inlineShort);
326
+ if (consumed === null)
327
+ return null;
328
+ index = consumed;
329
+ }
330
+ continue;
331
+ }
332
+ if (!filesMode && !hasExplicitPattern && !implicitPatternSeen) {
333
+ implicitPatternSeen = true;
334
+ continue;
335
+ }
336
+ inputOperandSeen = true;
337
+ if (argument !== '-')
338
+ paths.push(recursive(argument));
339
+ }
340
+ if (!filesMode && !hasExplicitPattern && !implicitPatternSeen)
341
+ return null;
342
+ if (!inputOperandSeen)
343
+ paths.push(recursive('.'));
344
+ return paths;
345
+ }
346
+ function analyzeFind(args) {
347
+ if (args.some(argument => FIND_MUTATING_PRIMARY_RE.test(argument)))
348
+ return null;
349
+ if (args.includes('-L') || args.includes('-H') || args.includes('-follow'))
350
+ return null;
351
+ const paths = [];
352
+ let index = 0;
353
+ while (index < args.length && (args[index] === '-P' || args[index].startsWith('-O') || args[index] === '-D')) {
354
+ if (args[index] === '-D') {
355
+ if (++index >= args.length)
356
+ return null;
357
+ }
358
+ index++;
359
+ }
360
+ while (index < args.length && !args[index].startsWith('-') && !['!', '(', ')'].includes(args[index])) {
361
+ paths.push(recursive(args[index++]));
362
+ }
363
+ if (paths.length === 0)
364
+ paths.push(recursive('.'));
365
+ const noValue = new Set([
366
+ '!', '(', ')', '-a', '-and', '-d', '-daystart', '-depth', '-empty', '-executable', '-false',
367
+ '-help', '--help', '-ignore_readdir_race', '-ls', '-mount', '-noignore_readdir_race', '-noleaf',
368
+ '-not', '-o', '-or', '-print', '-print0', '-prune', '-quit', '-readable', '-true', '-version',
369
+ '--version', '-writable', '-xdev',
370
+ ]);
371
+ const textValue = new Set([
372
+ '-amin', '-atime', '-cmin', '-ctime', '-fstype', '-gid', '-group', '-ilname', '-iname', '-inum',
373
+ '-ipath', '-iregex', '-iwholename', '-links', '-lname', '-maxdepth', '-mindepth', '-mmin', '-mtime',
374
+ '-name', '-path', '-perm', '-printf', '-regex', '-regextype', '-size', '-type', '-uid', '-used',
375
+ '-user', '-wholename', '-xtype',
376
+ ]);
377
+ const pathValue = new Set(['-anewer', '-cnewer', '-newer', '-samefile']);
378
+ while (index < args.length) {
379
+ const argument = args[index++];
380
+ // The file contents become additional starting points, so their protected
381
+ // path semantics cannot be proven from argv alone.
382
+ if (argument === '-files0-from')
383
+ return null;
384
+ if (noValue.has(argument))
385
+ continue;
386
+ if (textValue.has(argument)) {
387
+ if (index >= args.length)
388
+ return null;
389
+ index++;
390
+ continue;
391
+ }
392
+ if (/^-newer[aBcm]?t$/.test(argument)) {
393
+ if (index >= args.length)
394
+ return null;
395
+ index++;
396
+ continue;
397
+ }
398
+ if (/^-newer[A-Za-z]{2}$/.test(argument) || pathValue.has(argument)) {
399
+ if (index >= args.length)
400
+ return null;
401
+ paths.push(exact(args[index++]));
402
+ continue;
403
+ }
404
+ return null;
405
+ }
406
+ return paths;
407
+ }
408
+ function analyzeSed(args) {
409
+ if (!['-n', '--quiet', '--silent'].includes(args[0]))
410
+ return null;
411
+ const program = args[1];
412
+ if (!program || !/^(?:p|(?:\d+|\$)(?:,(?:\d+|\$))?p)$/.test(program))
413
+ return null;
414
+ if (args.slice(2).some(argument => argument.startsWith('-') && argument !== '-'))
415
+ return null;
416
+ return args.slice(2).filter(argument => argument !== '-').map(exact);
417
+ }
418
+ function analyzeHead(args) {
419
+ const parsed = parseOptionsAndPaths(args, new Set(['-c', '-n', '--bytes', '--lines']));
420
+ return parsed?.paths.map(exact) ?? null;
421
+ }
422
+ function analyzeSort(args) {
423
+ if (args.some(argument => /^(?:-o|-T)(?:.+|$)|^(?:--output|--compress-program|--temporary-directory|--files0-from)(?:=|$)/.test(argument))) {
424
+ return null;
425
+ }
426
+ const paths = [];
427
+ let optionsEnded = false;
428
+ const textValueOptions = new Set([
429
+ '-k', '-S', '-t', '--batch-size', '--buffer-size', '--field-separator', '--key', '--parallel', '--sort',
430
+ ]);
431
+ for (let index = 0; index < args.length; index++) {
432
+ const argument = args[index];
433
+ if (!optionsEnded && argument === '--') {
434
+ optionsEnded = true;
435
+ continue;
436
+ }
437
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
438
+ const option = argument.split('=', 1)[0];
439
+ if (option === '--random-source') {
440
+ const inline = argument.includes('=');
441
+ const consumed = consumeOptionValue(args, index, inline);
442
+ if (consumed === null)
443
+ return null;
444
+ paths.push(exact(inline ? argument.slice(argument.indexOf('=') + 1) : args[consumed]));
445
+ index = consumed;
446
+ continue;
447
+ }
448
+ if (textValueOptions.has(option)) {
449
+ const consumed = consumeOptionValue(args, index, argument.includes('='));
450
+ if (consumed === null)
451
+ return null;
452
+ index = consumed;
453
+ }
454
+ continue;
455
+ }
456
+ if (argument !== '-')
457
+ paths.push(exact(argument));
458
+ }
459
+ return paths;
460
+ }
461
+ function analyzeCommand(argv) {
462
+ const executable = normalizeExecutable(argv[0]);
463
+ if (!executable)
464
+ return null;
465
+ const args = argv.slice(1);
466
+ let pathOperands = null;
467
+ switch (executable) {
468
+ case 'echo':
469
+ pathOperands = [];
470
+ break;
471
+ case 'pwd':
472
+ pathOperands = args.every(argument => ['-L', '-P', '--help', '--version'].includes(argument)) ? [] : null;
473
+ break;
474
+ case 'ls':
475
+ pathOperands = analyzeLs(args);
476
+ break;
477
+ case 'cat':
478
+ pathOperands = analyzeCat(args);
479
+ break;
480
+ case 'grep':
481
+ pathOperands = analyzeGrep(args);
482
+ break;
483
+ case 'rg':
484
+ pathOperands = analyzeRg(args);
485
+ break;
486
+ case 'find':
487
+ pathOperands = analyzeFind(args);
488
+ break;
489
+ case 'sed':
490
+ pathOperands = analyzeSed(args);
491
+ break;
492
+ case 'head':
493
+ pathOperands = analyzeHead(args);
494
+ break;
495
+ case 'sort':
496
+ pathOperands = analyzeSort(args);
497
+ break;
498
+ }
499
+ if (!pathOperands)
500
+ return null;
501
+ return { command: { executable, argv, pathOperands } };
502
+ }
503
+ export function analyzeReadonlyShellQuery(command) {
504
+ const structure = splitQueryStructure(command);
505
+ if (!structure)
506
+ return { kind: 'unproven', reason: 'unsupported-shell-structure' };
507
+ const pipelines = [];
508
+ const pathOperands = [];
509
+ for (const stages of structure) {
510
+ const pipeline = [];
511
+ for (let index = 0; index < stages.length; index++) {
512
+ const argv = parseStageArgv(stages[index]);
513
+ if (!argv)
514
+ return { kind: 'unproven', reason: 'non-literal-command' };
515
+ const analyzed = analyzeCommand(argv);
516
+ if (!analyzed)
517
+ return { kind: 'unproven', reason: 'unsupported-command-or-option' };
518
+ if (index > 0) {
519
+ if (!['head', 'sort'].includes(analyzed.command.executable)) {
520
+ return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
521
+ }
522
+ if (analyzed.command.pathOperands.length > 0) {
523
+ return { kind: 'unproven', reason: 'pipeline-filter-must-use-stdin' };
524
+ }
525
+ }
526
+ pipeline.push(analyzed.command);
527
+ pathOperands.push(...analyzed.command.pathOperands);
528
+ }
529
+ pipelines.push(pipeline);
530
+ }
531
+ return { kind: 'proven-readonly', ir: { pipelines, pathOperands } };
532
+ }
@@ -0,0 +1,46 @@
1
+ const SHELL_CODE_INJECTION_ENV = new Set([
2
+ 'BASH_ENV',
3
+ 'BASHOPTS',
4
+ 'BASH_XTRACEFD',
5
+ 'CDPATH',
6
+ 'ENV',
7
+ 'GCONV_PATH',
8
+ 'GREP_OPTIONS',
9
+ 'LD_AUDIT',
10
+ 'LD_LIBRARY_PATH',
11
+ 'LD_PRELOAD',
12
+ 'LOCPATH',
13
+ 'NODE_OPTIONS',
14
+ 'NODE_PATH',
15
+ 'PROMPT_COMMAND',
16
+ 'PS4',
17
+ 'RIPGREP_CONFIG_PATH',
18
+ 'SHELLOPTS',
19
+ 'DYLD_FRAMEWORK_PATH',
20
+ 'DYLD_INSERT_LIBRARIES',
21
+ 'DYLD_LIBRARY_PATH',
22
+ ]);
23
+ const EXECUTABLE_RESOLUTION_ENV = ['PATH', 'HOME', 'SHELL'];
24
+ export function isShellCodeInjectionEnvironmentName(name) {
25
+ return SHELL_CODE_INJECTION_ENV.has(name)
26
+ || name.startsWith('BASH_FUNC_')
27
+ || name.startsWith('LD_')
28
+ || name.startsWith('DYLD_');
29
+ }
30
+ export function sanitizeShellExecutionEnvironment(input, options = {}) {
31
+ const sanitized = { ...input };
32
+ for (const name of Object.keys(sanitized)) {
33
+ if (isShellCodeInjectionEnvironmentName(name))
34
+ delete sanitized[name];
35
+ }
36
+ if (options.lockExecutableResolution) {
37
+ const baseline = options.baseline ?? process.env;
38
+ for (const name of EXECUTABLE_RESOLUTION_ENV) {
39
+ if (baseline[name] === undefined)
40
+ delete sanitized[name];
41
+ else
42
+ sanitized[name] = baseline[name];
43
+ }
44
+ }
45
+ return sanitized;
46
+ }