evolcore 0.0.12 → 0.0.13

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 (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
@@ -14,6 +14,66 @@ const EVOLCORE_COMMAND_POSITION_RE = /(?:^|[;&|()`])\s*ec(?=\s|[;&|()<>`]|$)/i;
14
14
  function trimShellSpacing(value) {
15
15
  return value.replace(/^[ \t]+|[ \t]+$/g, '');
16
16
  }
17
+ const MANAGED_TMPDIR_TOKEN = '__EC_MANAGED_TMPDIR__';
18
+ /**
19
+ * Preserve the one environment reference allowed in managed EC paths.
20
+ * The regular literal parser still rejects all expansion by default. The
21
+ * privileged EC classifier opts into this token only after the surrounding
22
+ * policy has validated the path against the session-owned TMPDIR.
23
+ */
24
+ function substituteManagedTmpDirToken(value) {
25
+ let result = '';
26
+ let quote = null;
27
+ for (let index = 0; index < value.length; index++) {
28
+ const char = value[index];
29
+ if (quote === 'single') {
30
+ result += char;
31
+ if (char === "'")
32
+ quote = null;
33
+ continue;
34
+ }
35
+ if (quote === 'double') {
36
+ if (char === '\\') {
37
+ result += char;
38
+ if (index + 1 < value.length)
39
+ result += value[++index];
40
+ continue;
41
+ }
42
+ if (char === '"') {
43
+ quote = null;
44
+ result += char;
45
+ continue;
46
+ }
47
+ }
48
+ else if (char === "'") {
49
+ quote = 'single';
50
+ result += char;
51
+ continue;
52
+ }
53
+ else if (char === '\\') {
54
+ result += char;
55
+ if (index + 1 < value.length)
56
+ result += value[++index];
57
+ continue;
58
+ }
59
+ if (value.startsWith('${TMPDIR}', index)) {
60
+ result += MANAGED_TMPDIR_TOKEN;
61
+ index += '${TMPDIR}'.length - 1;
62
+ }
63
+ else if (value.startsWith('$TMPDIR', index)
64
+ && !/[A-Za-z0-9_]/.test(value[index + '$TMPDIR'.length] ?? '')) {
65
+ result += MANAGED_TMPDIR_TOKEN;
66
+ index += '$TMPDIR'.length - 1;
67
+ }
68
+ else {
69
+ result += char;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ function restoreManagedTmpDirToken(value, managedTempDir) {
75
+ return value.replaceAll(MANAGED_TMPDIR_TOKEN, managedTempDir ?? '$TMPDIR');
76
+ }
17
77
  function isShellSpacing(value) {
18
78
  return value === ' ' || value === '\t';
19
79
  }
@@ -41,8 +101,10 @@ function doubleQuotedIssueAt(input, index) {
41
101
  * expansion, glob, pipeline, command substitution, redirection, or comment.
42
102
  * Anything outside this subset is never granted the privileged EC path.
43
103
  */
44
- export function parseLiteralShellCommand(command) {
45
- const input = trimShellSpacing(command);
104
+ export function parseLiteralShellCommand(command, options = {}) {
105
+ const input = trimShellSpacing(options.allowManagedTmpDir
106
+ ? substituteManagedTmpDirToken(command)
107
+ : command);
46
108
  if (!input)
47
109
  return { ok: false, issue: 'empty-command' };
48
110
  if (input.includes('\0'))
@@ -135,11 +197,16 @@ export function parseLiteralShellCommand(command) {
135
197
  return { ok: false, issue: 'invalid-quote' };
136
198
  finishToken();
137
199
  return argv.length > 0
138
- ? { ok: true, argv }
200
+ ? {
201
+ ok: true,
202
+ argv: options.allowManagedTmpDir
203
+ ? argv.map(value => restoreManagedTmpDirToken(value, options.managedTempDir))
204
+ : argv,
205
+ }
139
206
  : { ok: false, issue: 'empty-command' };
140
207
  }
141
- export function parseLiteralShellArgv(command) {
142
- const result = parseLiteralShellCommand(command);
208
+ export function parseLiteralShellArgv(command, options = {}) {
209
+ const result = parseLiteralShellCommand(command, options);
143
210
  return result.ok ? result.argv : null;
144
211
  }
145
212
  /**
@@ -157,12 +224,12 @@ export function unwrapCodexShellCommandArgv(argv) {
157
224
  * Codex app-server. This does not make shell composition safe: the nested
158
225
  * command still has to pass `parseLiteralShellCommand`.
159
226
  */
160
- export function parseCodexShellCommandArgv(command) {
161
- const outer = parseLiteralShellArgv(command);
227
+ export function parseCodexShellCommandArgv(command, options = {}) {
228
+ const outer = parseLiteralShellArgv(command, options);
162
229
  if (!outer)
163
230
  return null;
164
231
  const wrapped = unwrapCodexShellCommandArgv(outer);
165
- return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
232
+ return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped, options);
166
233
  }
167
234
  /**
168
235
  * Resolve a Codex command input while preserving the raw `commandArgv` form
@@ -173,10 +240,12 @@ export function parseCodexToolCommandArgv(input) {
173
240
  if (Array.isArray(input.commandArgv) && input.commandArgv.every(value => typeof value === 'string')) {
174
241
  const outer = input.commandArgv;
175
242
  const wrapped = unwrapCodexShellCommandArgv(outer);
176
- return wrapped === undefined ? outer : parseLiteralShellArgv(wrapped);
243
+ return wrapped === undefined
244
+ ? outer
245
+ : parseLiteralShellArgv(wrapped, { allowManagedTmpDir: true });
177
246
  }
178
247
  const command = typeof input.command === 'string' ? input.command : '';
179
- return parseCodexShellCommandArgv(command);
248
+ return parseCodexShellCommandArgv(command, { allowManagedTmpDir: true });
180
249
  }
181
250
  function scanBoundedOutputOperators(command) {
182
251
  const operators = [];
@@ -290,7 +359,7 @@ function parseStdinOnlyHeadArgs(segment) {
290
359
  * optional stderr merge, one bounded `head`, and one create-only output path.
291
360
  */
292
361
  export function parseBoundedOutputShellCommand(command) {
293
- const input = trimShellSpacing(command);
362
+ const input = trimShellSpacing(substituteManagedTmpDirToken(command));
294
363
  if (!input)
295
364
  return null;
296
365
  const operators = scanBoundedOutputOperators(input);
@@ -311,6 +380,10 @@ export function parseBoundedOutputShellCommand(command) {
311
380
  const argv = parseLiteralShellArgv(sourceText);
312
381
  if (!argv)
313
382
  return null;
383
+ // `$TMPDIR` is a transport-level path reference, never an argument
384
+ // expansion for the privileged command itself.
385
+ if (argv.some(value => value.includes(MANAGED_TMPDIR_TOKEN)))
386
+ return null;
314
387
  let mergeStderr = false;
315
388
  let pipe;
316
389
  let redirect;
@@ -345,7 +418,12 @@ export function parseBoundedOutputShellCommand(command) {
345
418
  const outputArgv = parseLiteralShellArgv(trimShellSpacing(input.slice(redirect.end)));
346
419
  if (!outputArgv || outputArgv.length !== 1)
347
420
  return null;
348
- outputPath = outputArgv[0];
421
+ // A single-quoted or escaped `$TMPDIR` is literal shell text, not the
422
+ // managed directory reference. Do not reinterpret it as a route to the
423
+ // session temp root.
424
+ if (outputArgv[0].includes('$') && !outputArgv[0].includes(MANAGED_TMPDIR_TOKEN))
425
+ return null;
426
+ outputPath = restoreManagedTmpDirToken(outputArgv[0]);
349
427
  }
350
428
  return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
351
429
  }
@@ -355,13 +433,14 @@ export function parseBoundedOutputShellCommand(command) {
355
433
  * invocation may leave the sandbox, while shell composition may not.
356
434
  */
357
435
  export function classifyEvolcoreShellCommand(command) {
358
- const literal = parseLiteralShellCommand(command);
436
+ const parseOptions = { allowManagedTmpDir: true };
437
+ const literal = parseLiteralShellCommand(command, parseOptions);
359
438
  if (literal.ok) {
360
439
  if (literal.argv[0] === 'ec')
361
440
  return { kind: 'literal' };
362
441
  const wrapped = unwrapCodexShellCommandArgv(literal.argv);
363
442
  if (wrapped !== undefined) {
364
- const nested = parseLiteralShellCommand(wrapped);
443
+ const nested = parseLiteralShellCommand(wrapped, parseOptions);
365
444
  if (nested.ok && nested.argv[0] === 'ec')
366
445
  return { kind: 'literal' };
367
446
  if (!nested.ok && EVOLCORE_COMMAND_POSITION_RE.test(wrapped)) {
@@ -2,6 +2,6 @@ export * from './tool-policy.js';
2
2
  export * from './ec-command-parser.js';
3
3
  export * from './readonly-shell-query.js';
4
4
  export * from './approval-gateway.js';
5
- export * from './execution-sandbox.js';
5
+ export * from './mode.js';
6
6
  // Tool summaries live in utils; keep the historical permission import path compatible.
7
7
  export { summarizeToolInput } from '../../utils/tool-summary.js';
@@ -22,3 +22,18 @@ export function normalizePermissionMode(value) {
22
22
  return { mode: 'readonly', workflow: 'plan', migratedFrom: 'plan' };
23
23
  return { mode: 'readonly' };
24
24
  }
25
+ /**
26
+ * Phase one only disables the task execution sandbox for an authenticated
27
+ * owner running in bypass mode. Every other combination remains fail-closed.
28
+ */
29
+ export function resolvePhaseOneExecutionSandbox(permissionMode, role) {
30
+ const normalizedMode = normalizePermissionMode(permissionMode).mode;
31
+ const normalizedRole = role ?? undefined;
32
+ const sandboxOff = normalizedMode === 'bypass' && normalizedRole === 'owner';
33
+ return {
34
+ permissionMode: normalizedMode,
35
+ role: normalizedRole,
36
+ state: sandboxOff ? 'off' : 'active',
37
+ reason: sandboxOff ? 'owner-bypass' : 'phase-one-required',
38
+ };
39
+ }
@@ -3,7 +3,7 @@ import { parseLiteralShellArgv } from './ec-command-parser.js';
3
3
  const MAX_QUERY_LISTS = 8;
4
4
  const MAX_PIPELINE_STAGES = 3;
5
5
  const SUPPORTED_EXECUTABLES = new Set([
6
- 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort',
6
+ 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat',
7
7
  ]);
8
8
  const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
9
9
  const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
@@ -71,8 +71,17 @@ function splitQueryStructure(command) {
71
71
  segment += char;
72
72
  continue;
73
73
  }
74
- if (char === '\0' || char === '\r' || char === '\n' || char === '&' || char === '<')
74
+ if (char === '\0' || char === '\r' || char === '\n' || char === '<')
75
75
  return null;
76
+ if (char === '&') {
77
+ // A bounded sequence of independently proven read-only commands is
78
+ // safe to run in order. A single ampersand would background execution;
79
+ // `||` remains rejected because it introduces conditional control flow.
80
+ if (command[index + 1] !== '&' || !finishPipeline())
81
+ return null;
82
+ index++;
83
+ continue;
84
+ }
76
85
  if (char === '|') {
77
86
  if (command[index + 1] === '|' || !finishStage())
78
87
  return null;
@@ -89,12 +98,14 @@ function splitQueryStructure(command) {
89
98
  return null;
90
99
  return pipelines;
91
100
  }
92
- function parseStageArgv(segment) {
101
+ function parseStageArgv(segment, options) {
93
102
  const redirectMatch = segment.match(STDERR_DEV_NULL_RE);
94
103
  const withoutRedirect = redirectMatch?.[1] ?? segment;
95
104
  if (withoutRedirect.includes('>'))
96
105
  return null;
97
- return parseLiteralShellArgv(withoutRedirect.trim());
106
+ return parseLiteralShellArgv(withoutRedirect.trim(), options.managedTempDir
107
+ ? { allowManagedTmpDir: true, managedTempDir: options.managedTempDir }
108
+ : undefined);
98
109
  }
99
110
  function exact(value) {
100
111
  return { value, access: 'exact' };
@@ -458,12 +469,118 @@ function analyzeSort(args) {
458
469
  }
459
470
  return paths;
460
471
  }
472
+ /**
473
+ * Git metadata queries do not expose file contents and their output paths are
474
+ * controlled by the repository, not by arbitrary shell operands. Keep this
475
+ * deliberately small: status and read-only diff summaries are the patterns
476
+ * used for workspace diagnostics. Mutating subcommands and pathspecs remain
477
+ * unproven so the normal H-class checks still apply.
478
+ */
479
+ function analyzeGit(args) {
480
+ if (args.length === 0)
481
+ return null;
482
+ const subcommand = args[0];
483
+ if (subcommand === 'status') {
484
+ // Keep the allow-list explicit; callers should request a bounded metadata
485
+ // view such as `--short` or `--branch`, rather than inheriting Git's full
486
+ // default output and any future options.
487
+ if (args.length === 1)
488
+ return null;
489
+ const allowed = new Set([
490
+ '--short', '--porcelain', '--porcelain=v1', '--porcelain=v2', '--branch',
491
+ '--untracked-files=no', '--untracked-files=normal', '--ignored=no',
492
+ '--ignore-submodules', '--renames', '--no-renames', '--ahead-behind',
493
+ '--no-ahead-behind', '--show-stash', '--no-optional-locks',
494
+ ]);
495
+ for (const arg of args.slice(1)) {
496
+ if (arg === '--')
497
+ return null;
498
+ if (!allowed.has(arg))
499
+ return null;
500
+ }
501
+ return [];
502
+ }
503
+ if (subcommand === 'diff') {
504
+ let sawSummary = false;
505
+ let optionsEnded = false;
506
+ const paths = [];
507
+ const allowedFlags = new Set([
508
+ '--stat', '--shortstat', '--numstat', '--name-only', '--name-status',
509
+ '--no-ext-diff', '--no-renames', '--no-textconv', '--no-index',
510
+ '--cached', '--staged', '--merge-base', '--no-color', '--color=never',
511
+ ]);
512
+ for (const arg of args.slice(1)) {
513
+ if (!optionsEnded && arg === '--') {
514
+ optionsEnded = true;
515
+ continue;
516
+ }
517
+ if (!optionsEnded && arg.startsWith('-')) {
518
+ if (arg === '--stat' || arg === '--shortstat' || arg === '--numstat'
519
+ || arg === '--name-only' || arg === '--name-status')
520
+ sawSummary = true;
521
+ if (!allowedFlags.has(arg) && !/^[0-9a-f]{7,64}$/.test(arg))
522
+ return null;
523
+ continue;
524
+ }
525
+ // Revisions are safe literals; pathspecs after -- are bounded exact reads.
526
+ if (optionsEnded)
527
+ paths.push({ value: arg, access: 'exact' });
528
+ else if (!/^[0-9A-Za-z._~^/:-]+$/.test(arg))
529
+ return null;
530
+ }
531
+ return sawSummary ? paths : null;
532
+ }
533
+ return null;
534
+ }
535
+ function analyzeWc(args) {
536
+ const allowed = new Set(['-l', '-w', '-c', '-m', '-L', '--lines', '--words', '--bytes', '--chars', '--max-line-length']);
537
+ const paths = [];
538
+ for (const arg of args) {
539
+ if (arg.startsWith('-')) {
540
+ if (!allowed.has(arg))
541
+ return null;
542
+ }
543
+ else if (arg !== '-') {
544
+ paths.push(exact(arg));
545
+ }
546
+ }
547
+ return paths;
548
+ }
549
+ function analyzeStat(args) {
550
+ // GNU stat defaults to lstat semantics. Following a final symlink would
551
+ // let a metadata-only query reach a protected target through an alias.
552
+ if (args.some(argument => argument === '-L' || argument === '--dereference'))
553
+ return null;
554
+ const valueOptions = new Set(['-c', '--format', '--printf']);
555
+ const paths = [];
556
+ let optionsEnded = false;
557
+ for (let index = 0; index < args.length; index++) {
558
+ const argument = args[index];
559
+ if (!optionsEnded && argument === '--') {
560
+ optionsEnded = true;
561
+ continue;
562
+ }
563
+ if (!optionsEnded && argument.startsWith('-') && argument !== '-') {
564
+ const option = argument.split('=', 1)[0];
565
+ if (valueOptions.has(option) && !argument.includes('=') && ++index >= args.length)
566
+ return null;
567
+ if (!valueOptions.has(option) && !new Set(['-f', '--file-system', '-t', '--terse', '--cached']).has(option)) {
568
+ return null;
569
+ }
570
+ continue;
571
+ }
572
+ if (argument !== '-')
573
+ paths.push(exact(argument));
574
+ }
575
+ return paths.length > 0 ? paths : null;
576
+ }
461
577
  function analyzeCommand(argv) {
462
578
  const executable = normalizeExecutable(argv[0]);
463
579
  if (!executable)
464
580
  return null;
465
581
  const args = argv.slice(1);
466
582
  let pathOperands = null;
583
+ let accessKind = 'content';
467
584
  switch (executable) {
468
585
  case 'echo':
469
586
  pathOperands = [];
@@ -495,12 +612,35 @@ function analyzeCommand(argv) {
495
612
  case 'sort':
496
613
  pathOperands = analyzeSort(args);
497
614
  break;
615
+ case 'git':
616
+ pathOperands = analyzeGit(args);
617
+ break;
618
+ case 'wc':
619
+ pathOperands = analyzeWc(args);
620
+ break;
621
+ case 'stat':
622
+ case 'lstat':
623
+ pathOperands = analyzeStat(args);
624
+ accessKind = 'metadata';
625
+ break;
498
626
  }
499
627
  if (!pathOperands)
500
628
  return null;
501
- return { command: { executable, argv, pathOperands } };
629
+ if (['ls', 'find'].includes(executable))
630
+ accessKind = 'enumerate';
631
+ return {
632
+ command: {
633
+ executable,
634
+ argv,
635
+ pathOperands,
636
+ // Keep the established IR shape for ordinary queries. Only metadata
637
+ // commands need an explicit action marker; content/enumeration are
638
+ // derived from their operands by policy consumers.
639
+ ...(accessKind === 'metadata' ? { accessKind } : {}),
640
+ },
641
+ };
502
642
  }
503
- export function analyzeReadonlyShellQuery(command) {
643
+ export function analyzeReadonlyShellQuery(command, options = {}) {
504
644
  const structure = splitQueryStructure(command);
505
645
  if (!structure)
506
646
  return { kind: 'unproven', reason: 'unsupported-shell-structure' };
@@ -509,14 +649,14 @@ export function analyzeReadonlyShellQuery(command) {
509
649
  for (const stages of structure) {
510
650
  const pipeline = [];
511
651
  for (let index = 0; index < stages.length; index++) {
512
- const argv = parseStageArgv(stages[index]);
652
+ const argv = parseStageArgv(stages[index], options);
513
653
  if (!argv)
514
654
  return { kind: 'unproven', reason: 'non-literal-command' };
515
655
  const analyzed = analyzeCommand(argv);
516
656
  if (!analyzed)
517
657
  return { kind: 'unproven', reason: 'unsupported-command-or-option' };
518
658
  if (index > 0) {
519
- if (!['head', 'sort'].includes(analyzed.command.executable)) {
659
+ if (!['head', 'sort', 'grep', 'wc'].includes(analyzed.command.executable)) {
520
660
  return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
521
661
  }
522
662
  if (analyzed.command.pathOperands.length > 0) {
@@ -4,6 +4,27 @@ import { resolveCommandPath } from '../../utils/cross-platform.js';
4
4
  import { getExistingHClassMaskTargets, getExistingLClassReadOnlyTargets, isSameOrDescendant, } from '../protected-paths.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  let cachedBubblewrapPath;
7
+ export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
8
+ /** Recognize sandbox bootstrap failures independently of localized stderr. */
9
+ export function isSandboxInitializationFailure(error, stderr = []) {
10
+ const text = [error instanceof Error ? error.message : String(error ?? ''), ...stderr]
11
+ .join('\n')
12
+ .toLowerCase();
13
+ return text.includes(SANDBOX_INITIALIZATION_FAILED)
14
+ || /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
15
+ || /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
16
+ || /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
17
+ || /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text);
18
+ }
19
+ export function createSandboxInitializationError(error, stderr = []) {
20
+ const detail = error instanceof Error ? error.message : String(error ?? 'sandbox bootstrap failed');
21
+ const wrapped = new Error(`${SANDBOX_INITIALIZATION_FAILED}: ${detail}`);
22
+ wrapped.name = 'SandboxInitializationError';
23
+ wrapped.code = SANDBOX_INITIALIZATION_FAILED;
24
+ if (stderr.length > 0)
25
+ wrapped.stderr = [...stderr].slice(-20);
26
+ return wrapped;
27
+ }
7
28
  /**
8
29
  * Claude Agent SDK's native sandbox is unavailable on Windows. WSL2 reports
9
30
  * itself as Linux, so it remains fail-closed there together with macOS/Linux.