evolcore 0.0.33 → 0.0.35-test.0

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 (152) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +38 -4
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +93 -14
  6. package/dist/agents/codex/diagnostics.js +79 -22
  7. package/dist/agents/codex-runner.js +245 -47
  8. package/dist/agents/ecagent-runner.js +90 -27
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/agentmd.js +13 -2
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/identity.js +1 -1
  13. package/dist/aun/msg/managed-operation.js +16 -0
  14. package/dist/aun/msg/p2p.js +18 -2
  15. package/dist/channels/aun.js +123 -7
  16. package/dist/channels/daemon.js +6 -3
  17. package/dist/cli/aun-commands.js +39 -14
  18. package/dist/cli/ctl-command.js +3 -1
  19. package/dist/cli/daemon-commands.js +130 -23
  20. package/dist/cli/fs-command.js +2 -1
  21. package/dist/cli/getmeta-command.js +211 -0
  22. package/dist/cli/index.js +26 -6
  23. package/dist/cli/integrity-command.js +196 -0
  24. package/dist/cli/managed-command-guard.js +3 -0
  25. package/dist/cli/memory-command.js +200 -156
  26. package/dist/cli/thread-command.js +17 -7
  27. package/dist/cli/trigger-command.js +6 -1
  28. package/dist/config/config-field-policy.js +4 -0
  29. package/dist/config/config-manager.js +19 -0
  30. package/dist/config/contact-book-store.js +5 -0
  31. package/dist/config/resolved-config-op.js +25 -8
  32. package/dist/config/role-service.js +13 -0
  33. package/dist/config/runtime-env.js +16 -0
  34. package/dist/config-store.js +30 -0
  35. package/dist/core/audit/log-integrity.js +3 -1
  36. package/dist/core/auth/agent-delegation.js +108 -35
  37. package/dist/core/auth/delegation-diagnostics.js +414 -0
  38. package/dist/core/auth/operation-authorizer.js +22 -3
  39. package/dist/core/auth/operation-catalog.js +16 -0
  40. package/dist/core/channel-runtime.js +1 -1
  41. package/dist/core/command/agent-control.js +18 -26
  42. package/dist/core/command/command-handler.js +145 -5
  43. package/dist/core/command/daemon-diagnostics.js +1 -0
  44. package/dist/core/command/menu-catalog.js +3 -0
  45. package/dist/core/command/menu-handler.js +24 -7
  46. package/dist/core/command/menu-protocol.js +1 -0
  47. package/dist/core/command/slash-handler.js +2 -0
  48. package/dist/core/command/topic-slash.js +4 -7
  49. package/dist/core/daemon-bootstrap.js +4 -7
  50. package/dist/core/event-catalog.js +14 -0
  51. package/dist/core/file-integrity/read.js +61 -0
  52. package/dist/core/file-integrity/store.js +262 -0
  53. package/dist/core/ipc-registration.js +47 -2
  54. package/dist/core/memory/batch-meta.js +133 -0
  55. package/dist/core/memory/consolidation-context.js +132 -28
  56. package/dist/core/memory/consolidation-ops-records.js +14 -16
  57. package/dist/core/memory/consolidation-trigger.js +15 -7
  58. package/dist/core/memory/consolidation.js +146 -26
  59. package/dist/core/memory/index-manager.js +19 -28
  60. package/dist/core/memory/memory-admin.js +19 -14
  61. package/dist/core/memory/memory-document.js +53 -9
  62. package/dist/core/memory/memory-writer.js +50 -19
  63. package/dist/core/message/aun-ipc-sender.js +19 -1
  64. package/dist/core/message/im-renderer.js +20 -4
  65. package/dist/core/message/message-bridge.js +5 -4
  66. package/dist/core/message/message-log.js +1 -0
  67. package/dist/core/message/peer-mode.js +17 -0
  68. package/dist/core/message/response-engine.js +598 -50
  69. package/dist/core/permission/ec-command-parser.js +169 -44
  70. package/dist/core/permission/protected-paths.js +5 -1
  71. package/dist/core/permission/readonly-shell-query.js +43 -5
  72. package/dist/core/permission/session-path-grants.js +334 -0
  73. package/dist/core/permission/tool-canonical.js +226 -0
  74. package/dist/core/permission/tool-policy.js +60 -62
  75. package/dist/core/relation/peer-identity.js +87 -12
  76. package/dist/core/role/runtime-policy.js +21 -10
  77. package/dist/core/session/session-fs-store.js +123 -8
  78. package/dist/core/session/session-manager.js +131 -6
  79. package/dist/core/session/session-title.js +26 -1
  80. package/dist/core/skill/index-generator.js +17 -3
  81. package/dist/core/system-session/grants.js +40 -0
  82. package/dist/core/system-session/memory.js +289 -0
  83. package/dist/core/system-session/registry.js +74 -0
  84. package/dist/core/system-session/types.js +14 -0
  85. package/dist/core/thread/command-parser.js +6 -1
  86. package/dist/core/thread/ipc-operation.js +33 -16
  87. package/dist/core/thread/thread-operation.js +62 -24
  88. package/dist/core/thread/thread-service.js +258 -51
  89. package/dist/eck/kit-renderer.js +7 -2
  90. package/dist/eck/manifest-engine.js +24 -4
  91. package/dist/index.js +11 -2
  92. package/dist/ipc.js +51 -40
  93. package/dist/paths.js +70 -0
  94. package/dist/stats/billing.js +20 -8
  95. package/dist/trigger/feedback.js +6 -1
  96. package/dist/trigger/parser.js +8 -1
  97. package/dist/trigger/system-definitions.js +3 -3
  98. package/dist/utils/cross-platform.js +17 -5
  99. package/dist/utils/error-dict.json +18 -4
  100. package/dist/utils/error-utils.js +121 -0
  101. package/dist/utils/instance-registry.js +13 -8
  102. package/dist/utils/log-writer.js +12 -0
  103. package/dist/utils/process-introspect.js +11 -0
  104. package/kits/docs/context-assembly.md +0 -1
  105. package/kits/docs/evolcore/agent-create.md +5 -4
  106. package/kits/docs/evolcore/agent.md +2 -2
  107. package/kits/docs/evolcore/config.md +2 -2
  108. package/kits/docs/evolcore/ctl.md +2 -2
  109. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  110. package/kits/docs/evolcore/fs-architecture.md +9 -10
  111. package/kits/docs/evolcore/fs.md +2 -2
  112. package/kits/docs/evolcore/group-collaboration.md +3 -3
  113. package/kits/docs/evolcore/group.md +14 -12
  114. package/kits/docs/evolcore/memory-architecture.md +4 -5
  115. package/kits/docs/evolcore/memory-consolidation.md +19 -16
  116. package/kits/docs/evolcore/memory-tags.md +3 -1
  117. package/kits/docs/evolcore/memory.md +24 -12
  118. package/kits/docs/evolcore/model.md +16 -0
  119. package/kits/docs/evolcore/msg.md +7 -7
  120. package/kits/docs/evolcore/stats.md +1 -1
  121. package/kits/docs/evolcore/thread-chain.md +30 -14
  122. package/kits/docs/prompt-loading-architecture.md +0 -1
  123. package/kits/eck_manifest.auxiliary.json +9 -0
  124. package/kits/eck_manifest.json +20 -20
  125. package/kits/eck_manifest.memory.json +20 -12
  126. package/kits/rules/01-overview.md +11 -0
  127. package/kits/rules/03-identity.md +6 -3
  128. package/kits/rules/06-channel.md +1 -1
  129. package/kits/rules/07-memory-and-thread.md +13 -16
  130. package/kits/schemas/_meta.json +7 -1
  131. package/kits/schemas/agent-config.schema.13.json +1 -1
  132. package/kits/schemas/daemon.schema.7.json +140 -0
  133. package/kits/skills/ec-group/SKILL.md +8 -5
  134. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  135. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  136. package/kits/skills/thread-ops/SKILL.md +18 -18
  137. package/kits/templates/memory-consolidation/forget.md +2 -2
  138. package/kits/templates/memory-consolidation/generalize.md +4 -4
  139. package/kits/templates/memory-consolidation/notes.md +12 -12
  140. package/kits/templates/memory-consolidation/profile.md +3 -3
  141. package/kits/templates/memory-consolidation/promote.md +3 -3
  142. package/kits/templates/memory-consolidation/workflow.md +42 -7
  143. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  144. package/kits/templates/system-fragments/bash-description.md +9 -0
  145. package/kits/templates/system-fragments/bootstrap.md +4 -3
  146. package/kits/templates/system-fragments/channel.md +2 -2
  147. package/kits/templates/system-fragments/memory.md +16 -9
  148. package/kits/templates/system-fragments/reply.md +6 -6
  149. package/kits/templates/system-fragments/session.md +1 -1
  150. package/package.json +4 -3
  151. package/dist/core/memory/memory-session-policy.js +0 -41
  152. package/dist/core/memory/memory-signature.js +0 -257
