evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -1,8 +1,22 @@
1
1
  export const PUBLIC_PERMISSION_MODES = ['readonly', 'auto', 'request', 'bypass'];
2
+ export const FULL_ACCESS_PERMISSION_MODE = 'fullaccess';
2
3
  export function isPublicPermissionMode(value) {
3
4
  return typeof value === 'string'
4
5
  && PUBLIC_PERMISSION_MODES.includes(value);
5
6
  }
7
+ export function isExecutionPermissionMode(value) {
8
+ return value === FULL_ACCESS_PERMISSION_MODE || isPublicPermissionMode(value);
9
+ }
10
+ /**
11
+ * Normalize a trusted execution-call mode without exposing fullaccess as a
12
+ * role/session configuration value. Legacy persisted public values retain the
13
+ * same migration behavior as normalizePermissionMode().
14
+ */
15
+ export function normalizeExecutionPermissionMode(value) {
16
+ if (value === FULL_ACCESS_PERMISSION_MODE)
17
+ return FULL_ACCESS_PERMISSION_MODE;
18
+ return normalizePermissionMode(value).mode;
19
+ }
6
20
  /**
7
21
  * Normalize persisted legacy values at the runtime boundary.
8
22
  *
@@ -27,13 +41,14 @@ export function normalizePermissionMode(value) {
27
41
  * owner running in bypass mode. Every other combination remains fail-closed.
28
42
  */
