evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -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) {
@@ -0,0 +1,334 @@
1
+ /**
2
+ * session-path-grants.ts — 用户会话路径授权表
3
+ *
4
+ * 形状:路径模板 → [{ identities, access }],identities 为角色名(owner/admin/member/visitor)。
5
+ *
6
+ * 只服务用户会话。系统会话(记忆整理等)与角色体系无关,路径边界由
7
+ * src/core/system-session/ 下的模块在代码里决定,不走本表、不并运行时授予。
8
+ *
9
+ * 判定规则(两条,别加第三条):
10
+ * 1. 同会话多条命中 → 取最大权限(none 显式覆盖通配)
11
+ * 2. 与权限模式取交集:readonly 下 write 降级为 read
12
+ *
13
+ * 多表合并(mergeSessionPathGrantTables)同路径追加而非替换。
14
+ * 运行时动态授予(ec ctl path grant)走 grantSessionPath(),按 sessionId 持久化。
15
+ *
16
+ * 详见 docs/memory/system-session-plugin-design.md
17
+ */
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { resolveRoot } from '../../paths.js';
21
+ import { resolveProtectedCandidate, isSameOrDescendant } from './protected-paths.js';
22
+ import { logger } from '../../utils/logger.js';
23
+ import { registerContent, unregisterFile } from '../file-integrity/store.js';
24
+ const ACCESS_RANK = { none: -1, read: 1, write: 2 };
25
+ /**
26
+ * 内置默认授权表。当前为空:用户会话缺省只能读工作目录,
27
+ * 额外路径由 owner/admin 经 `ec ctl path grant` 按会话授予。
28
+ */
29
+ export const BUILTIN_SESSION_PATH_GRANTS = {};
30
+ /**
31
+ * 合并多张授权表:同路径模板的条目**追加**,不替换。
32
+ *
33
+ * 对象展开 `{ ...a, ...b }` 在同键时会整组丢弃前者——会话表里一旦出现同名路径,
34
+ * 前一张表的授予就静默消失。追加后判定仍走「同路径取最大、none 锁定」,
35
+ * 所以想收窄只能显式写 none,不会因为键名巧合丢权限。
36
+ */
37
+ export function mergeSessionPathGrantTables(...tables) {
38
+ const merged = {};
39
+ for (const table of tables) {
40
+ if (!table)
41
+ continue;
42
+ for (const [template, entries] of Object.entries(table)) {
43
+ if (!Array.isArray(entries))
44
+ continue;
45
+ merged[template] = [...(merged[template] ?? []), ...entries];
46
+ }
47
+ }
48
+ return merged;
49
+ }
50
+ /** 判断一份授权表里的条目是否合法。 */
51
+ export function isValidGrantEntry(entry) {
52
+ return Array.isArray(entry.identities)
53
+ && entry.identities.length > 0
54
+ && entry.identities.every(value => typeof value === 'string' && value.trim().length > 0)
55
+ && (entry.access === 'none' || entry.access === 'read' || entry.access === 'write');
56
+ }
57
+ /** 替换路径模板里的变量。未知变量保持原样,由调用方按「不存在」处理。 */
58
+ export function substitutePathTemplate(template, ctx, vars) {
59
+ return template.replace(/\$([A-Z_]+)/g, (match, name) => vars[name] ?? match);
60
+ }
61
+ function buildVars(ctx) {
62
+ return {
63
+ HOME: resolveRoot(),
64
+ SELF_AID: ctx.selfAid,
65
+ PEER: ctx.peerKey,
66
+ OWNER: ctx.ownerAid,
67
+ PROJECT: ctx.projectPath,
68
+ };
69
+ }
70
+ /** 模板里出现未解析的变量时视为不可用条目。展开后的路径必须是绝对路径。 */
71
+ function resolveTemplate(template, ctx) {
72
+ const resolved = substitutePathTemplate(template, ctx, buildVars(ctx));
73
+ const unresolved = resolved.match(/\$[A-Z_]+/g);
74
+ if (unresolved) {
75
+ // 未知变量(拼错)与已知变量缺值(如私聊会话没有 $OWNER)都会走到这里;
76
+ // 两者都让整条授予失效,留一行日志,否则下游只剩一个原因不明的读拒绝。
77
+ logger.warn(`[path-grants] 授权模板含未解析变量,条目跳过: template=${template} unresolved=${[...new Set(unresolved)].join(',')} identity=${ctx.identity ?? ''}`);
78
+ return undefined;
79
+ }
80
+ if (!path.isAbsolute(resolved)) {
81
+ logger.warn(`[path-grants] 授权模板展开后不是绝对路径,条目跳过: template=${template} resolved=${resolved}`);
82
+ return undefined;
83
+ }
84
+ return resolveProtectedCandidate(resolved);
85
+ }
86
+ /** 当前会话是否命中某条授予的身份。 */
87
+ function identityMatches(entry, identity) {
88
+ const normalized = identity.trim().toLowerCase();
89
+ return entry.identities.some(value => value.trim().toLowerCase() === normalized);
90
+ }
91
+ /**
92
+ * 权限模式交集。系统会话不传 permissionMode,按授予原样生效。
93
+ * readonly 下 write 降级为 read;none 永不升级。
94
+ */
95
+ function applyPermissionModeCeiling(access, permissionMode) {
96
+ if (access === 'none' || !permissionMode)
97
+ return access;
98
+ return permissionMode === 'readonly' ? 'read' : access;
99
+ }
100
+ /**
101
+ * 计算当前会话在给定授权表下的**有效路径集合**。
102
+ *
103
+ * 返回 path → access 的映射,只含 access 非 none 的条目。同路径多条命中取最大权限
104
+ * (none 除外——任一命中 none 即整体拒绝)。
105
+ */
106
+ export function resolveEffectivePathGrants(table, ctx) {
107
+ const effective = new Map();
108
+ const identity = (ctx.identity ?? '').trim();
109
+ if (!identity)
110
+ return effective;
111
+ for (const [template, entries] of Object.entries(table)) {
112
+ if (!Array.isArray(entries))
113
+ continue;
114
+ const hits = entries.filter(entry => isValidGrantEntry(entry) && identityMatches(entry, identity));
115
+ if (hits.length === 0)
116
+ continue;
117
+ const resolved = resolveTemplate(template, ctx);
118
+ if (!resolved)
119
+ continue;
120
+ const previous = effective.get(resolved);
121
+ // none 是显式拒绝,一旦出现就锁定,不被同路径的其它授予放宽
122
+ if (previous === 'none')
123
+ continue;
124
+ let next = 'none';
125
+ for (const hit of hits) {
126
+ const grant = applyPermissionModeCeiling(hit.access, ctx.permissionMode);
127
+ if (grant === 'none') {
128
+ next = 'none';
129
+ break;
130
+ }
131
+ if (ACCESS_RANK[grant] > ACCESS_RANK[next])
132
+ next = grant;
133
+ }
134
+ if (next === 'none')
135
+ effective.set(resolved, 'none');
136
+ else
137
+ effective.set(resolved, next);
138
+ }
139
+ return effective;
140
+ }
141
+ /**
142
+ * 有效路径授权表的可读根目录列表(含只读授予)。
143
+ *
144
+ * 判定语义是「本路径落在任一可读根之下即为可读」,因此这里连同 write 授予一起返回——
145
+ * write 蕴含 read。
146
+ */
147
+ export function resolveGrantedReadRoots(table, ctx) {
148
+ const roots = [];
149
+ for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
150
+ if (access === 'read' || access === 'write')
151
+ roots.push(value);
152
+ }
153
+ return roots;
154
+ }
155
+ /** 候选路径是否落在任一授权根之下(含根自身)。 */
156
+ export function isPathGranted(candidate, roots) {
157
+ const resolved = resolveProtectedCandidate(candidate);
158
+ return roots.some(root => isSameOrDescendant(resolved, root));
159
+ }
160
+ const runtimeGrants = new Map();
161
+ /**
162
+ * 持久化位置:$HOME/data/path/grants/<sessionId>.json —— **每会话一个文件**。
163
+ *
164
+ * 不用单个共用大文件:那样一份损坏或半写会让**所有**会话的授予一起丢,
165
+ * 而且清理某个会话要重写整文件。一会话一文件让隔离和回收都变成本地操作。
166
+ *
167
+ * 代价是首次加载要扫目录。授予是低频操作,目录规模等于活跃会话数,可接受。
168
+ */
169
+ function runtimeGrantDir() {
170
+ return path.join(resolveRoot(), 'data', 'path', 'grants');
171
+ }
172
+ /** sessionId 形如 meta_YYYYMMDD_<epochMs>,字符集安全;仍做一次防御性收敛。 */
173
+ function runtimeGrantFilePath(sessionId) {
174
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, '_');
175
+ return path.join(runtimeGrantDir(), `${safe}.json`);
176
+ }
177
+ /** 原子写:同目录临时文件 + rename,避免读到半写的 JSON。 */
178
+ function persistSessionGrants(sessionId) {
179
+ const file = runtimeGrantFilePath(sessionId);
180
+ try {
181
+ const list = runtimeGrants.get(sessionId);
182
+ if (!list || list.length === 0) {
183
+ fs.rmSync(file, { force: true });
184
+ try {
185
+ unregisterFile(file);
186
+ }
187
+ catch { /* best-effort */ }
188
+ return;
189
+ }
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ const tmp = `${file}.tmp-${process.pid}`;
192
+ const content = `${JSON.stringify(list, null, 2)}\n`;
193
+ // 先登记,再写盘:避免 CLI 进程读到"新内容 + 旧台账"的窗口
194
+ try {
195
+ registerContent(file, content);
196
+ }
197
+ catch { /* best-effort */ }
198
+ fs.writeFileSync(tmp, content, 'utf-8');
199
+ fs.renameSync(tmp, file);
200
+ }
201
+ catch { /* 持久化失败不阻断本次会话:内存授予仍然生效 */ }
202
+ }
203
+ let runtimeGrantsLoaded = false;
204
+ /** 首次使用时从磁盘加载一次。单个文件损坏只影响它自己那个会话。 */
205
+ function ensureRuntimeGrantsLoaded() {
206
+ if (runtimeGrantsLoaded)
207
+ return;
208
+ runtimeGrantsLoaded = true;
209
+ let files;
210
+ try {
211
+ files = fs.readdirSync(runtimeGrantDir()).filter(name => name.endsWith('.json'));
212
+ }
213
+ catch {
214
+ return; // 首次运行:目录还不存在
215
+ }
216
+ for (const name of files) {
217
+ const sessionId = name.slice(0, -'.json'.length);
218
+ try {
219
+ const parsed = JSON.parse(fs.readFileSync(path.join(runtimeGrantDir(), name), 'utf-8'));
220
+ if (Array.isArray(parsed) && parsed.length > 0)
221
+ runtimeGrants.set(sessionId, parsed);
222
+ }
223
+ catch { /* 该会话的授权文件损坏:跳过它,不影响其它会话 */ }
224
+ }
225
+ }
226
+ /** 供测试与重载清空(含持久化,避免下个用例读到上个用例的残留)。 */
227
+ export function clearRuntimePathGrants(sessionId) {
228
+ ensureRuntimeGrantsLoaded();
229
+ if (sessionId) {
230
+ runtimeGrants.delete(sessionId);
231
+ persistSessionGrants(sessionId);
232
+ return;
233
+ }
234
+ for (const id of [...runtimeGrants.keys()]) {
235
+ runtimeGrants.delete(id);
236
+ persistSessionGrants(id);
237
+ }
238
+ }
239
+ /** 登记一条运行时授予。授权校验由 IPC 层负责(仅 owner/admin)。 */
240
+ export function grantSessionPath(record) {
241
+ ensureRuntimeGrantsLoaded();
242
+ const list = runtimeGrants.get(record.sessionId) ?? [];
243
+ // 同路径同身份重复登记按覆盖处理,避免列表无限增长
244
+ const kept = list.filter(existing => !(existing.path === record.path && existing.identity === record.identity));
245
+ kept.push(record);
246
+ runtimeGrants.set(record.sessionId, kept);
247
+ persistSessionGrants(record.sessionId);
248
+ }
249
+ /** 撤销一条运行时授予,返回是否命中。 */
250
+ export function revokeSessionPath(sessionId, template, identity) {
251
+ ensureRuntimeGrantsLoaded();
252
+ const list = runtimeGrants.get(sessionId);
253
+ if (!list)
254
+ return false;
255
+ const before = list.length;
256
+ const kept = list.filter(record => !(record.path === template && (identity === undefined || record.identity === identity)));
257
+ if (kept.length === before)
258
+ return false;
259
+ if (kept.length === 0)
260
+ runtimeGrants.delete(sessionId);
261
+ else
262
+ runtimeGrants.set(sessionId, kept);
263
+ persistSessionGrants(sessionId);
264
+ return true;
265
+ }
266
+ /** 列出某会话的运行时授予。 */
267
+ export function listRuntimePathGrants(sessionId) {
268
+ ensureRuntimeGrantsLoaded();
269
+ return runtimeGrants.get(sessionId) ?? [];
270
+ }
271
+ /**
272
+ * 会话结束时的清理入口。
273
+ *
274
+ * 只清**该会话自己的**运行时授予:`ec ctl path grant` 是以当前会话为归属登记的,
275
+ * 会话结束后不再有会话身份去消费它。
276
+ */
277
+ export function releaseSessionPathGrants(sessionId) {
278
+ ensureRuntimeGrantsLoaded();
279
+ if (!runtimeGrants.delete(sessionId))
280
+ return;
281
+ persistSessionGrants(sessionId);
282
+ }
283
+ /**
284
+ * 把某会话的运行时授予并入授权表。
285
+ *
286
+ * 运行时授予以**会话身份**登记:identity 缺省时表示「该会话自身」,
287
+ * 亦即用调用方的 identity 匹配。
288
+ */
289
+ export function mergeRuntimeGrantsForSession(table, sessionId, identity) {
290
+ if (!sessionId)
291
+ return table;
292
+ ensureRuntimeGrantsLoaded();
293
+ const list = runtimeGrants.get(sessionId);
294
+ if (!list || list.length === 0)
295
+ return table;
296
+ const merged = { ...table };
297
+ for (const record of list) {
298
+ if (!path.isAbsolute(record.path))
299
+ continue;
300
+ if (record.identity && record.identity.trim().toLowerCase() !== identity.trim().toLowerCase())
301
+ continue;
302
+ const existing = merged[record.path];
303
+ merged[record.path] = [...(existing ?? []), { identities: [identity], access: record.access }];
304
+ }
305
+ return merged;
306
+ }
307
+ /**
308
+ * 会话运行目录本身恒为可读根:它是本会话私有的临时根,runner 自己的临时产物
309
+ * 都落在这里(见 task-context.ts 的 ensureSessionRuntimeDir)。
310
+ *
311
+ * TMPDIR 写能力另由 managedTempDir 负责,不在这里表达。
312
+ */
313
+ export function sessionOwnTempReadRoot(sessionRuntimeDir) {
314
+ if (!sessionRuntimeDir || !path.isAbsolute(sessionRuntimeDir))
315
+ return [];
316
+ return [resolveProtectedCandidate(sessionRuntimeDir)];
317
+ }
318
+ /** 缺省的文件系统根:会话工作目录。 */
319
+ export function defaultProjectRoot(projectPath) {
320
+ if (!projectPath || !path.isAbsolute(projectPath))
321
+ return [];
322
+ return [resolveProtectedCandidate(projectPath)];
323
+ }
324
+ /** 供诊断输出:当前会话的有效路径集合(含运行时授予)。 */
325
+ export function describeEffectivePaths(table, ctx, sessionRuntimeDir) {
326
+ const result = [];
327
+ for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
328
+ result.push({ path: value, access });
329
+ }
330
+ for (const value of sessionOwnTempReadRoot(sessionRuntimeDir)) {
331
+ result.push({ path: value, access: 'read' });
332
+ }
333
+ return result;
334
+ }