@@ -619,6 +619,10 @@ function isLiteralArgBoundary(value) {
619
619
  * 判为 `shell-composition`,拦截力度不变,只让错误归因准确。
620
620
  */
621
621
  function isPosixQuotedTokenBoundary(value) {
622
+ // 相邻引号段直接拼接(`'a''b'`、`$'a''b'`、`'a'"b"`)与 bash 一致:
623
+ // 闭合引号后紧跟另一个开引号不是错误,两个段拼进同一个 token。
624
+ if (value === "'" || value === '"')
625
+ return true;
622
626
  return value === undefined || isShellSpacing(value) || SHELL_UNQUOTED_META.has(value);
623
627
  }
624
628
  function isShellParameterExpansionStart(value) {
@@ -714,6 +718,77 @@ function decodePositionalText(argv, start, optionFlags) {
714
718
  argv[index] = decodeEvolcoreInlineText(value);
715
719
  }
716
720
  }
721
+ // 取值为「逗号分隔、单项不含空格」的列表参数。aid、标签值、角色名里都不会有
722
+ // 空格,所以按空格拆分是安全的。
723
+ const MEMORY_LIST_FLAGS = new Set(['--tags', '--speakers']);
724
+ const SEND_LIST_FLAGS = new Set(['--mention']);
725
+ const FS_LIST_FLAGS = new Set(['--allow-roles']);
726
+ // 取值可能含空格的列表参数,只能按逗号拆:关键词是自由文本,记忆路径是 config
727
+ // 相对路径,两者都可能有空格。
728
+ const MEMORY_COMMA_ONLY_FLAGS = new Set(['--keywords', '--replaces']);
729
+ /** 逗号分隔项:去空、去首尾空白,重新用逗号连接。 */
730
+ function joinCommaList(value) {
731
+ return value.split(',').map(item => item.trim()).filter(Boolean).join(',');
732
+ }
733
+ /**
734
+ * 把列表参数统一成逗号分隔。PowerShell 把未加引号的 `a,b` 当数组,经 npm 的
735
+ * ec.ps1 用 `$args` 转发给 node 时逗号消失:`a,b` 变成 `a b`,`a, b` 和
736
+ * `a,b, c` 也变成 `a b` / `a b c`(实测 PS 5.1)。审批侧按命令串解析时看到的
737
+ * 是 `a,` 和 `b` 两个 token。两边在这里收敛到同一表示,委托哈希才能对上。
738
+ *
739
+ * 值以逗号结尾说明逗号数组还没结束:把后续 token 并进同一个值里再拆。值里本来
740
+ * 带逗号却没有空格的写法(bash 下 `--tags "a, b"`)也会被拆成两个标签,这是刻
741
+ * 意取舍——标签都是不含空格的单词,而 ec.ps1 已经把 `a, b` 和 `a,b` 归成了一
742
+ * 个值,无法再区分。
743
+ */
744
+ function canonicalizeListFlags(argv, flags) {
745
+ for (let index = 0; index < argv.length; index++) {
746
+ const value = argv[index];
747
+ if (value === '--')
748
+ break;
749
+ if (!flags.has(value))
750
+ continue;
751
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith('--'))
752
+ continue;
753
+ // `--tags a, b` 在 PowerShell 里是同一个值,审批侧却解析成 `a,` 加 `b`,
754
+ // 这里按逗号是否收尾把后续 token 续接回来
755
+ let end = index + 1;
756
+ while (argv[end].endsWith(',') && argv[end + 1] !== undefined
757
+ && !argv[end + 1].startsWith('--')) {
758
+ end++;
759
+ }
760
+ argv[index + 1] = argv.slice(index + 1, end + 1)
761
+ .join(' ')
762
+ .split(/[,\s]+/u)
763
+ .filter(Boolean)
764
+ .join(',');
765
+ if (end > index + 1)
766
+ argv.splice(index + 2, end - index - 1);
767
+ index++;
768
+ }
769
+ }
770
+ /**
771
+ * 只按逗号归一化,不动值里的空格。
772
+ *
773
+ * 与 canonicalizeListFlags 的区别:`--keywords 'a b'` 是一个含空格的关键词,
774
+ * 不能被拆成两个。所以只在值本身带逗号时整理(顺带去掉逗号周围的空白),
775
+ * `--keywords a,b` 这种未加引号的写法在 PowerShell 下会先被折成 `a b`,此处
776
+ * 认不出来——那属于被拒绝的写法,靠规范要求单引号避免。
777
+ */
778
+ function canonicalizeCommaOnlyFlags(argv, flags) {
779
+ for (let index = 0; index < argv.length; index++) {
780
+ const value = argv[index];
781
+ if (value === '--')
782
+ break;
783
+ if (!flags.has(value))
784
+ continue;
785
+ const next = argv[index + 1];
786
+ if (next === undefined || next.startsWith('--') || !next.includes(','))
787
+ continue;
788
+ argv[index + 1] = joinCommaList(next);
789
+ index++;
790
+ }
791
+ }
717
792
  function canonicalizeHandoffText(argv) {
718
793
  const action = argv[2];
719
794
  if (action !== 'return' && action !== 'recover-return')
@@ -739,7 +814,8 @@ function canonicalizeHandoffText(argv) {
739
814
  * Convert an EC invocation to the one argv representation shared by approval,
740
815
  * delegation hashing, IPC, and command execution. Shell syntax is already
741
816
  * resolved before this function is called; this function only normalizes the
742
- * EC entrypoint and applies the command-specific inline text contract.
817
+ * EC entrypoint, applies the command-specific inline text contract, and joins
818
+ * memory list flags with commas.
743
819
  */
744
820
  export function canonicalizeEvolcoreCommandArgv(argv) {
745
821
  const canonical = normalizeEvolcoreCommandArgv(argv);
@@ -759,11 +835,13 @@ export function canonicalizeEvolcoreCommandArgv(argv) {
759
835
  else if (command === 'msg' && subcommand === 'send') {
760
836
  decodeInlineFlags(result, INLINE_TEXT_FLAGS.msgSend);
761
837
  decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
838
+ canonicalizeListFlags(result, SEND_LIST_FLAGS);
762
839
  }
763
840
  else if (command === 'group') {
764
841
  if (subcommand === 'send') {
765
842
  decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupSend);
766
843
  decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
844
+ canonicalizeListFlags(result, SEND_LIST_FLAGS);
767
845
  }
768
846
  else if (subcommand === 'create')
769
847
  decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupCreate);
@@ -785,6 +863,13 @@ export function canonicalizeEvolcoreCommandArgv(argv) {
785
863
  else if (command === 'handoff') {
786
864
  canonicalizeHandoffText(result);
787
865
  }
866
+ else if (command === 'memory') {
867
+ canonicalizeListFlags(result, MEMORY_LIST_FLAGS);
868
+ canonicalizeCommaOnlyFlags(result, MEMORY_COMMA_ONLY_FLAGS);
869
+ }
870
+ else if (command === 'fs' && subcommand === 'chmod') {
871
+ canonicalizeListFlags(result, FS_LIST_FLAGS);
872
+ }
788
873
  return result;
789
874
  }
790
875
  const POWERSHELL_CREDENTIAL_ENV_RE = /^(EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)$/i;
@@ -1496,6 +1581,8 @@ export function parseLiteralShellCommand(command, options = {}) {
1496
1581
  let token = '';
1497
1582
  let tokenStarted = false;
1498
1583
  let quote = null;
1584
+ // EC 命令的自由文本有额外的写法约束(见双引号分支的反引号/$ 转义处理)。
1585
+ const isEcCommand = hasEcCommandPrefix(input);
1499
1586
  const finishToken = () => {
1500
1587
  if (!tokenStarted)
1501
1588
  return;
@@ -1527,6 +1614,12 @@ export function parseLiteralShellCommand(command, options = {}) {
1527
1614
  return parseFailure(issue, input, index, argv.length, 'posix');
1528
1615
  if (char === '\\') {
1529
1616
  const next = input[++index];
1617
+ // EC 自由文本只接受单引号一种写法。双引号里用反斜杠转义 ` 或 $ 虽然
1618
+ // 能得到字面量,但同一条规则下要记两套写法,所以对 ec 命令显式拒绝,
1619
+ // 把正文一律导向单引号。非 ec 命令保持原行为。
1620
+ if (isEcCommand && (next === '`' || next === '$')) {
1621
+ return parseFailure('unsupported-escape', input, index, argv.length, 'posix');
1622
+ }
1530
1623
  token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
1531
1624
  continue;
1532
1625
  }
@@ -1568,6 +1661,54 @@ export function parseLiteralShellCommand(command, options = {}) {
1568
1661
  token += next;
1569
1662
  continue;
1570
1663
  }
1664
+ // ANSI-C quoting: $'...' allows \n \t \r \\ \' escapes, same as bash $'...'.
1665
+ // Must be checked before SHELL_UNQUOTED_META because $ is in that set.
1666
+ if (char === '$' && input[index + 1] === "'") {
1667
+ tokenStarted = true;
1668
+ index += 2; // skip $ and opening '
1669
+ let ansiClosed = false;
1670
+ while (index < input.length) {
1671
+ const c = input[index];
1672
+ if (c === "'") {
1673
+ ansiClosed = true;
1674
+ break; // leave index at closing '; for-loop increment moves past it
1675
+ }
1676
+ if (c === '\\') {
1677
+ const esc = input[++index];
1678
+ if (esc === undefined)
1679
+ break;
1680
+ switch (esc) {
1681
+ case 'n':
1682
+ token += '\n';
1683
+ break;
1684
+ case 't':
1685
+ token += '\t';
1686
+ break;
1687
+ case 'r':
1688
+ token += '\r';
1689
+ break;
1690
+ case '\\':
1691
+ token += '\\';
1692
+ break;
1693
+ case "'":
1694
+ token += "'";
1695
+ break;
1696
+ default:
1697
+ token += `\\${esc}`;
1698
+ break;
1699
+ }
1700
+ index++;
1701
+ continue;
1702
+ }
1703
+ token += c;
1704
+ index++;
1705
+ }
1706
+ if (!ansiClosed)
1707
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
1708
+ if (!isPosixQuotedTokenBoundary(input[index + 1]))
1709
+ return parseFailure('invalid-quote', input, index, argv.length, 'posix');
1710
+ continue;
1711
+ }
1571
1712
  // `#` starts a shell comment only at the beginning of a word. Channel
1572
1713
  // peer keys such as `aun#alice` are ordinary literal arguments.
1573
1714
  if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
@@ -1623,6 +1764,11 @@ export function parseCodexShellCommandArgv(command, options = {}) {
1623
1764
  });
1624
1765
  return parsed.ok ? canonicalizeEvolcoreCommandArgv(parsed.argv) : null;
1625
1766
  }
1767
+ const stringCarrier = resolveCodexShellCarrierString(command);
1768
+ if (stringCarrier) {
1769
+ const parsed = parseCodexCarrierCommand(stringCarrier, options);
1770
+ return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
1771
+ }
1626
1772
  const outer = parseLiteralShellArgv(command, options);
1627
1773
  if (outer) {
1628
1774
  const carrier = resolveCodexShellCarrierArgv(outer);
@@ -1630,20 +1776,11 @@ export function parseCodexShellCommandArgv(command, options = {}) {
1630
1776
  const parsed = parseCodexCarrierCommand(carrier, options);
1631
1777
  return parsed.ok ? parsed.argv : null;
1632
1778
  }
1633
- const stringCarrier = resolveCodexShellCarrierString(command);
1634
- if (stringCarrier) {
1635
- const parsed = parseCodexCarrierCommand(stringCarrier, options);
1636
- return parsed.ok ? parsed.argv : null;
1637
- }
1638
1779
  if (parseCodexCmdCarrierStringFailure(command, options))
1639
1780
  return null;
1640
1781
  return normalizeEvolcoreCommandArgv(outer);
1641
1782
  }
1642
- const stringCarrier = resolveCodexShellCarrierString(command);
1643
- if (!stringCarrier)
1644
- return null;
1645
- const parsed = parseCodexCarrierCommand(stringCarrier, options);
1646
- return parsed.ok ? normalizeEvolcoreCommandArgv(parsed.argv) : null;
1783
+ return null;
1647
1784
  }
1648
1785
  /** Parse a Codex tool input while preserving safe syntax failure metadata. */
1649
1786
  export function parseCodexToolCommand(input, options = {}) {
@@ -1687,22 +1824,20 @@ export function parseCodexToolCommand(input, options = {}) {
1687
1824
  ? { ...parsed, argv: normalizeEvolcoreCommandArgv(parsed.argv) }
1688
1825
  : parsed;
1689
1826
  }
1827
+ const stringCarrier = resolveCodexShellCarrierString(command);
1828
+ if (stringCarrier) {
1829
+ return parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options });
1830
+ }
1690
1831
  const outer = parseLiteralShellCommand(command, { allowManagedTmpDir: true });
1691
1832
  if (outer.ok) {
1692
1833
  const carrier = resolveCodexShellCarrierArgv(outer.argv);
1693
1834
  if (carrier !== undefined) {
1694
1835
  return parseCodexCarrierCommand(carrier, { allowManagedTmpDir: true, ...options });
1695
1836
  }
1696
- const stringCarrier = resolveCodexShellCarrierString(command);
1697
- return stringCarrier
1698
- ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
1699
- : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
1700
- ?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
1837
+ return parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true })
1838
+ ?? { ...outer, argv: normalizeEvolcoreCommandArgv(outer.argv) };
1701
1839
  }
