evolcore 0.0.27 → 0.0.29-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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -4,8 +4,8 @@ import os from 'os';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { resolveRoot } from '../../paths.js';
7
- import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
8
- import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
7
+ import { containsHClassReference, containsAgentScopeReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
8
+ import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseShellApplyPatchInvocation, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
9
9
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
10
10
  /** Resolve the session-owned temporary root supplied by the execution host. */
11
11
  export function resolveManagedTempDir(env = process.env) {
@@ -88,6 +88,99 @@ export function checkManagedTempShellCommand(command, options = {}) {
88
88
  }
89
89
  return { kind: 'allow', paths: uniqueCandidates };
90
90
  }
91
+ function sharedTemporaryRoots(options) {
92
+ if (process.platform === 'win32')
93
+ return windowsTempRoots(options);
94
+ return ['/tmp', '/var/tmp', '/dev/shm']
95
+ .map(value => resolveProtectedCandidate(value))
96
+ .filter((value, index, values) => values.indexOf(value) === index);
97
+ }
98
+ function collectManagedTempToolPaths(toolName, input) {
99
+ const paths = [];
100
+ if (toolName === 'FileChange') {
101
+ if (typeof input.grantRoot === 'string' && input.grantRoot)
102
+ paths.push(input.grantRoot);
103
+ collectFileChangePaths(input.fileChanges, paths);
104
+ }
105
+ else if (['Write', 'Edit', 'NotebookEdit'].includes(toolName)) {
106
+ for (const key of ['file_path', 'notebook_path', 'path']) {
107
+ if (typeof input[key] === 'string' && input[key])
108
+ paths.push(input[key]);
109
+ }
110
+ }
111
+ else if (toolName === 'PermissionGrant') {
112
+ if (typeof input.grantRoot === 'string' && input.grantRoot)
113
+ paths.push(input.grantRoot);
114
+ collectFilesystemWriteGrantPaths(input.permissions, paths);
115
+ collectFilesystemWriteGrantPaths(input.additionalPermissions, paths);
116
+ }
117
+ return [...new Set(paths)];
118
+ }
119
+ /**
120
+ * Enforce the session temporary boundary for path-oriented tools before a
121
+ * runner-specific approval flow. Project paths remain governed by the normal
122
+ * H/L-class and workspace policies; only temporary-looking operands are
123
+ * restricted to the current managed TMPDIR.
124
+ */
125
+ export function checkManagedTempToolPaths(toolName, input, options = {}) {
126
+ const candidates = collectManagedTempToolPaths(toolName, input);
127
+ if (candidates.length === 0)
128
+ return { kind: 'none' };
129
+ const root = resolveManagedTempDir(options.env);
130
+ const workspace = options.workspacePath
131
+ ? resolveProtectedCandidate(options.workspacePath, options.cwd)
132
+ : undefined;
133
+ const temporaryRoots = sharedTemporaryRoots(options);
134
+ const outside = [];
135
+ let hardLink;
136
+ for (const rawCandidate of candidates) {
137
+ const explicitManagedTemp = /^\$(?:\{TMPDIR\}|TMPDIR)(?:[\\/]|$)/.test(rawCandidate.trim());
138
+ const expanded = substituteTmpDir(rawCandidate, root);
139
+ const lexical = path.isAbsolute(expanded)
140
+ ? path.resolve(expanded)
141
+ : path.resolve(options.cwd ?? process.cwd(), expanded);
142
+ const canonical = resolveProtectedCandidate(lexical, options.cwd);
143
+ const canonicalInWorkspace = !!workspace && isSameOrDescendant(canonical, workspace);
144
+ const lexicalInManagedTemp = !!root && isSameOrDescendant(lexical, root);
145
+ const canonicalInManagedTemp = !!root && isSameOrDescendant(canonical, root);
146
+ const temporaryLooking = explicitManagedTemp
147
+ || lexicalInManagedTemp
148
+ || temporaryRoots.some(tempRoot => isSameOrDescendant(lexical, tempRoot))
149
+ || temporaryRoots.some(tempRoot => isSameOrDescendant(canonical, tempRoot));
150
+ if (canonicalInWorkspace && !explicitManagedTemp)
151
+ continue;
152
+ if (!temporaryLooking)
153
+ continue;
154
+ if (!root || !canonicalInManagedTemp) {
155
+ outside.push(rawCandidate);
156
+ continue;
157
+ }
158
+ try {
159
+ const stat = fs.statSync(canonical);
160
+ if (stat.isFile() && stat.nlink > 1)
161
+ hardLink = canonical;
162
+ }
163
+ catch {
164
+ // A new target is valid when its canonical existing ancestor is inside
165
+ // the managed root; the runner will create it after this preflight.
166
+ }
167
+ }
168
+ if (hardLink) {
169
+ return {
170
+ kind: 'deny',
171
+ paths: [hardLink],
172
+ reason: 'managed TMPDIR target is a hard link and cannot be safely modified',
173
+ };
174
+ }
175
+ if (outside.length > 0) {
176
+ return {
177
+ kind: 'deny',
178
+ paths: outside,
179
+ reason: 'temporary path is outside the session-managed TMPDIR',
180
+ };
181
+ }
182
+ return { kind: 'none' };
183
+ }
91
184
  /**
92
185
  * Replace only already-validated temporary path operands before a second
93
186
  * policy pass. This prevents a harmless `$TMPDIR/CA/root` name from looking
@@ -689,7 +782,7 @@ function protectedClassPath(value, className, options) {
689
782
  if (!looksLikeLiteralShellPath(value))
690
783
  return undefined;
691
784
  const matched = className === 'h'
692
- ? (containsHClassReference(value) || isHClassPath(value, options))
785
+ ? (containsHClassReference(value, options) || containsAgentScopeReference(value, options) || isHClassPath(value, options))
693
786
  : (containsLClassReference(value) || isLClassPath(value, options));
694
787
  return matched ? resolveProtectedCandidate(value, options.cwd) : undefined;
695
788
  }
@@ -942,6 +1035,35 @@ function literalGitOperands(argv) {
942
1035
  function literalReadonlyOperands(argv) {
943
1036
  const executable = commandName(argv[0]);
944
1037
  const args = argv.slice(1);
1038
+ if (['get-content', 'gc', 'type'].includes(executable)) {
1039
+ const values = [];
1040
+ const pathOptions = new Set(['-path', '-literalpath', '-pspath']);
1041
+ const optionsWithValues = new Set([
1042
+ ...pathOptions,
1043
+ '-filter', '-include', '-exclude', '-encoding', '-readcount', '-totalcount',
1044
+ '-delimiter', '-raw', '-wait', '-tail', '-stream', '-force', '-credential',
1045
+ ]);
1046
+ for (let index = 0; index < args.length; index++) {
1047
+ const argument = args[index];
1048
+ const option = argument.split('=', 1)[0].toLowerCase();
1049
+ if (argument.startsWith('-')) {
1050
+ if (pathOptions.has(option)) {
1051
+ const value = argument.includes('=') ? argument.slice(argument.indexOf('=') + 1) : args[index + 1];
1052
+ if (value) {
1053
+ values.push(value);
1054
+ if (!argument.includes('='))
1055
+ index++;
1056
+ }
1057
+ continue;
1058
+ }
1059
+ if (optionsWithValues.has(option) && !argument.includes('=') && index + 1 < args.length)
1060
+ index++;
1061
+ continue;
1062
+ }
1063
+ values.push(argument);
1064
+ }
1065
+ return values;
1066
+ }
945
1067
  if (['cat', 'ls', 'head', 'sort', 'wc', 'stat', 'lstat'].includes(executable)) {
946
1068
  const valueOptions = new Set(['-c', '--format', '--printf', '-I', '-T', '-w', '--block-size', '--color', '--format', '--hide', '--ignore', '--key', '--parallel', '--sort']);
947
1069
  const values = [];
@@ -1164,7 +1286,9 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
1164
1286
  return undefined;
1165
1287
  }
1166
1288
  }
1167
- const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
1289
+ const reference = className === 'h'
1290
+ ? (containsHClassReference(operand.value, options) || containsAgentScopeReference(operand.value, options))
1291
+ : containsLClassReference(operand.value);
1168
1292
  if (reference)
1169
1293
  return resolveProtectedCandidate(operand.value, options.cwd);
1170
1294
  if (operand.access === 'recursive') {
@@ -1192,12 +1316,12 @@ function hClassReadScopeIncludesProtectedPath(value, options) {
1192
1316
  return true;
1193
1317
  if (!isSameOrDescendant(scope, root))
1194
1318
  return false;
1195
- return getExistingHClassMaskTargets(root).some(target => isSameOrDescendant(target.path, scope));
1319
+ return getExistingHClassMaskTargets(root, { selfAid: options.selfAid }).some(target => isSameOrDescendant(target.path, scope));
1196
1320
  }
1197
1321
  function analyzeShellProtectedOperands(command, options) {
1198
1322
  const analysis = analyzeReadonlyShellQuery(command, { managedTempDir: options.managedTempDir });
1199
1323
  if (analysis.kind === 'unproven') {
1200
- const pathOptions = { cwd: options.cwd, root: options.root };
1324
+ const pathOptions = { cwd: options.cwd, root: options.root, selfAid: options.selfAid };
1201
1325
  const hClassPath = literalProtectedShellOperand(command, 'h', pathOptions);
1202
1326
  const lClassPath = literalProtectedShellOperand(command, 'l', pathOptions);
1203
1327
  return {
@@ -1272,7 +1396,7 @@ function protectedReadToolMatchesLClass(toolName, input, options) {
1272
1396
  }
1273
1397
  if (toolName === 'Glob') {
1274
1398
  const scope = globReadScope(input, options);
1275
- return lClassGrantIncludesProtectedPath(scope, { root: options.root })
1399
+ return lClassGrantIncludesProtectedPath(scope, { root: options.root, selfAid: options.selfAid })
1276
1400
  ? resolveProtectedCandidate(scope, options.cwd)
1277
1401
  : undefined;
1278
1402
  }
@@ -1280,7 +1404,9 @@ function protectedReadToolMatchesLClass(toolName, input, options) {
1280
1404
  }
1281
1405
  function protectedReadToolMatchesHClass(toolName, input, options) {
1282
1406
  const paths = protectedReadToolPaths(toolName, input);
1283
- const direct = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options));
1407
+ const direct = paths.find(filePath => containsHClassReference(filePath, options)
1408
+ || containsAgentScopeReference(filePath, options)
1409
+ || isHClassPath(filePath, options));
1284
1410
  if (direct) {
1285
1411
  return resolveProtectedCandidate(direct, options.cwd);
1286
1412
  }
@@ -1447,11 +1573,13 @@ export function checkReadonly(toolName, input, projectPath, context) {
1447
1573
  ]);
1448
1574
  if (readOnlyTools.has(toolName)) {
1449
1575
  const paths = protectedReadToolPaths(toolName, input);
1450
- const pathOptions = { cwd: projectPath, root: context?.root };
1576
+ const pathOptions = { cwd: projectPath, root: context?.root, selfAid: context?.selfAid };
1451
1577
  if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
1452
1578
  return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
1453
1579
  }
1454
- const directHPath = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions));
1580
+ const directHPath = paths.find(filePath => containsHClassReference(filePath, pathOptions)
1581
+ || containsAgentScopeReference(filePath, pathOptions)
1582
+ || isHClassPath(filePath, pathOptions));
1455
1583
  if (directHPath) {
1456
1584
  return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: resolveProtectedCandidate(directHPath, projectPath) };
1457
1585
  }
@@ -1480,6 +1608,7 @@ export function checkReadonly(toolName, input, projectPath, context) {
1480
1608
  : context?.managedTempDir ?? resolveManagedTempDir();
1481
1609
  const protectedOperands = analyzeShellProtectedOperands(cmd, {
1482
1610
  cwd: projectPath,
1611
+ selfAid: context?.selfAid,
1483
1612
  root: context?.root,
1484
1613
  managedTempDir,
1485
1614
  allowProtectedMetadata: context?.allowProtectedMetadata,
@@ -1725,6 +1854,32 @@ function collectFileChangePaths(value, output) {
1725
1854
  }
1726
1855
  }
1727
1856
  }
1857
+ function applyPatchChangePaths(changes) {
1858
+ const paths = [];
1859
+ for (const change of changes) {
1860
+ paths.push(change.path);
1861
+ if (change.movePath)
1862
+ paths.push(change.movePath);
1863
+ }
1864
+ return paths;
1865
+ }
1866
+ function applyPatchWorkspaceEscape(changes, context) {
1867
+ const workspace = resolveProtectedCandidate(context.workspacePath ?? context.projectPath);
1868
+ const managedTempDir = context.managedTempDir
1869
+ ? resolveProtectedCandidate(context.managedTempDir)
1870
+ : undefined;
1871
+ for (const candidate of applyPatchChangePaths(changes)) {
1872
+ const resolved = path.isAbsolute(candidate)
1873
+ ? resolveProtectedCandidate(candidate)
1874
+ : resolveProtectedCandidate(candidate, workspace);
1875
+ if (isSameOrDescendant(resolved, workspace))
1876
+ continue;
1877
+ if (managedTempDir && isSameOrDescendant(resolved, managedTempDir))
1878
+ continue;
1879
+ return resolved;
1880
+ }
1881
+ return undefined;
1882
+ }
1728
1883
  function requestsFilesystemRoot(value) {
1729
1884
  if (Array.isArray(value))
1730
1885
  return value.some(requestsFilesystemRoot);
@@ -1793,6 +1948,7 @@ export function checkHClassWrite(toolName, input, context) {
1793
1948
  : command;
1794
1949
  const protectedOperands = analyzeShellProtectedOperands(policyCommand, {
1795
1950
  cwd: context?.projectPath,
1951
+ selfAid: context?.selfAid,
1796
1952
  root: context?.root,
1797
1953
  managedTempDir: context?.managedTempDir,
1798
1954
  allowProtectedMetadata: context?.allowProtectedMetadata,
@@ -1830,7 +1986,7 @@ export function checkHClassWrite(toolName, input, context) {
1830
1986
  // *.json_ backup indistinguishable from an actual file access. Any nested
1831
1987
  // filesystem operation is checked again when it reaches Read/Write/Bash,
1832
1988
  // FileChange, or PermissionGrant above.
1833
- const pathOptions = { cwd: context?.projectPath, root: context?.root };
1989
+ const pathOptions = { cwd: context?.projectPath, root: context?.root, selfAid: context?.selfAid };
1834
1990
  if (['Read', 'Glob', 'Grep'].includes(toolName)) {
1835
1991
  const matchedReadPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
1836
1992
  if (matchedReadPath) {
@@ -1854,7 +2010,8 @@ export function checkHClassWrite(toolName, input, context) {
1854
2010
  for (const grantPath of grantPaths) {
1855
2011
  if (!grantPath)
1856
2012
  continue;
1857
- if (containsHClassReference(grantPath) || hClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
2013
+ if (containsHClassReference(grantPath, pathOptions) || containsAgentScopeReference(grantPath, pathOptions)
2014
+ || hClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
1858
2015
  logger.warn(`[H-Class Protection] 🔒 Permission grant covers protected paths: tool=${toolName} path=${grantPath} ` +
1859
2016
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
1860
2017
  return {
@@ -1877,7 +2034,8 @@ export function checkHClassWrite(toolName, input, context) {
1877
2034
  const candidate = path.isAbsolute(subpath)
1878
2035
  ? subpath
1879
2036
  : path.resolve(permissionWorkspace, subpath);
1880
- if (containsHClassReference(subpath) || hClassGrantIncludesProtectedPath(candidate, { root: context?.root })) {
2037
+ if (containsHClassReference(subpath, pathOptions) || containsAgentScopeReference(subpath, pathOptions)
2038
+ || hClassGrantIncludesProtectedPath(candidate, pathOptions)) {
1881
2039
  logger.warn(`[H-Class Protection] 🔒 project_roots grant covers protected paths: tool=${toolName} path=${subpath} ` +
1882
2040
  `workspace=${permissionWorkspace} session=${context?.sessionId} channel=${context?.channel} ` +
1883
2041
  `peer=${context?.peerId} role=${context?.role}`);
@@ -1890,7 +2048,9 @@ export function checkHClassWrite(toolName, input, context) {
1890
2048
  }
1891
2049
  for (const filePath of paths) {
1892
2050
  const normalized = filePath.replace(/\\/g, '/');
1893
- const matched = containsHClassReference(normalized) || isHClassPath(filePath, pathOptions);
2051
+ const matched = containsHClassReference(normalized, pathOptions)
2052
+ || containsAgentScopeReference(normalized, pathOptions)
2053
+ || isHClassPath(filePath, pathOptions);
1894
2054
  if (matched) {
1895
2055
  const fileBasename = path.basename(filePath);
1896
2056
  logger.warn(`[H-Class Protection] 🔒 Protected file access blocked: tool=${toolName} path=${filePath} ` +
@@ -1932,7 +2092,7 @@ export function checkHClassWrite(toolName, input, context) {
1932
2092
  */
1933
2093
  export function checkLClassWrite(toolName, input, context) {
1934
2094
  if (['Read', 'Glob', 'Grep'].includes(toolName)) {
1935
- const pathOptions = { cwd: context?.projectPath, root: context?.root };
2095
+ const pathOptions = { cwd: context?.projectPath, root: context?.root, selfAid: context?.selfAid };
1936
2096
  const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
1937
2097
  if (lClassRead && context?.permissionMode === 'readonly') {
1938
2098
  return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', matchedPath: lClassRead };
@@ -1961,7 +2121,7 @@ export function checkLClassWrite(toolName, input, context) {
1961
2121
  }
1962
2122
  else if (toolName === 'Bash') {
1963
2123
  const command = typeof input.command === 'string' ? input.command : '';
1964
- const pathOptions = { cwd: context?.projectPath, root: context?.root };
2124
+ const pathOptions = { cwd: context?.projectPath, root: context?.root, selfAid: context?.selfAid };
1965
2125
  const protectedOperands = analyzeShellProtectedOperands(command, {
1966
2126
  ...pathOptions,
1967
2127
  managedTempDir: context?.managedTempDir,
@@ -1991,7 +2151,7 @@ export function checkLClassWrite(toolName, input, context) {
1991
2151
  // Do not infer mutation targets from arbitrary orchestration-tool strings.
1992
2152
  // Skills commonly pass args: "" and delegation prompts may discuss
1993
2153
  // protected paths; neither is itself a filesystem mutation.
1994
- const pathOptions = { cwd: context?.projectPath, root: context?.root };
2154
+ const pathOptions = { cwd: context?.projectPath, root: context?.root, selfAid: context?.selfAid };
1995
2155
  for (const grantPath of writeGrantPaths) {
1996
2156
  if (grantPath && lClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
1997
2157
  logger.warn(`[L-Class Protection] Write grant covers protected paths: tool=${toolName} path=${grantPath}`);
@@ -2008,7 +2168,7 @@ export function checkLClassWrite(toolName, input, context) {
2008
2168
  return { behavior: 'deny', message: '🔒 无法确定 L-class 写权限的 thread workspace,已拒绝授权' };
2009
2169
  }
2010
2170
  const candidate = path.isAbsolute(subpath) ? subpath : path.resolve(permissionWorkspace, subpath);
2011
- if (lClassGrantIncludesProtectedPath(candidate, { root: context?.root })) {
2171
+ if (lClassGrantIncludesProtectedPath(candidate, { root: context?.root, selfAid: context?.selfAid })) {
2012
2172
  return { behavior: 'deny', message: '🔒 请求的 project_roots 写权限包含 L-class 只读路径,已拒绝授权' };
2013
2173
  }
2014
2174
  }
@@ -2053,6 +2213,7 @@ export function checkBlacklist(toolName, input) {
2053
2213
  function managedTempOptions(context) {
2054
2214
  return {
2055
2215
  cwd: context.projectPath,
2216
+ workspacePath: context.workspacePath ?? context.projectPath,
2056
2217
  // A managed session must never inherit the daemon's unrelated TMPDIR.
2057
2218
  env: context.sessionId
2058
2219
  ? { ...process.env, TMPDIR: context.managedTempDir ?? '' }
@@ -2061,6 +2222,28 @@ function managedTempOptions(context) {
2061
2222
  : process.env,
2062
2223
  };
2063
2224
  }
2225
+ /**
2226
+ * 校验 `cd <dir> && ec ...` 白名单里的目录。
2227
+ *
2228
+ * 白名单放宽的是"前缀拼接"这一层,目录本身的可见范围不能跟着放大:它必须
2229
+ * 落在当前项目(或 workspace)之内。目录是字面量、已经过词法层过滤(不含
2230
+ * 变量展开/通配),这里只需做包含性判断,禁用相对路径逃逸与绝对路径出界。
2231
+ */
2232
+ function validateCdPrefixedCwd(cwd, context) {
2233
+ // 零副作用前缀(pwd/echo)不带目录,无需校验。
2234
+ if (!cwd)
2235
+ return undefined;
2236
+ const projectRoot = resolveProtectedCandidate(context.projectPath);
2237
+ const workspaceRoot = resolveProtectedCandidate(context.workspacePath ?? context.projectPath);
2238
+ // 相对路径按项目根解析——与 shell 里 cd 的语义一致(runner 的起始目录)。
2239
+ const resolved = path.isAbsolute(cwd)
2240
+ ? resolveProtectedCandidate(cwd)
2241
+ : resolveProtectedCandidate(path.resolve(projectRoot, cwd));
2242
+ if (!isSameOrDescendant(resolved, projectRoot) && !isSameOrDescendant(resolved, workspaceRoot)) {
2243
+ return '🔒 `cd <目录> && ec ...` 的目录必须位于当前项目范围内;如需其它目录请直接调用 `ec ...`,不要拼接 `cd`。';
2244
+ }
2245
+ return undefined;
2246
+ }
2064
2247
  function validateBoundedOutputPath(command, context) {
2065
2248
  const outputPath = command.outputPath;
2066
2249
  if (!outputPath)
@@ -2140,14 +2323,18 @@ function prepareBoundedOutputInput(input, command, kind, context) {
2140
2323
  return { behavior: 'deny', input, message: '🔒 安全输出 helper 不可用,命令已拒绝', policyCode: 'bounded_output_helper_unavailable' };
2141
2324
  }
2142
2325
  const payload = Buffer.from(JSON.stringify({
2143
- v: 1,
2326
+ // v3 carries a chain of stdin-only filters; v1 named only the filter's
2327
+ // arguments and v2 carried a single filter argv, so both are rejected
2328
+ // rather than misread.
2329
+ v: 3,
2144
2330
  nonce: randomUUID(),
2145
2331
  kind,
2146
2332
  workspace: path.resolve(context.workspacePath ?? context.projectPath),
2147
- cwd: path.resolve(context.projectPath),
2333
+ cwd: command.cwd ? path.resolve(command.cwd) : path.resolve(context.projectPath),
2148
2334
  argv: command.argv,
2149
2335
  mergeStderr: command.mergeStderr,
2150
- ...(command.headArgs ? { headArgs: command.headArgs } : {}),
2336
+ ...(command.filterChain?.length ? { filterChain: command.filterChain } : {}),
2337
+ ...(command.suffixArgv?.length ? { suffixArgv: command.suffixArgv } : {}),
2151
2338
  ...(command.outputPath
2152
2339
  ? { outputPath: expandManagedTempPath(command.outputPath, managedTempOptions(context)) }
2153
2340
  : {}),
@@ -2161,6 +2348,18 @@ function prepareBoundedOutputInput(input, command, kind, context) {
2161
2348
  }
2162
2349
  /** Shared fail-closed preflight used before runner-specific mode and approval handling. */
2163
2350
  export function evaluateToolPreflight(toolName, input, context) {
2351
+ let applyPatchFileChanges;
2352
+ const managedPathCheck = checkManagedTempToolPaths(toolName, input, managedTempOptions(context));
2353
+ if (managedPathCheck.kind === 'deny') {
2354
+ const currentTmpDir = resolveManagedTempDir(managedTempOptions(context).env);
2355
+ return {
2356
+ behavior: 'deny',
2357
+ input,
2358
+ message: `🔒 临时文件必须位于当前会话的 $TMPDIR 内(当前值:${currentTmpDir ?? '<不可用>'}):${managedPathCheck.reason}`,
2359
+ policyCode: 'temporary_path_outside_tmpdir',
2360
+ matchedPath: managedPathCheck.paths[0],
2361
+ };
2362
+ }
2164
2363
  if (toolName === 'Bash') {
2165
2364
  const command = typeof input.command === 'string' ? input.command : '';
2166
2365
  const explicitCommandArgv = Array.isArray(input.commandArgv)
@@ -2215,6 +2414,41 @@ export function evaluateToolPreflight(toolName, input, context) {
2215
2414
  : {}),
2216
2415
  };
2217
2416
  const parsedCodex = context.sessionId ? parseCodexToolCommand(input, parserOptions) : null;
2417
+ const applyPatch = parseShellApplyPatchInvocation(input, parserOptions, parsedCodex?.ok ? parsedCodex.argv : undefined);
2418
+ if (applyPatch.kind === 'invalid') {
2419
+ return {
2420
+ behavior: 'deny',
2421
+ input,
2422
+ message: '文件修改 patch 无法安全解析:' + applyPatch.reason,
2423
+ policyCode: 'apply_patch_unparseable',
2424
+ errorDetailCode: 'APPLY_PATCH_UNPARSEABLE',
2425
+ };
2426
+ }
2427
+ if (applyPatch.kind === 'file-change') {
2428
+ applyPatchFileChanges = applyPatch.fileChanges;
2429
+ if (context.permissionMode === 'readonly') {
2430
+ return {
2431
+ behavior: 'deny',
2432
+ input,
2433
+ message: '🔒 只读模式:禁止通过 apply_patch 修改文件',
2434
+ policyCode: 'readonly_file_write',
2435
+ errorDetailCode: 'READONLY_FILE_WRITE',
2436
+ };
2437
+ }
2438
+ const escapedPath = applyPatchWorkspaceEscape(applyPatch.fileChanges, context);
2439
+ if (escapedPath && context.sessionId
2440
+ && context.permissionMode !== 'request'
2441
+ && context.permissionMode !== 'fullaccess') {
2442
+ return {
2443
+ behavior: 'deny',
2444
+ input,
2445
+ message: '🔒 apply_patch 目标必须位于当前 workspace 或会话 $TMPDIR 内',
2446
+ policyCode: 'apply_patch_workspace_escape',
2447
+ errorDetailCode: 'APPLY_PATCH_WORKSPACE_ESCAPE',
2448
+ matchedPath: escapedPath,
2449
+ };
2450
+ }
2451
+ }
2218
2452
  if (context.sessionId && parsedCodex && !parsedCodex.ok && parsedCodex.issue === 'untrusted-shell') {
2219
2453
  return {
2220
2454
  behavior: 'deny',
@@ -2310,6 +2544,13 @@ export function evaluateToolPreflight(toolName, input, context) {
2310
2544
  if (ecCommand.kind === 'bounded-output') {
2311
2545
  return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
2312
2546
  }
2547
+ if (ecCommand.kind === 'cd-prefixed') {
2548
+ const cwdError = validateCdPrefixedCwd(ecCommand.cwd, context);
2549
+ if (cwdError) {
2550
+ return { behavior: 'deny', input, message: cwdError, policyCode: 'ec_shell_composite_command' };
2551
+ }
2552
+ return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
2553
+ }
2313
2554
  if (ecCommand.kind === 'composite') {
2314
2555
  if (ecCommand.issue === 'untrusted-shell') {
2315
2556
  return {
@@ -2334,7 +2575,7 @@ export function evaluateToolPreflight(toolName, input, context) {
2334
2575
  }
2335
2576
  if (ecCommand.issue === 'invalid-quote') {
2336
2577
  const message = shellDialect === 'powershell'
2337
- ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
2578
+ ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 也会解释中文弯引号。请按 PowerShell 规则转义,或把消息正文写入当前 $TMPDIR 后使用 --text-from-file'
2338
2579
  : shellDialect === 'cmd'
2339
2580
  ? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
2340
2581
  : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
@@ -2345,6 +2586,12 @@ export function evaluateToolPreflight(toolName, input, context) {
2345
2586
  policyCode: 'ec_shell_invalid_quote',
2346
2587
  };
2347
2588
  }
2589
+ if (ecCommand.issue === 'unsupported-escape') {
2590
+ return { behavior: 'deny', input, message: '🔒 EC 内联文本不支持 PowerShell `n;请使用字面量 \\n。', policyCode: 'ec_shell_unsupported_escape' };
2591
+ }
2592
+ if (ecCommand.issue === 'lossy-cmd-shim') {
2593
+ return { behavior: 'deny', input, message: '🔒 PowerShell 调用 ec.cmd 会截断物理多行参数;请保持原有 ec ... 调用,由 PowerShell 原生 shim 无损传递。', policyCode: 'ec_shell_lossy_cmd_shim' };
2594
+ }
2348
2595
  const message = shellDialect === 'powershell'
2349
2596
  ? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
2350
2597
  : shellDialect === 'cmd'
@@ -2375,11 +2622,16 @@ export function evaluateToolPreflight(toolName, input, context) {
2375
2622
  return { behavior: 'deny', input, message: blacklist.message, policyCode: 'tool_blacklist' };
2376
2623
  }
2377
2624
  const checkedInput = blacklist.updatedInput;
2378
- const hClass = checkHClassWrite(toolName, checkedInput, {
2625
+ const policyInput = applyPatchFileChanges
2626
+ ? { ...checkedInput, fileChanges: applyPatchFileChanges }
2627
+ : checkedInput;
2628
+ const policyToolName = applyPatchFileChanges ? 'FileChange' : toolName;
2629
+ const hClass = checkHClassWrite(policyToolName, policyInput, {
2379
2630
  sessionId: context.sessionId,
2380
2631
  channel: context.channel,
2381
2632
  peerId: context.userId,
2382
2633
  role: context.role,
2634
+ selfAid: context.selfAid,
2383
2635
  projectPath: context.projectPath,
2384
2636
  workspacePath: context.workspacePath,
2385
2637
  root: context.root,
@@ -2391,11 +2643,12 @@ export function evaluateToolPreflight(toolName, input, context) {
2391
2643
  if (hClass.behavior === 'deny') {
2392
2644
  return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection', matchedPath: hClass.matchedPath };
2393
2645
  }
2394
- const lClass = checkLClassWrite(toolName, checkedInput, {
2646
+ const lClass = checkLClassWrite(policyToolName, policyInput, {
2395
2647
  sessionId: context.sessionId,
2396
2648
  channel: context.channel,
2397
2649
  peerId: context.userId,
2398
2650
  role: context.role,
2651
+ selfAid: context.selfAid,
2399
2652
  permissionMode: context.permissionMode,
2400
2653
  projectPath: context.projectPath,
2401
2654
  workspacePath: context.workspacePath,
@@ -2,7 +2,7 @@ import { getRoleDefinition } from '../../config/roles.js';
2
2
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
3
3
  import { ConfigTarget, read } from '../../config/config-manager.js';
4
4
  import { resolvePrincipal } from '../../config/contact-book.js';
5
- import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
5
+ import { loadDaemonConfig } from '../../config-store.js';
6
6
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
7
7
  const PERMISSION_MODE_AUTHORITY = {
8
8
  readonly: 0,
@@ -145,17 +145,16 @@ export function validateRuntimeStringFieldOverride(input) {
145
145
  }
146
146
  /**
147
147
  * Resolve the Agent-level host execution policy for one authenticated principal.
148
- * The Agent flag is subordinate to daemon fullaccess and independent of role permissionMode values.
148
+ * The Agent flag is independent of role permissionMode values.
149
149
  */
150
150
  export function agentFullAccessStatus(agentAid, principalId) {
151
151
  const config = read(ConfigTarget.Agent, { self: agentAid }, { cache: false });
152
152
  const daemon = loadDaemonConfig();
153
153
  const enabled = config?.fullaccess?.enabled === true;
154
- const hostEnabled = isFullAccessEnabled(daemon);
155
154
  const eligible = !!principalId
156
155
  && !!config?.owners?.includes(principalId)
157
156
  && !!daemon.owners?.includes(principalId);
158
- return { enabled, hostEnabled, eligible, effective: enabled && hostEnabled && eligible };
157
+ return { enabled, eligible, effective: enabled && eligible };
159
158
  }
160
159
  /** Create a trusted per-call claim for a direct private message. */
161
160
  export function createAgentPolicyAuthorization(input) {
@@ -198,8 +197,5 @@ export function agentFullAccessWriteError(input) {
198
197
  if (!input.daemonOwner || input.delegated) {
199
198
  return 'Only authenticated ECWeb management may configure Agent fullaccess';
200
199
  }
201
- if (input.enabled && !isFullAccessEnabled()) {
202
- return 'Host fullaccess is disabled';
203
- }
204
200
  return undefined;
205
201
  }
@@ -86,7 +86,7 @@ function eventTaskId(event, aliases) {
86
86
  const useId = typeof event.toolUseId === 'string' ? event.toolUseId.trim() : '';
87
87
  return useId ? aliases.get(useId) ?? `tool:${useId}` : null;
88
88
  }
89
- export function readRunnerTaskHistory(logsDir, evolSessionId) {
89
+ export function readRunnerEventSummary(logsDir, evolSessionId) {
90
90
  let files;
91
91
  try {
92
92
  files = fs.readdirSync(logsDir)
@@ -545,82 +545,6 @@ export function scanNestedAgentChatDirs(sessionsRoot) {
545
545
  }
546
546
  return results.sort((left, right) => left.dirPath < right.dirPath ? -1 : left.dirPath > right.dirPath ? 1 : 0);
547
547
  }
548
- /** Scan the pre-migration data/sessions layout without changing it. */
549
- export function scanLegacyChatDirs(sessionsDir) {
550
- const results = [];
551
- let typeEntries;
552
- try {
553
- typeEntries = fs.readdirSync(sessionsDir, { withFileTypes: true });
554
- }
555
- catch {
556
- return results;
557
- }
558
- for (const typeEntry of typeEntries) {
559
- if (!typeEntry.isDirectory())
560
- continue;
561
- const channelType = typeEntry.name;
562
- const typeDir = path.join(sessionsDir, channelType);
563
- let level2Entries;
564
- try {
565
- level2Entries = fs.readdirSync(typeDir, { withFileTypes: true });
566
- }
567
- catch {
568
- continue;
569
- }
570
- if (channelType === 'aun') {
571
- for (const selfEntry of level2Entries) {
572
- if (!selfEntry.isDirectory())
573
- continue;
574
- const selfDir = path.join(typeDir, selfEntry.name);
575
- let chatEntries;
576
- try {
577
- chatEntries = fs.readdirSync(selfDir, { withFileTypes: true });
578
- }
579
- catch {
580
- continue;
581
- }
582
- for (const chatEntry of chatEntries)
583
- if (chatEntry.isDirectory())
584
- results.push({
585
- channelType, selfAID: decodeSegment(selfEntry.name), channelId: decodeSegment(chatEntry.name), dirPath: path.join(selfDir, chatEntry.name),
586
- });
587
- }
588
- continue;
589
- }
590
- if (channelType === 'wecom') {
591
- for (const entry of level2Entries) {
592
- if (!entry.isDirectory())
593
- continue;
594
- const decoded = decodeSegment(entry.name);
595
- const entryDir = path.join(typeDir, entry.name);
596
- if (decoded.startsWith('wecom#') && decoded.split('#').length === 3) {
597
- let chatEntries;
598
- try {
599
- chatEntries = fs.readdirSync(entryDir, { withFileTypes: true });
600
- }
601
- catch {
602
- continue;
603
- }
604
- for (const chatEntry of chatEntries)
605
- if (chatEntry.isDirectory())
606
- results.push({
607
- channelType, selfAID: '', channelKey: decoded, channelId: decodeSegment(chatEntry.name), dirPath: path.join(entryDir, chatEntry.name),
608
- });
609
- }
610
- else {
611
- results.push({ channelType, selfAID: '', channelId: decoded, dirPath: entryDir });
612
- }
613
- }
614
- continue;
615
- }
616
- for (const chatEntry of level2Entries)
617
- if (chatEntry.isDirectory())
618
- results.push({
619
- channelType, selfAID: '', channelId: decodeSegment(chatEntry.name), dirPath: path.join(typeDir, chatEntry.name),
620
- });
621
- }
622
- return results.sort((left, right) => left.dirPath < right.dirPath ? -1 : left.dirPath > right.dirPath ? 1 : 0);
623
- }
624
548
  export function scanMetaFiles(chatDir) {
625
549
  try {
626
550
  const entries = fs.readdirSync(chatDir);