29
43
  export function resolvePhaseOneExecutionSandbox(permissionMode, role) {
30
- const normalizedMode = normalizePermissionMode(permissionMode).mode;
44
+ const normalizedMode = normalizeExecutionPermissionMode(permissionMode);
31
45
  const normalizedRole = role ?? undefined;
32
- const sandboxOff = normalizedMode === 'bypass' && normalizedRole === 'owner';
46
+ const fullAccess = normalizedMode === FULL_ACCESS_PERMISSION_MODE;
47
+ const sandboxOff = fullAccess || (normalizedMode === 'bypass' && normalizedRole === 'owner');
33
48
  return {
34
49
  permissionMode: normalizedMode,
35
50
  role: normalizedRole,
36
51
  state: sandboxOff ? 'off' : 'active',
37
- reason: sandboxOff ? 'owner-bypass' : 'phase-one-required',
52
+ reason: fullAccess ? 'fullaccess' : sandboxOff ? 'owner-bypass' : 'phase-one-required',
38
53
  };
39
54
  }
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { resolveRoot } from '../paths.js';
3
+ import { resolveRoot } from '../../paths.js';
4
4
  const H_CLASS_RELATIVE_PATTERNS = [
5
5
  /^daemon\.json$/,
6
6
  /^agents\/defaults\.json$/,
@@ -28,13 +28,13 @@ const H_CLASS_RELATIVE_PATTERNS = [
28
28
  /^data\/outbox(?:\/|$)/,
29
29
  /^data\/handoff(?:\/|$)/,
30
30
  /^agents\/[^/]+\/sessions(?:\/|$)/,
31
- /^agents\/[^/]+\/data\/(?:channels(?:\/|$)|handoff(?:\/|$)|outbox\.jsonl(?:_|__|\.tmp-.*)?$|contact-audit\.jsonl$)/,
31
+ /^agents\/[^/]+\/data\/(?:channels(?:\/|$)|handoff(?:\/|$)|(?:outbox|activity-outbox)\.jsonl(?:_|__|\.tmp-.*)?$|contact-audit\.jsonl$)/,
32
32
  ];
33
33
  const H_CLASS_REFERENCE_PATTERNS = [
34
34
  /(?:^|[^A-Za-z0-9_.-])daemon\.json(?=$|[^A-Za-z0-9_.-])/,
35
35
  /(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|contact-operations\.jsonl|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
36
36
  /(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys|private))(?:[\\/]|$|[\s"';&|<>)},])/,
37
- /(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|sessions|contact-book-audit\.jsonl|feishu-seen-[^/\\\s"']+\.jsonl|[a-z0-9_-]+-greeted-[^/\\\s"']+\.jsonl|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|daemon[\\/](?:message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|control|migrations)|channel-state|instance[\\/]control\.token|outbox|handoff)|agents[\\/][^/\\\s"']+[\\/](?:sessions|data[\\/](?:channels|handoff|outbox\.jsonl|contact-audit\.jsonl)))(?:[\\/]|$|[\s"';&|<>)},])/,
37
+ /(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|sessions|contact-book-audit\.jsonl|feishu-seen-[^/\\\s"']+\.jsonl|[a-z0-9_-]+-greeted-[^/\\\s"']+\.jsonl|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|daemon[\\/](?:message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|control|migrations)|channel-state|instance[\\/]control\.token|outbox|handoff)|agents[\\/][^/\\\s"']+[\\/](?:sessions|data[\\/](?:channels|handoff|(?:outbox|activity-outbox)\.jsonl|contact-audit\.jsonl)))(?:[\\/]|$|[\s"';&|<>)},])/,
38
38
  /(?:^|[/\\\s"'=<>])data[\\/]triggers(?:[\\/]|$|[\s"';&|<>)},])/,
39
39
  /(?:^|[/\\\s"'=<>])(?:[^/\\\s"']+\.json_|[^/\\\s"']+\.json\.migrated|defaults_\d+\.json)(?=$|[\s"';&|<>)},])/,
40
40
  ];
@@ -287,6 +287,7 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
287
287
  path.join('agents', '*', 'data', 'channels', '**'),
288
288
  path.join('agents', '*', 'data', 'handoff', '**'),
289
289
  path.join('agents', '*', 'data', 'outbox.jsonl*'),
290
+ path.join('agents', '*', 'data', 'activity-outbox.jsonl*'),
290
291
  path.join('agents', '*', 'data', 'contact-audit.jsonl'),
291
292
  ];
292
293
  return entries.map(entry => path.join(absoluteRoot, entry));
@@ -369,6 +370,7 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
369
370
  addExistingTarget(targets, path.join(agentRoot, 'data', 'channels'));
370
371
  addExistingTarget(targets, path.join(agentRoot, 'data', 'handoff'));
371
372
  addExistingTarget(targets, path.join(agentRoot, 'data', 'outbox.jsonl'));
373
+ addExistingTarget(targets, path.join(agentRoot, 'data', 'activity-outbox.jsonl'));
372
374
  addExistingTarget(targets, path.join(agentRoot, 'data', 'contact-audit.jsonl'));
373
375
  }
374
376
  }
@@ -435,12 +437,22 @@ export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
435
437
  export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
436
438
  const lClassRules = {};
437
439
  const lClassAccess = options.denyLClassRead ? 'deny' : 'read';
440
+ // Codex's Linux sandbox expands glob deny rules into per-file mount targets.
441
+ // When an outer H-class guard has already mounted the L-class directory
442
+ // read-only, that expansion can race with the mount and fail before the
443
+ // command starts (for example while creating triggers/history.jsonl).
444
+ // Directory-level deny is sufficient for the policy bridge and avoids the
445
+ // nested bwrap per-file projection. Callers on platforms without that
446
+ // outer guard retain the historical expansion by default.
447
+ const expandLClassReadDeny = options.expandLClassReadDeny ?? true;
438
448
  for (const target of getExistingLClassReadOnlyTargets(root)) {
439
449
  if (target.kind === 'directory') {
440
- if (options.denyLClassRead || path.basename(target.path) === 'triggers') {
450
+ if (!expandLClassReadDeny || options.denyLClassRead || path.basename(target.path) === 'triggers') {
441
451
  lClassRules[target.path] = lClassAccess;
442
452
  }
443
- lClassRules[path.join(target.path, '**')] = lClassAccess;
453
+ if (expandLClassReadDeny || !options.denyLClassRead) {
454
+ lClassRules[path.join(target.path, '**')] = lClassAccess;
455
+ }
444
456
  }
445
457
  else {
446
458
  lClassRules[target.path] = lClassAccess;
@@ -3,7 +3,8 @@ 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', 'git', 'wc', 'stat', 'lstat',
6
+ 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat', 'awk',
7
+ 'bash', 'sh',
7
8
  ]);
8
9
  const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
9
10
  const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
@@ -113,6 +114,22 @@ function exact(value) {
113
114
  function recursive(value) {
114
115
  return { value, access: 'recursive' };
115
116
  }
117
+ function isLiteralGitPathspec(value) {
118
+ // Git pathspec magic and wildcards can expand a seemingly harmless token to
119
+ // protected files elsewhere in the repository. The readonly diagnostic
120
+ // capability accepts only a literal path or directory scope.
121
+ return !!value
122
+ && !value.startsWith('-')
123
+ && !value.startsWith(':')
124
+ && !/[\0*?[\\]/.test(value);
125
+ }
126
+ function isLikelyGitPath(value) {
127
+ // Before `--`, Git accepts both revisions and pathspecs. Only classify
128
+ // tokens with an unmistakable path shape here; bare names such as `HEAD`
129
+ // and `main` remain revision selectors and do not become filesystem scope.
130
+ return isLiteralGitPathspec(value)
131
+ && (value === '.' || value === '..' || value.includes('/') || value.startsWith('.') || /\.[A-Za-z0-9_-]+$/.test(value));
132
+ }
116
133
  function parseOptionsAndPaths(args, valueOptions, recursiveOptions = new Set()) {
117
134
  const paths = [];
118
135
  let optionsEnded = false;
@@ -472,9 +489,10 @@ function analyzeSort(args) {
472
489
  /**
473
490
  * Git metadata queries do not expose file contents and their output paths are
474
491
  * 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.
492
+ * deliberately small: status, bounded diff summaries, pickaxe log queries,
493
+ * and blame are the patterns used for workspace diagnostics. Mutating
494
+ * subcommands and unrecognised options remain unproven so normal H/L checks
495
+ * still apply.
478
496
  */
479
497
  function analyzeGit(args) {
480
498
  if (args.length === 0)
@@ -502,6 +520,7 @@ function analyzeGit(args) {
502
520
  }
503
521
  if (subcommand === 'diff') {
504
522
  let sawSummary = false;
523
+ let noIndex = false;
505
524
  let optionsEnded = false;
506
525
  const paths = [];
507
526
  const allowedFlags = new Set([
@@ -518,17 +537,152 @@ function analyzeGit(args) {
518
537
  if (arg === '--stat' || arg === '--shortstat' || arg === '--numstat'
519
538
  || arg === '--name-only' || arg === '--name-status')
520
539
  sawSummary = true;
540
+ if (arg === '--no-index')
541
+ noIndex = true;
521
542
  if (!allowedFlags.has(arg) && !/^[0-9a-f]{7,64}$/.test(arg))
522
543
  return null;
523
544
  continue;
524
545
  }
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))
546
+ // A Git pathspec may name a directory or wildcard scope, so model it as
547
+ // recursive even when the token itself is literal. This keeps `-- .`
548
+ // from bypassing nested protected paths.
549
+ if (optionsEnded) {
550
+ if (!isLiteralGitPathspec(arg))
551
+ return null;
552
+ paths.push(recursive(arg));
553
+ }
554
+ else if (noIndex) {
555
+ if (!isLiteralGitPathspec(arg))
556
+ return null;
557
+ // `--no-index` compares two filesystem paths directly, so both
558
+ // operands must participate in workspace/H-class checks.
559
+ paths.push(recursive(arg));
560
+ }
561
+ else {
562
+ if (!/^[0-9A-Za-z._~^/:-]+$/.test(arg))
563
+ return null;
564
+ if (isLikelyGitPath(arg))
565
+ paths.push(recursive(arg));
566
+ }
567
+ }
568
+ if (!sawSummary)
569
+ return null;
570
+ if (noIndex && paths.length !== 2)
571
+ return null;
572
+ return paths;
573
+ }
574
+ if (subcommand === 'log') {
575
+ let optionsEnded = false;
576
+ let sawSearch = false;
577
+ const paths = [];
578
+ const valueOptions = new Set([
579
+ '-S', '-G', '--grep', '--author', '--committer', '--since', '--after', '--until', '--before',
580
+ '--format', '--pretty', '--max-count', '-n', '--skip', '--decorate-refs',
581
+ ]);
582
+ const allowedFlags = new Set([
583
+ '--all', '--branches', '--tags', '--remotes', '--oneline', '--no-patch', '--no-decorate', '--decorate',
584
+ '--follow', '--pickaxe-all', '--pickaxe-regex', '--reverse', '--topo-order', '--date-order',
585
+ '--author-date-order', '--first-parent', '--no-merges', '--merges', '--stat', '--shortstat',
586
+ '--name-only', '--name-status', '--no-renames', '--no-ext-diff', '--no-textconv', '--no-color',
587
+ '--color=never',
588
+ ]);
589
+ for (let index = 1; index < args.length; index++) {
590
+ const arg = args[index];
591
+ if (!optionsEnded && arg === '--') {
592
+ optionsEnded = true;
593
+ continue;
594
+ }
595
+ if (optionsEnded) {
596
+ if (!isLiteralGitPathspec(arg))
597
+ return null;
598
+ paths.push(recursive(arg));
599
+ continue;
600
+ }
601
+ if (arg.startsWith('-')) {
602
+ const option = arg.split('=', 1)[0];
603
+ if (arg === '-S' || arg === '-G' || /^-[SG].+/.test(arg)) {
604
+ sawSearch = true;
605
+ if (arg.length === 2) {
606
+ if (++index >= args.length)
607
+ return null;
608
+ }
609
+ continue;
610
+ }
611
+ if (valueOptions.has(option)) {
612
+ if (!arg.includes('=') && !arg.startsWith('-S') && !arg.startsWith('-G')) {
613
+ if (++index >= args.length)
614
+ return null;
615
+ }
616
+ continue;
617
+ }
618
+ if (!allowedFlags.has(arg) && !/^(?:[0-9A-Fa-f]{7,64}|HEAD(?:~[0-9]+)?|[A-Za-z0-9._/-]+\.\.\.?)$/.test(arg))
619
+ return null;
620
+ continue;
621
+ }
622
+ // Revisions before `--` are metadata selectors, never filesystem paths.
623
+ if (!/^[A-Za-z0-9._~^/:@+-]+$/.test(arg))
624
+ return null;
625
+ }
626
+ return sawSearch && paths.length > 0 ? paths : null;
627
+ }
628
+ if (subcommand === 'blame') {
629
+ let optionsEnded = false;
630
+ let sawPath = false;
631
+ const paths = [];
632
+ const valueOptions = new Set(['-L', '--abbrev', '--date', '--ignore-rev']);
633
+ const pathValueOptions = new Set(['--ignore-revs-file', '--contents']);
634
+ const allowedFlags = new Set([
635
+ '--porcelain', '--line-porcelain', '--incremental', '--root', '--show-email', '--show-number',
636
+ '--show-name', '--reverse', '--progress', '--score-debug', '--encoding', '--no-progress',
637
+ ]);
638
+ for (let index = 1; index < args.length; index++) {
639
+ const arg = args[index];
640
+ if (!optionsEnded && arg === '--') {
641
+ optionsEnded = true;
642
+ continue;
643
+ }
644
+ if (optionsEnded) {
645
+ if (!isLiteralGitPathspec(arg) || sawPath)
646
+ return null;
647
+ paths.push(exact(arg));
648
+ sawPath = true;
649
+ continue;
650
+ }
651
+ if (arg.startsWith('-')) {
652
+ const option = arg.split('=', 1)[0];
653
+ if (option === '-L' || option === '-C') {
654
+ // `-L` takes a line range; `-C` is a repeatable flag with an
655
+ // optional score and therefore must not consume the following path.
656
+ if (option === '-L' && !arg.includes('=') && arg.length === 2
657
+ && ++index >= args.length)
658
+ return null;
659
+ continue;
660
+ }
661
+ if (pathValueOptions.has(option)) {
662
+ const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : args[++index];
663
+ if (!value)
664
+ return null;
665
+ paths.push(exact(value));
666
+ continue;
667
+ }
668
+ if (valueOptions.has(option)) {
669
+ if (!arg.includes('=') && ++index >= args.length)
670
+ return null;
671
+ continue;
672
+ }
673
+ if (!allowedFlags.has(arg))
674
+ return null;
675
+ continue;
676
+ }
677
+ // The common diagnostic form is `git blame <path>`. Keep one literal
678
+ // path operand; callers that need a revision should use `-- <path>` so
679
+ // the boundary is unambiguous to the policy parser.
680
+ if (sawPath || !isLiteralGitPathspec(arg))
529
681
  return null;
682
+ paths.push(exact(arg));
683
+ sawPath = true;
530
684
  }
531
- return sawSummary ? paths : null;
685
+ return paths.length === 1 ? paths : null;
532
686
  }
533
687
  return null;
534
688
  }
@@ -574,6 +728,99 @@ function analyzeStat(args) {
574
728
  }
575
729
  return paths.length > 0 ? paths : null;
576
730
  }
731
+ function analyzeAwk(args) {
732
+ let index = 0;
733
+ while (index < args.length && args[index].startsWith('-')) {
734
+ const option = args[index];
735
+ if (option === '--') {
736
+ index++;
737
+ break;
738
+ }
739
+ if (option === '-F') {
740
+ if (++index >= args.length)
741
+ return null;
742
+ index++;
743
+ continue;
744
+ }
745
+ if (option.startsWith('-F') && !option.startsWith('--') && option.length > 2) {
746
+ index++;
747
+ continue;
748
+ }
749
+ if (option === '-v' || option === '--assign') {
750
+ if (++index >= args.length)
751
+ return null;
752
+ index++;
753
+ continue;
754
+ }
755
+ if (option.startsWith('-v') && !option.startsWith('--') && option.length > 2) {
756
+ index++;
757
+ continue;
758
+ }
759
+ if (option === '--field-separator') {
760
+ if (++index >= args.length)
761
+ return null;
762
+ index++;
763
+ continue;
764
+ }
765
+ if (option.startsWith('--field-separator=') || option.startsWith('--assign=')) {
766
+ index++;
767
+ continue;
768
+ }
769
+ return null;
770
+ }
771
+ const program = args[index++];
772
+ // Permit the common formatting/filtering form while excluding awk's
773
+ // execution, file-redirection and shell escape features.
774
+ if (!program || !/^\{[\s\S]*\}$/.test(program)
775
+ || /\b(?:system|getline|close|nextfile)\b|(?:>>?|\|)|\b(?:delete|exit)\b/.test(program)
776
+ || /(^|[^=!<>])=([^=]|$)/.test(program))
777
+ return null;
778
+ const paths = args.slice(index).filter(value => value !== '-').map(exact);
779
+ return paths;
780
+ }
781
+ /**
782
+ * `bash -n`/`sh -n` only parses a script and never executes it. Treat the
783
+ * script operands as exact reads, but keep every other shell option and
784
+ * nested command fail-closed. This is intentionally narrower than allowing
785
+ * arbitrary `bash -c`, which would re-introduce an opaque shell carrier.
786
+ */
787
+ function analyzeShellSyntaxCheck(args) {
788
+ if (args.length < 2)
789
+ return null;
790
+ let index = 0;
791
+ let sawNoExec = false;
792
+ while (index < args.length && args[index].startsWith('-') && args[index] !== '-') {
793
+ const option = args[index++];
794
+ if (option === '--') {
795
+ // The increment above already consumed the delimiter; the following
796
+ // token is the script path, not another option.
797
+ break;
798
+ }
799
+ if (option === '-n' || option === '--noexec') {
800
+ sawNoExec = true;
801
+ continue;
802
+ }
803
+ // `-n` may be combined with other bash flags, but only when the whole
804
+ // cluster is made of harmless parser flags. Do not accept flags that can
805
+ // select an inline command, source files, or alter execution semantics.
806
+ if (/^-[A-Za-z]*n[A-Za-z]*$/.test(option)
807
+ && !/[cCeEirsSx]/.test(option.replace(/^-/, ''))) {
808
+ sawNoExec = true;
809
+ continue;
810
+ }
811
+ return null;
812
+ }
813
+ if (!sawNoExec || index >= args.length)
814
+ return null;
815
+ const paths = [];
816
+ for (; index < args.length; index++) {
817
+ const value = args[index];
818
+ if (!value || value === '-' || value.startsWith('-'))
819
+ return null;
820
+ paths.push(exact(value));
821
+ }
822
+ return paths;
823
+ }
577
824
  function analyzeCommand(argv) {
578
825
  const executable = normalizeExecutable(argv[0]);
579
826
  if (!executable)
@@ -623,6 +870,13 @@ function analyzeCommand(argv) {
623
870
  pathOperands = analyzeStat(args);
624
871
  accessKind = 'metadata';
625
872
  break;
873
+ case 'awk':
874
+ pathOperands = analyzeAwk(args);
875
+ break;
876
+ case 'bash':
877
+ case 'sh':
878
+ pathOperands = analyzeShellSyntaxCheck(args);
879
+ break;
626
880
  }
627
881
  if (!pathOperands)
628
882
  return null;
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolveCommandPath } from '../../utils/cross-platform.js';
4
- import { getExistingHClassMaskTargets, getExistingLClassReadOnlyTargets, isSameOrDescendant, } from '../protected-paths.js';
4
+ import { getExistingHClassMaskTargets, getExistingLClassReadOnlyTargets, isSameOrDescendant, } from './protected-paths.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  let cachedBubblewrapPath;
7
7
  let sandboxInitializationCircuit;
@@ -9,6 +9,7 @@ let sandboxCircuitNoticeAt = 0;
9
9
  export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
10
10
  export const SANDBOX_INITIALIZATION_COOLDOWN_MS = 5 * 60 * 1000;
11
11
  export const SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS = 60 * 1000;
12
+ const SANDBOX_DIAGNOSTIC_TARGET_LIMIT = 256;
12
13
  /** Open a process-wide fail-closed circuit after a confirmed host bootstrap failure. */
13
14
  export function recordSandboxInitializationFailure(error, stderr = [], now = Date.now()) {
14
15
  const detail = createSandboxInitializationError(error, stderr).message.slice(0, 512);
@@ -218,6 +219,163 @@ function pushMaskedDirectory(args, candidate) {
218
219
  return;
219
220
  args.push('--tmpfs', candidate);
220
221
  }
222
+ function decodeMountInfoPath(value) {
223
+ // mountinfo escapes whitespace, backslashes, and newlines as octal bytes.
224
+ return value.replace(/\\([0-7]{3})/g, (_match, octal) => {
225
+ const code = Number.parseInt(octal, 8);
226
+ return Number.isNaN(code) ? _match : String.fromCharCode(code);
227
+ });
228
+ }
229
+ function readMountInfoForPaths(paths, priorityPaths = []) {
230
+ if (process.platform !== 'linux' || paths.length === 0)
231
+ return [];
232
+ let contents;
233
+ try {
234
+ contents = fs.readFileSync('/proc/self/mountinfo', 'utf8');
235
+ }
236
+ catch (error) {
237
+ return [`mountinfo unavailable: ${error instanceof Error ? error.message : String(error)}`];
238
+ }
239
+ const wanted = paths.map(candidate => path.resolve(candidate));
240
+ const priority = priorityPaths.map(candidate => path.resolve(candidate));
241
+ const matches = [];
242
+ for (const [order, line] of contents.split(/\r?\n/).filter(Boolean).entries()) {
243
+ const mountPointField = line.split(' - ', 1)[0]?.split(' ')[4];
244
+ if (!mountPointField)
245
+ continue;
246
+ const mountPoint = path.resolve(decodeMountInfoPath(mountPointField));
247
+ if (!wanted.some(target => isSameOrDescendant(target, mountPoint)
248
+ || isSameOrDescendant(mountPoint, target)))
249
+ continue;
250
+ const isPriority = priority.some(target => target === mountPoint);
251
+ const isPriorityAncestor = !isPriority && priority.some(target => isSameOrDescendant(target, mountPoint));
252
+ matches.push({ line, score: isPriority ? 0 : isPriorityAncestor ? 1 : 2, order });
253
+ }
254
+ return matches
255
+ .sort((left, right) => left.score - right.score || left.order - right.order)
256
+ .slice(0, 32)
257
+ .map(match => match.line);
258
+ }
259
+ function snapshotSandboxPath(candidate) {
260
+ const resolved = path.resolve(candidate);
261
+ try {
262
+ const stat = fs.lstatSync(resolved);
263
+ let kind = 'other';
264
+ if (stat.isFile())
265
+ kind = 'file';
266
+ else if (stat.isDirectory())
267
+ kind = 'directory';
268
+ else if (stat.isSymbolicLink())
269
+ kind = 'symlink';
270
+ let realpath;
271
+ try {
272
+ realpath = fs.realpathSync(resolved);
273
+ }
274
+ catch { }
275
+ return {
276
+ path: resolved,
277
+ exists: true,
278
+ kind,
279
+ dev: stat.dev,
280
+ ino: stat.ino,
281
+ mode: stat.mode & 0o7777,
282
+ uid: stat.uid,
283
+ gid: stat.gid,
284
+ nlink: stat.nlink,
285
+ size: stat.size,
286
+ mtimeMs: stat.mtimeMs,
287
+ ctimeMs: stat.ctimeMs,
288
+ ...(realpath ? { realpath } : {}),
289
+ };
290
+ }
291
+ catch (error) {
292
+ const code = error.code;
293
+ return {
294
+ path: resolved,
295
+ exists: false,
296
+ ...(code ? { error: code } : { error: error instanceof Error ? error.message : String(error) }),
297
+ };
298
+ }
299
+ }
300
+ /** Capture the host-side state used to construct an H-class guard. */
301
+ export function collectSandboxGuardDiagnostics(root = resolveRoot()) {
302
+ const absoluteRoot = path.resolve(root);
303
+ const candidates = [
304
+ path.join(absoluteRoot, 'data', 'causation-aun.json'),
305
+ path.join(absoluteRoot, 'data', 'causation-aun.json_'),
306
+ path.join(absoluteRoot, 'data', 'causation-aun.json__'),
307
+ ];
308
+ let targets = [];
309
+ try {
310
+ targets = getExistingHClassMaskTargets(absoluteRoot).map(target => target.path);
311
+ }
312
+ catch { }
313
+ // Keep the causation file and its atomic-write sidecars first. The full H
314
+ // target list can grow with the number of agents and must not turn every
315
+ // launch into an unbounded log payload.
316
+ const paths = [...new Set([...candidates, ...targets])];
317
+ const selectedPaths = paths.slice(0, SANDBOX_DIAGNOSTIC_TARGET_LIMIT);
318
+ return {
319
+ root: absoluteRoot,
320
+ targetCount: paths.length,
321
+ targetsTruncated: paths.length > selectedPaths.length,
322
+ targets: selectedPaths.map(snapshotSandboxPath),
323
+ mountInfo: readMountInfoForPaths(selectedPaths, candidates),
324
+ };
325
+ }
326
+ /** Reduce path and mount diagnostics to metadata suitable for INFO logs. */
327
+ export function summarizeSandboxGuardDiagnostics(diagnostics) {
328
+ let existingTargetCount = 0;
329
+ let missingTargetCount = 0;
330
+ const targetKindCounts = {};
331
+ for (const target of diagnostics.targets) {
332
+ if (!target.exists) {
333
+ missingTargetCount += 1;
334
+ continue;
335
+ }
336
+ existingTargetCount += 1;
337
+ const kind = target.kind ?? 'unknown';
338
+ targetKindCounts[kind] = (targetKindCounts[kind] ?? 0) + 1;
339
+ }
340
+ return {
341
+ root: diagnostics.root,
342
+ targetCount: diagnostics.targetCount,
343
+ targetsTruncated: diagnostics.targetsTruncated,
344
+ existingTargetCount,
345
+ missingTargetCount,
346
+ targetKindCounts,
347
+ mountInfoCount: diagnostics.mountInfo.length,
348
+ };
349
+ }
350
+ /** Keep the exact mount portion of a bwrap argv while summarizing child argv. */
351
+ export function summarizeBubblewrapArgs(args) {
352
+ const separator = args.indexOf('--');
353
+ const mountArgs = [...args.slice(0, separator >= 0 ? separator : args.length)];
354
+ const child = separator >= 0 ? args.slice(separator + 1) : [];
355
+ return {
356
+ mountArgs,
357
+ ...(child[0] ? { childCommand: child[0] } : {}),
358
+ childArgCount: Math.max(0, child.length - 1),
359
+ };
360
+ }
361
+ /** Keep only argument counts and option names for routine INFO logs. */
362
+ export function summarizeBubblewrapArgsForLog(args) {
363
+ const separator = args.indexOf('--');
364
+ const mountArgs = args.slice(0, separator >= 0 ? separator : args.length);
365
+ const child = separator >= 0 ? args.slice(separator + 1) : [];
366
+ const mountOptionCounts = {};
367
+ for (const arg of mountArgs) {
368
+ if (!arg.startsWith('--'))
369
+ continue;
370
+ mountOptionCounts[arg] = (mountOptionCounts[arg] ?? 0) + 1;
371
+ }
372
+ return {
373
+ mountArgCount: mountArgs.length,
374
+ mountOptionCounts,
375
+ ...(child[0] ? { childCommand: child[0] } : {}),
376
+ childArgCount: Math.max(0, child.length - 1),
377
+ };
378
+ }
221
379
  function appendRuntimeBackedEtcFiles(args) {
222
380
  if (process.platform !== 'linux' || !fs.existsSync('/run'))
223
381
  return;