1702
- const stringCarrier = resolveCodexShellCarrierString(command);
1703
- return stringCarrier
1704
- ? parseCodexCarrierCommand(stringCarrier, { allowManagedTmpDir: true, ...options })
1705
- : parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
1840
+ return parseCodexCmdCarrierStringFailure(command, { allowManagedTmpDir: true }) ?? outer;
1706
1841
  }
1707
1842
  function isApplyPatchExecutable(value) {
1708
1843
  const normalized = value.replaceAll('\\', '/').replace(/\/+$/u, '');
@@ -1715,7 +1850,7 @@ function normalizeApplyPatchPath(value) {
1715
1850
  return undefined;
1716
1851
  return normalized;
1717
1852
  }
1718
- function parseApplyPatchDocument(source) {
1853
+ export function parseApplyPatchDocument(source) {
1719
1854
  const lines = source.replace(/\r\n?/gu, '\n').split('\n');
1720
1855
  while (lines.at(-1) === '')
1721
1856
  lines.pop();
@@ -2658,6 +2793,17 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
2658
2793
  });
2659
2794
  return { kind: 'composite', issue: parsed.ok ? 'shell-composition' : parsed.issue };
2660
2795
  }
2796
+ const stringCarrier = resolveCodexShellCarrierString(stripped);
2797
+ if (stringCarrier) {
2798
+ const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
2799
+ if (nested.ok && nested.argv[0] === 'ec')
2800
+ return { kind: 'literal' };
2801
+ if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
2802
+ || (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
2803
+ return { kind: 'composite', issue: nested.issue };
2804
+ }
2805
+ return { kind: 'none' };
2806
+ }
2661
2807
  const literal = parseLiteralShellCommand(stripped, parseOptions);
2662
2808
  if (literal.ok) {
2663
2809
  if (normalizeEvolcoreCommandArgv(literal.argv)[0] === 'ec')
@@ -2672,16 +2818,6 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
2672
2818
  return { kind: 'composite', issue: nested.issue };
2673
2819
  }
2674
2820
  }
2675
- const stringCarrier = resolveCodexShellCarrierString(stripped);
2676
- if (stringCarrier) {
2677
- const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
2678
- if (nested.ok && nested.argv[0] === 'ec')
2679
- return { kind: 'literal' };
2680
- if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
2681
- || (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
2682
- return { kind: 'composite', issue: nested.issue };
2683
- }
2684
- }
2685
2821
  const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
2686
2822
  if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
2687
2823
  const nested = parseLiteralShellCommand(malformedCmdBody, {
@@ -2694,17 +2830,6 @@ export function classifyEvolcoreShellCommand(command, options = {}) {
2694
2830
  }
2695
2831
  return { kind: 'none' };
2696
2832
  }
2697
- const stringCarrier = resolveCodexShellCarrierString(stripped);
2698
- if (stringCarrier) {
2699
- const nested = parseCodexCarrierCommand(stringCarrier, parseOptions);
2700
- if (nested.ok && nested.argv[0] === 'ec')
2701
- return { kind: 'literal' };
2702
- if (!nested.ok && (hasCarrierEcCommandPrefix(stringCarrier.command, stringCarrier.dialect)
2703
- || (stringCarrier.dialect === 'powershell' && hasPowerShellCredentialAssignment(stringCarrier.command)))) {
2704
- return { kind: 'composite', issue: nested.issue };
2705
- }
2706
- return { kind: 'none' };
2707
- }
2708
2833
  const malformedCmdBody = extractCodexCmdCarrierBody(stripped);
2709
2834
  if (malformedCmdBody && looksLikeEcCommandBody(malformedCmdBody)) {
2710
2835
  const nested = parseLiteralShellCommand(malformedCmdBody, {
@@ -3022,7 +3147,7 @@ export function describeProactiveSendCommandFailure(toolName, input, session) {
3022
3147
  const from = argv[3];
3023
3148
  const to = argv[4];
3024
3149
  const fullFormHint = `${prefix}缺少发送者与目标。完整形式为 ${expectedForm};`
3025
- + '省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send "<正文>" 继承当前会话。';
3150
+ + `省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send '<正文>' 继承当前会话。`;
3026
3151
  if (!from || from.startsWith('--')) {
3027
3152
  return fullFormHint;
3028
3153
  }
@@ -3030,7 +3155,7 @@ export function describeProactiveSendCommandFailure(toolName, input, session) {
3030
3155
  return looksLikeAid(from)
3031
3156
  ? fullFormHint
3032
3157
  : `${prefix}缺少发送者与目标("${from}" 是正文而非 AID)。完整形式为 ${expectedForm};`
3033
- + '省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send "<正文>" 继承当前会话。';
3158
+ + `省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send '<正文>' 继承当前会话。`;
3034
3159
  }
3035
3160
  if (!looksLikeAid(from) || !looksLikeAid(to)) {
3036
3161
  return `${prefix}发送者/目标不是合法 AID(收到 from="${from}" to="${to}")。完整形式为 ${expectedForm}。`;
@@ -938,7 +938,11 @@ export function lClassGrantIncludesProtectedPath(value, options = {}) {
938
938
  // A grant covering an Agent root also covers its L-class configuration.
939
939
  if (segments.length <= 2)
940
940
  return true;
941
- return ['config.json', 'contact.json', 'contact-operations.jsonl', 'relations', 'roles', 'triggers']
941
+ // relations/ 下的 L 类只有 relations/<peer>/config.json:只有范围是 relations/ 或
942
+ // relations/<peer> 本身时才覆盖它,更深的路径(如 relations/<peer>/memory/…)不会。
943
+ if (segments[2] === 'relations')
944
+ return segments.length <= 4;
945
+ return ['config.json', 'contact.json', 'contact-operations.jsonl', 'roles', 'triggers']
942
946
  .includes(segments[2]);
943
947
  }
944
948
  return false;
@@ -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', 'git', 'wc', 'stat', 'lstat', 'awk',
6
+ 'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'tail', 'sort', 'git', 'wc', 'stat', 'lstat', 'awk',
7
7
  'bash', 'sh',
8
8
  ]);
9
9
  const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
@@ -447,6 +447,13 @@ function analyzeHead(args) {
447
447
  const parsed = parseOptionsAndPaths(args, new Set(['-c', '-n', '--bytes', '--lines']));
448
448
  return parsed?.paths.map(exact) ?? null;
449
449
  }
450
+ function analyzeTail(args) {
451
+ // -f/--follow opens an indefinite watch loop — not a bounded read query.
452
+ if (args.some(a => a === '-f' || a === '--follow' || /^-[^-]*f/.test(a)))
453
+ return null;
454
+ const parsed = parseOptionsAndPaths(args, new Set(['-c', '-n', '--bytes', '--lines']));
455
+ return parsed?.paths.map(exact) ?? null;
456
+ }
450
457
  function analyzeSort(args) {
451
458
  if (args.some(argument => /^(?:-o|-T)(?:.+|$)|^(?:--output|--compress-program|--temporary-directory|--files0-from)(?:=|$)/.test(argument))) {
452
459
  return null;
@@ -828,68 +835,95 @@ function analyzeCommand(argv) {
828
835
  const args = argv.slice(1);
829
836
  let pathOperands = null;
830
837
  let accessKind = 'content';
838
+ let purpose = 'unknown';
831
839
  switch (executable) {
832
840
  case 'echo':
833
841
  pathOperands = [];
842
+ purpose = 'passthrough';
834
843
  break;
835
844
  case 'pwd':
836
845
  pathOperands = args.every(argument => ['-L', '-P', '--help', '--version'].includes(argument)) ? [] : null;
846
+ purpose = 'passthrough';
837
847
  break;
838
848
  case 'ls':
839
849
  pathOperands = analyzeLs(args);
850
+ purpose = 'enumerate';
840
851
  break;
841
852
  case 'cat':
842
853
  pathOperands = analyzeCat(args);
854
+ purpose = 'content-read';
843
855
  break;
844
856
  case 'grep':
845
857
  pathOperands = analyzeGrep(args);
858
+ purpose = 'search';
846
859
  break;
847
860
  case 'rg':
848
861
  pathOperands = analyzeRg(args);
862
+ purpose = 'search';
849
863
  break;
850
864
  case 'find':
851
865
  pathOperands = analyzeFind(args);
866
+ purpose = 'enumerate';
852
867
  break;
853
868
  case 'sed':
854
869
  pathOperands = analyzeSed(args);
870
+ purpose = 'content-read';
855
871
  break;
856
872
  case 'head':
857
873
  pathOperands = analyzeHead(args);
874
+ purpose = 'content-read';
875
+ break;
876
+ case 'tail':
877
+ pathOperands = analyzeTail(args);
878
+ purpose = 'content-read';
858
879
  break;
859
880
  case 'sort':
860
881
  pathOperands = analyzeSort(args);
882
+ // sort 有路径参数时是文件读取,无路径参数时是纯 stdin 排序
883
+ purpose = 'stdin-transform';
861
884
  break;
862
885
  case 'git':
863
886
  pathOperands = analyzeGit(args);
887
+ purpose = 'vcs-query';
864
888
  break;
865
889
  case 'wc':
866
890
  pathOperands = analyzeWc(args);
891
+ // wc 有路径参数时是元数据读取,无路径参数时是纯 stdin 统计
892
+ purpose = 'metadata-read';
867
893
  break;
868
894
  case 'stat':
869
895
  case 'lstat':
870
896
  pathOperands = analyzeStat(args);
871
897
  accessKind = 'metadata';
898
+ purpose = 'metadata-read';
872
899
  break;
873
900
  case 'awk':
874
901
  pathOperands = analyzeAwk(args);
902
+ purpose = 'content-read';
875
903
  break;
876
904
  case 'bash':
877
905
  case 'sh':
878
906
  pathOperands = analyzeShellSyntaxCheck(args);
907
+ purpose = 'syntax-check';
879
908
  break;
880
909
  }
881
910
  if (!pathOperands)
882
911
  return null;
883
912
  if (['ls', 'find'].includes(executable))
884
913
  accessKind = 'enumerate';
914
+ // 无路径操作数的命令是纯 stdin→stdout 变换,可安全出现在管道任意位置。
915
+ // 对于 sort/wc/awk/grep/head/tail/sed:有路径参数时读文件,无路径参数时为 stdin 变换。
916
+ const stdinTransform = pathOperands.length === 0
917
+ && ['sort', 'wc', 'awk', 'grep', 'rg', 'head', 'tail', 'sed'].includes(executable);
918
+ // 修正 purpose:stdin 变换时统一标记,避免 'metadata-read'/'content-read' 歧义
919
+ const resolvedPurpose = stdinTransform ? 'stdin-transform' : purpose;
885
920
  return {
886
921
  command: {
887
922
  executable,
888
923
  argv,
889
924
  pathOperands,
890
- // Keep the established IR shape for ordinary queries. Only metadata
891
- // commands need an explicit action marker; content/enumeration are
892
- // derived from their operands by policy consumers.
925
+ stdinTransform,
926
+ purpose: resolvedPurpose,
893
927
  ...(accessKind === 'metadata' ? { accessKind } : {}),
894
928
  },
895
929
  };
@@ -910,7 +944,11 @@ export function analyzeReadonlyShellQuery(command, options = {}) {
910
944
  if (!analyzed)
911
945
  return { kind: 'unproven', reason: 'unsupported-command-or-option' };
912
946
  if (index > 0) {
913
- if (!['head', 'sort', 'grep', 'wc'].includes(analyzed.command.executable)) {
947
+ // 管道后续段必须是纯 stdin→stdout 变换(stdinTransform=true 由 analyzeCommand 打标,
948
+ // 表示命令无 FS 路径操作数、只处理 stdin 流)。
949
+ // 黑名单规则已在 analyzeCommand 内部拦截(如 sort -o、tail -f 等),到这里的都是
950
+ // 白名单规则捞回后的安全命令,用标签判断而非硬编码可执行文件列表。
951
+ if (!analyzed.command.stdinTransform) {
914
952
  return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
915
953
  }
916
954
  if (analyzed.command.pathOperands.length > 0) {