evolcore 0.0.3 → 0.0.4

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 (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -55,10 +55,10 @@ function structuredApprovalAuditSummary(input) {
55
55
  .slice(0, 16);
56
56
  return fields.length > 0 ? `input_fields=${fields.join(',')}` : 'input_withheld';
57
57
  }
58
- function requiredApproverRole(policy, routeKind) {
58
+ function requiredApproverRole(policy) {
59
59
  if (policy === 'requester')
60
60
  return 'requester';
61
- if (policy === 'agent_owner' || routeKind === 'handoff')
61
+ if (policy === 'agent_owner')
62
62
  return 'owner';
63
63
  return 'manager';
64
64
  }
@@ -148,13 +148,20 @@ export function planApprovalRoute(challenge, context) {
148
148
  approverPrincipalId: actor.principalId,
149
149
  };
150
150
  }
151
- if (owners.length === 0) {
152
- return { kind: 'unavailable', reason: 'no_agent_owner_configured' };
151
+ const handoffCandidates = challenge.approverPolicy === 'agent_manager'
152
+ ? Array.from(new Set([...admins, ...owners]))
153
+ : owners;
154
+ if (handoffCandidates.length === 0) {
155
+ return {
156
+ kind: 'unavailable',
157
+ reason: challenge.approverPolicy === 'agent_manager'
158
+ ? 'no_agent_manager_configured'
159
+ : 'no_agent_owner_configured',
160
+ };
153
161
  }
154
- // Both management policies hand off to the first configured AUN owner.
155
- // Admin handoff approval and implicit approver selection are intentionally
156
- // out of scope; a future caller may explicitly select from the owner list.
157
- const approverId = owners[0];
162
+ // Manager handoffs prefer the configured admin pool for routine approvals;
163
+ // owner-only policies remain restricted to owners. Only one card is sent.
164
+ const approverId = handoffCandidates[0];
158
165
  if (!approverId) {
159
166
  return { kind: 'unavailable', reason: 'no_aun_owner_available' };
160
167
  }
@@ -505,7 +512,7 @@ export class PermissionGateway {
505
512
  grantScope,
506
513
  approverPolicy: challenge.approverPolicy,
507
514
  approvalRouteKind: route.kind,
508
- requiredApproverRole: requiredApproverRole(challenge.approverPolicy, route.kind),
515
+ requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
509
516
  approverOperatorId: route.approverOperatorId,
510
517
  approverChannelKey: route.approverChannelKey,
511
518
  approverPrincipalId: route.approverPrincipalId,
@@ -763,7 +770,7 @@ export class PermissionGateway {
763
770
  grantScope,
764
771
  approverPolicy: challenge.approverPolicy,
765
772
  approvalRouteKind: route.kind,
766
- requiredApproverRole: requiredApproverRole(challenge.approverPolicy, route.kind),
773
+ requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
767
774
  approverOperatorId: route.approverOperatorId,
768
775
  approverChannelKey: route.approverChannelKey,
769
776
  approverPrincipalId: route.approverPrincipalId,
@@ -1,5 +1,4 @@
1
- const DEFAULT_HEAD_LINES = 10;
2
- const MAX_HEAD_LINES = 10_000;
1
+ const HEAD_EXECUTABLES = new Set(['head', '/bin/head', '/usr/bin/head']);
3
2
  const SHELL_UNQUOTED_META = new Set([
4
3
  ';', '&', '|', '`', '<', '>', '$', '(', ')', '*', '?', '[', ']', '{', '}', '#',
5
4
  ]);
@@ -11,16 +10,36 @@ function trimShellSpacing(value) {
11
10
  function isShellSpacing(value) {
12
11
  return value === ' ' || value === '\t';
13
12
  }
13
+ function isLiteralArgBoundary(value) {
14
+ return value === undefined || isShellSpacing(value);
15
+ }
16
+ function isShellParameterExpansionStart(value) {
17
+ return value === '(' || value === '{' || !!value && /[A-Za-z0-9_@*#?$!\-]/.test(value);
18
+ }
19
+ function doubleQuotedIssueAt(input, index) {
20
+ const char = input[index];
21
+ if (char === '`' || (char === '$' && isShellParameterExpansionStart(input[index + 1]))) {
22
+ return 'unsafe-expansion';
23
+ }
24
+ if (char !== '\\')
25
+ return null;
26
+ const next = input[index + 1];
27
+ if (next === undefined)
28
+ return 'invalid-quote';
29
+ return next === '\r' || next === '\n' ? 'structural-newline' : null;
30
+ }
14
31
  /**
15
32
  * Parse the deliberately small shell subset accepted by the privileged EC
16
33
  * command path. The returned argv must describe one literal invocation: no
17
34
  * expansion, glob, pipeline, command substitution, redirection, or comment.
18
35
  * Anything outside this subset is never granted the privileged EC path.
19
36
  */
20
- export function parseLiteralShellArgv(command) {
37
+ export function parseLiteralShellCommand(command) {
21
38
  const input = trimShellSpacing(command);
22
- if (!input || input.includes('\0'))
23
- return null;
39
+ if (!input)
40
+ return { ok: false, issue: 'empty-command' };
41
+ if (input.includes('\0'))
42
+ return { ok: false, issue: 'invalid-control-char' };
24
43
  const argv = [];
25
44
  let token = '';
26
45
  let tokenStarted = false;
@@ -35,26 +54,28 @@ export function parseLiteralShellArgv(command) {
35
54
  for (let index = 0; index < input.length; index++) {
36
55
  const char = input[index];
37
56
  if (quote === 'single') {
38
- if (char === "'")
57
+ if (char === "'") {
58
+ if (!isLiteralArgBoundary(input[index + 1]))
59
+ return { ok: false, issue: 'invalid-quote' };
39
60
  quote = null;
61
+ }
40
62
  else
41
63
  token += char;
42
64
  continue;
43
65
  }
44
66
  if (quote === 'double') {
45
67
  if (char === '"') {
68
+ if (!isLiteralArgBoundary(input[index + 1]))
69
+ return { ok: false, issue: 'invalid-quote' };
46
70
  quote = null;
47
71
  continue;
48
72
  }
49
- if (char === '$' || char === '`')
50
- return null;
73
+ const issue = doubleQuotedIssueAt(input, index);
74
+ if (issue)
75
+ return { ok: false, issue };
51
76
  if (char === '\\') {
52
77
  const next = input[++index];
53
- if (next === undefined || next === '\r' || next === '\n')
54
- return null;
55
- if (next === '$' || next === '`')
56
- return null;
57
- token += ['"', '\\'].includes(next) ? next : `\\${next}`;
78
+ token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
58
79
  continue;
59
80
  }
60
81
  token += char;
@@ -70,8 +91,11 @@ export function parseLiteralShellArgv(command) {
70
91
  // includes NBSP, line/paragraph separators, VT and FF, which the shell
71
92
  // keeps inside the executable name; accepting those would misclassify a
72
93
  // different executable as `ec`.
73
- if (char === '\r' || char === '\n' || /\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f')
74
- return null;
94
+ if (char === '\r' || char === '\n')
95
+ return { ok: false, issue: 'structural-newline' };
96
+ if (/\s/u.test(char) || char.charCodeAt(0) < 0x20 || char === '\x7f') {
97
+ return { ok: false, issue: 'invalid-control-char' };
98
+ }
75
99
  if (char === "'") {
76
100
  tokenStarted = true;
77
101
  quote = 'single';
@@ -84,8 +108,10 @@ export function parseLiteralShellArgv(command) {
84
108
  }
85
109
  if (char === '\\') {
86
110
  const next = input[++index];
87
- if (next === undefined || next === '\r' || next === '\n')
88
- return null;
111
+ if (next === undefined)
112
+ return { ok: false, issue: 'invalid-quote' };
113
+ if (next === '\r' || next === '\n')
114
+ return { ok: false, issue: 'structural-newline' };
89
115
  tokenStarted = true;
90
116
  token += next;
91
117
  continue;
@@ -94,14 +120,20 @@ export function parseLiteralShellArgv(command) {
94
120
  // peer keys such as `aun#alice` are ordinary literal arguments.
95
121
  if ((SHELL_UNQUOTED_META.has(char) && (char !== '#' || !tokenStarted))
96
122
  || (char === '~' && !tokenStarted))
97
- return null;
123
+ return { ok: false, issue: 'shell-composition' };
98
124
  tokenStarted = true;
99
125
  token += char;
100
126
  }
101
127
  if (quote)
102
- return null;
128
+ return { ok: false, issue: 'invalid-quote' };
103
129
  finishToken();
104
- return argv.length > 0 ? argv : null;
130
+ return argv.length > 0
131
+ ? { ok: true, argv }
132
+ : { ok: false, issue: 'empty-command' };
133
+ }
134
+ export function parseLiteralShellArgv(command) {
135
+ const result = parseLiteralShellCommand(command);
136
+ return result.ok ? result.argv : null;
105
137
  }
106
138
  function scanBoundedOutputOperators(command) {
107
139
  const operators = [];
@@ -118,12 +150,10 @@ function scanBoundedOutputOperators(command) {
118
150
  quote = null;
119
151
  continue;
120
152
  }
121
- if (char === '$' || char === '`')
153
+ if (doubleQuotedIssueAt(command, index))
122
154
  return null;
123
155
  if (char === '\\') {
124
- const next = command[++index];
125
- if (next === undefined || next === '\r' || next === '\n' || next === '$' || next === '`')
126
- return null;
156
+ index++;
127
157
  }
128
158
  continue;
129
159
  }
@@ -171,21 +201,46 @@ function scanBoundedOutputOperators(command) {
171
201
  }
172
202
  return quote ? null : operators;
173
203
  }
174
- function parseHeadLines(segment) {
204
+ /**
205
+ * Accept `head` as a generic, trusted stdout filter without mirroring its
206
+ * option grammar here. Non-option operands are the security boundary: they
207
+ * could make `head` read a file instead of the EC process' stdout.
208
+ *
209
+ * GNU/POSIX head only uses a separate value for -n/-c (and their long forms).
210
+ * All other dash-prefixed arguments are safe to leave to head itself; unknown
211
+ * or malformed options simply make head fail.
212
+ */
213
+ function parseStdinOnlyHeadArgs(segment) {
175
214
  const argv = parseLiteralShellArgv(segment);
176
- if (!argv || (argv[0] !== 'head' && argv[0] !== '/usr/bin/head'))
177
- return null;
178
- let value;
179
- if (argv.length === 1)
180
- value = String(DEFAULT_HEAD_LINES);
181
- else if (argv.length === 2 && /^-\d+$/.test(argv[1]))
182
- value = argv[1].slice(1);
183
- else if (argv.length === 3 && argv[1] === '-n' && /^\d+$/.test(argv[2]))
184
- value = argv[2];
185
- else
215
+ if (!argv || !HEAD_EXECUTABLES.has(argv[0]))
186
216
  return null;
187
- const lines = Number(value);
188
- return Number.isSafeInteger(lines) && lines >= 1 && lines <= MAX_HEAD_LINES ? lines : null;
217
+ const args = argv.slice(1);
218
+ let optionsEnded = false;
219
+ let expectsOptionValue = false;
220
+ for (const arg of args) {
221
+ if (expectsOptionValue) {
222
+ expectsOptionValue = false;
223
+ continue;
224
+ }
225
+ if (optionsEnded) {
226
+ if (arg !== '-')
227
+ return null;
228
+ continue;
229
+ }
230
+ if (arg === '--') {
231
+ optionsEnded = true;
232
+ continue;
233
+ }
234
+ if (arg === '-')
235
+ continue;
236
+ if (!arg.startsWith('-'))
237
+ return null;
238
+ if (arg === '-n' || arg === '-c' || arg === '--lines' || arg === '--bytes') {
239
+ expectsOptionValue = true;
240
+ continue;
241
+ }
242
+ }
243
+ return args;
189
244
  }
190
245
  /**
191
246
  * Parse the only shell wrapper allowed around a privileged literal process:
@@ -234,13 +289,13 @@ export function parseBoundedOutputShellCommand(command) {
234
289
  else {
235
290
  redirect = first;
236
291
  }
237
- let headLines;
292
+ let headArgs;
238
293
  if (pipe) {
239
294
  const headEnd = redirect?.start ?? input.length;
240
- const parsedLines = parseHeadLines(trimShellSpacing(input.slice(pipe.end, headEnd)));
241
- if (parsedLines === null)
295
+ const parsedArgs = parseStdinOnlyHeadArgs(trimShellSpacing(input.slice(pipe.end, headEnd)));
296
+ if (parsedArgs === null)
242
297
  return null;
243
- headLines = parsedLines;
298
+ headArgs = parsedArgs;
244
299
  }
245
300
  let outputPath;
246
301
  if (redirect) {
@@ -249,7 +304,7 @@ export function parseBoundedOutputShellCommand(command) {
249
304
  return null;
250
305
  outputPath = outputArgv[0];
251
306
  }
252
- return { argv, mergeStderr, ...(headLines ? { headLines } : {}), ...(outputPath ? { outputPath } : {}) };
307
+ return { argv, mergeStderr, ...(headArgs ? { headArgs } : {}), ...(outputPath ? { outputPath } : {}) };
253
308
  }
254
309
  /**
255
310
  * Classify only the shell boundary around an EC process invocation. Runner
@@ -257,9 +312,9 @@ export function parseBoundedOutputShellCommand(command) {
257
312
  * invocation may leave the sandbox, while shell composition may not.
258
313
  */
259
314
  export function classifyEvolcoreShellCommand(command) {
260
- const literalArgv = parseLiteralShellArgv(command);
261
- if (literalArgv) {
262
- return literalArgv[0] === 'ec'
315
+ const literal = parseLiteralShellCommand(command);
316
+ if (literal.ok) {
317
+ return literal.argv[0] === 'ec'
263
318
  ? { kind: 'literal' }
264
319
  : { kind: 'none' };
265
320
  }
@@ -268,7 +323,7 @@ export function classifyEvolcoreShellCommand(command) {
268
323
  return { kind: 'bounded-output', command: boundedOutput };
269
324
  }
270
325
  return EVOLCORE_COMMAND_POSITION_RE.test(command)
271
- ? { kind: 'composite' }
326
+ ? { kind: 'composite', issue: literal.issue }
272
327
  : { kind: 'none' };
273
328
  }
274
329
  function isDeterministicFilePath(value) {
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
- import { containsHClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
6
+ import { containsHClassReference, containsLClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
7
7
  import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
8
8
  // 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
9
9
  // 这些是系统级破坏操作,任何情况下都不应该允许
@@ -18,6 +18,15 @@ const READONLY_SHELL_EXECUTABLES = new Set([
18
18
  'ls', '/bin/ls', '/usr/bin/ls',
19
19
  'cat', '/bin/cat', '/usr/bin/cat',
20
20
  ]);
21
+ const L_CLASS_READONLY_SHELL_EXECUTABLES = new Set([
22
+ ...READONLY_SHELL_EXECUTABLES,
23
+ 'grep', '/bin/grep', '/usr/bin/grep',
24
+ 'rg', '/bin/rg', '/usr/bin/rg',
25
+ 'find', '/bin/find', '/usr/bin/find',
26
+ 'sed', '/bin/sed', '/usr/bin/sed',
27
+ ]);
28
+ const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
29
+ const RG_PROCESS_SPAWNING_OPTION_RE = /^--pre(?:=|$)|^--pre-glob(?:=|$)/;
21
30
  // 危险操作(需要用户审批才能执行)
22
31
  // 这些操作有合理使用场景,但需要用户明确授权
23
32
  const DANGEROUS_PATTERNS = [
@@ -552,28 +561,50 @@ function collectFileChangePaths(value, output) {
552
561
  }
553
562
  function shellCommandReferencesLClass(command, options) {
554
563
  const normalized = command.replace(/\\/g, '/');
555
- const root = path.resolve(options.root ?? resolveRoot()).replace(/\\/g, '/');
556
- const protectedRoot = `${root.replace(/\/+$/, '')}/data/triggers`;
557
- if (normalized.includes(protectedRoot))
564
+ if (containsLClassReference(normalized))
558
565
  return true;
559
- const references = normalized.match(/(?:^|[\s"'=<>;&|()])((?:\.\.\/|\.\/)*data\/triggers(?:\/[^\s"';&|<>)]*)?)/g) ?? [];
560
- return references.some(reference => {
561
- const candidate = reference.replace(/^[\s"'=<>;&|()]+/, '');
562
- return isLClassPath(candidate, options);
563
- });
566
+ const argv = parseLiteralShellArgv(command);
567
+ return argv?.slice(1).some(argument => {
568
+ if (argument === '-' || argument === '--' || argument.startsWith('-'))
569
+ return false;
570
+ return isLClassPath(argument, options);
571
+ }) ?? false;
564
572
  }
565
- function isLClassMutatingShellCommand(command, options) {
573
+ function isVerifiedLClassReadOnlyShellCommand(command) {
574
+ const argv = parseLiteralShellArgv(command);
575
+ if (!argv || !L_CLASS_READONLY_SHELL_EXECUTABLES.has(argv[0]))
576
+ return false;
577
+ switch (argv[0].replace(/^.*\//, '')) {
578
+ case 'grep':
579
+ // grep never mutates its operands. Shell composition is rejected by the
580
+ // literal parser before this point.
581
+ return true;
582
+ case 'rg':
583
+ // ripgrep's --pre hooks may execute an arbitrary process.
584
+ return !argv.slice(1).some(argument => RG_PROCESS_SPAWNING_OPTION_RE.test(argument));
585
+ case 'find':
586
+ // Keep only the predicates/actions which cannot create, mutate, or
587
+ // execute files. Other find options remain fail-closed.
588
+ return !argv.slice(1).some(argument => FIND_MUTATING_PRIMARY_RE.test(argument));
589
+ case 'sed': {
590
+ // sed can write via -i or a `w` command. Permit only the bounded
591
+ // inspection form used for line-range reads: sed -n 'START[,END]p'.
592
+ const quiet = argv[1] === '-n' || argv[1] === '--quiet' || argv[1] === '--silent';
593
+ const program = quiet ? argv[2] : undefined;
594
+ return !!program
595
+ && /^(?:\d+(?:,\d+)?|\$)?p$/.test(program)
596
+ // GNU sed accepts options after the script as well. Reject every
597
+ // later option so -i/-e/-f cannot be smuggled into this read form.
598
+ && !argv.slice(3).some(argument => argument.startsWith('-'));
599
+ }
600
+ default:
601
+ return true;
602
+ }
603
+ }
604
+ function isUnverifiedLClassShellCommand(command, options) {
566
605
  if (!shellCommandReferencesLClass(command, options))
567
606
  return false;
568
- const mutationPatterns = [
569
- /(?:^|[;&|]\s*)\s*(?:rm|unlink|rmdir|mv|rename|truncate|touch|chmod|chown|chgrp|mkdir)\b/i,
570
- /(?:^|[;&|]\s*)\s*(?:sed\s+[^;&|]*\s-i\b|perl\s+[^;&|]*\s-pi\b)/i,
571
- /(?:^|[;&|]\s*)\s*tee\b/i,
572
- /\bfind\b[^;&|]*(?:-delete|-exec\s+(?:rm|mv|chmod|chown)\b)/i,
573
- /\bgit\s+(?:checkout|restore|clean|reset)\b/i,
574
- /(?:^|[^<])>{1,2}\s*[^;&|\r\n]*data[\\/]triggers(?:[\\/]|$)/i,
575
- ];
576
- return mutationPatterns.some(pattern => pattern.test(command));
607
+ return !isVerifiedLClassReadOnlyShellCommand(command);
577
608
  }
578
609
  function requestsFilesystemRoot(value) {
579
610
  if (Array.isArray(value))
@@ -747,12 +778,12 @@ export function checkLClassWrite(toolName, input, context) {
747
778
  else if (toolName === 'Bash') {
748
779
  const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
749
780
  const pathOptions = { cwd: context?.projectPath, root: context?.root };
750
- if (isLClassMutatingShellCommand(command, pathOptions)) {
751
- logger.warn(`[L-Class Protection] Protected path mutation requested by shell command: tool=${toolName} ` +
781
+ if (isUnverifiedLClassShellCommand(command, pathOptions)) {
782
+ logger.warn(`[L-Class Protection] Unverified L-class shell command denied: tool=${toolName} ` +
752
783
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
753
784
  return {
754
785
  behavior: 'deny',
755
- message: '🔒 Shell 命令试图修改 L-class 只读路径;读取允许,修改请使用 ec trigger',
786
+ message: '🔒 L-class 路径仅允许单个可验证的只读命令。请拆分为单条 ls/cat/grep/rg/find 或 sed -n,或使用 Read;禁止管道、重定向、变量展开和命令拼接。修改必须通过受控的 EvolCore 命令',
756
787
  };
757
788
  }
758
789
  collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
@@ -791,7 +822,7 @@ export function checkLClassWrite(toolName, input, context) {
791
822
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
792
823
  return {
793
824
  behavior: 'deny',
794
- message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用 ec trigger 命令修改`,
825
+ message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
795
826
  };
796
827
  }
797
828
  return { behavior: 'allow' };
@@ -894,7 +925,7 @@ function prepareBoundedOutputInput(input, command, kind, context) {
894
925
  cwd: path.resolve(context.projectPath),
895
926
  argv: command.argv,
896
927
  mergeStderr: command.mergeStderr,
897
- ...(command.headLines ? { headLines: command.headLines } : {}),
928
+ ...(command.headArgs ? { headArgs: command.headArgs } : {}),
898
929
  ...(command.outputPath ? { outputPath: command.outputPath } : {}),
899
930
  }), 'utf8').toString('base64url');
900
931
  const rewrittenCommand = `${quotePosixShellArg(process.execPath)} ${quotePosixShellArg(helperPath)} ${quotePosixShellArg(payload)}`;
@@ -916,6 +947,20 @@ export function evaluateToolPreflight(toolName, input, context) {
916
947
  return prepareBoundedOutputInput(input, ecCommand.command, 'ec', context);
917
948
  }
918
949
  if (ecCommand.kind === 'composite') {
950
+ if (ecCommand.issue === 'unsafe-expansion') {
951
+ return {
952
+ behavior: 'deny',
953
+ input,
954
+ message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
955
+ };
956
+ }
957
+ if (ecCommand.issue === 'invalid-quote') {
958
+ return {
959
+ behavior: 'deny',
960
+ input,
961
+ message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
962
+ };
963
+ }
919
964
  return {
920
965
  behavior: 'deny',
921
966
  input,
@@ -6,7 +6,7 @@ const H_CLASS_RELATIVE_PATTERNS = [
6
6
  /^agents\/defaults\.json$/,
7
7
  /^agents\/[^/]+\/config\.json$/,
8
8
  /^agents\/[^/]+\/contact\.json$/,
9
- /^agents\/[^/]+\/roles(?:\/|$)/,
9
+ /^agents\/[^/]+\/contact-operations\.jsonl$/,
10
10
  /^agents\/[^/]+\/relations\/[^/]+\/config\.json$/,
11
11
  /^backups\/config(?:\/|$)/,
12
12
  /^\.snapshots(?:\/|$)/,
@@ -29,17 +29,18 @@ const H_CLASS_RELATIVE_PATTERNS = [
29
29
  ];
30
30
  const H_CLASS_REFERENCE_PATTERNS = [
31
31
  /(?:^|[^A-Za-z0-9_.-])daemon\.json(?=$|[^A-Za-z0-9_.-])/,
32
- /(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
33
- /(?:^|[^A-Za-z0-9_.-])agents[\\/][^/\\\s"']+[\\/]roles(?:[\\/]|$|[\s"';&|<>)},])/,
32
+ /(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|contact-operations\.jsonl|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
34
33
  /(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys))(?:[\\/]|$|[\s"';&|<>)},])/,
35
34
  /(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|instance[\\/]control\.token|outbox|handoff))(?:[\\/]|$|[\s"';&|<>)},])/,
36
35
  /(?:^|[/\\\s"'=<>])(?:[^/\\\s"']+\.json_|[^/\\\s"']+\.json\.migrated|defaults_\d+\.json)(?=$|[\s"';&|<>)},])/,
37
36
  ];
38
37
  const L_CLASS_RELATIVE_PATTERNS = [
39
38
  /^data\/triggers(?:\/|$)/,
39
+ /^agents\/[^/]+\/roles(?:\/|$)/,
40
40
  ];
41
41
  const L_CLASS_REFERENCE_PATTERNS = [
42
42
  /(?:^|[/\\\s"'=<>])data[\\/]triggers(?:[\\/]|$|[\s"';&|<>)},])/,
43
+ /(?:^|[^A-Za-z0-9_.-])agents[\\/][^/\\\s"']+[\\/]roles(?:[\\/]|$|[\s"';&|<>)},])/,
43
44
  ];
44
45
  function normalizeForComparison(value) {
45
46
  const resolved = path.resolve(value);
@@ -127,8 +128,7 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
127
128
  path.join('agents', 'defaults.json'),
128
129
  path.join('agents', '*', 'config.json'),
129
130
  path.join('agents', '*', 'contact.json'),
130
- path.join('agents', '*', 'roles'),
131
- path.join('agents', '*', 'roles', '**'),
131
+ path.join('agents', '*', 'contact-operations.jsonl'),
132
132
  path.join('agents', '*', 'relations', '*', 'config.json'),
133
133
  path.join('backups', 'config'),
134
134
  path.join('backups', 'config', '**'),
@@ -169,6 +169,7 @@ export function getLClassWritePatterns(root = resolveRoot()) {
169
169
  return [
170
170
  path.join(absoluteRoot, 'data', 'triggers'),
171
171
  path.join(absoluteRoot, 'data', 'triggers', '**'),
172
+ path.join(absoluteRoot, 'agents', '*', 'roles', '**'),
172
173
  ];
173
174
  }
174
175
  const CLAUDE_PROTECTED_TARGET_LIMIT = 8192;
@@ -213,10 +214,9 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
213
214
  if (!entry.isDirectory())
214
215
  continue;
215
216
  const agentRoot = path.join(agentsDir, entry.name);
216
- for (const fileName of ['config.json', 'config.json_', 'contact.json', 'contact.json_']) {
217
+ for (const fileName of ['config.json', 'config.json_', 'contact.json', 'contact.json_', 'contact-operations.jsonl']) {
217
218
  addExistingTarget(targets, path.join(agentRoot, fileName));
218
219
  }
219
- addExistingTarget(targets, path.join(agentRoot, 'roles'));
220
220
  const relationsDir = path.join(agentRoot, 'relations');
221
221
  try {
222
222
  for (const relation of fs.readdirSync(relationsDir, { withFileTypes: true })) {
@@ -247,7 +247,17 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
247
247
  }
248
248
  export function getExistingLClassReadOnlyTargets(root = resolveRoot()) {
249
249
  const targets = new Map();
250
- addExistingTarget(targets, path.join(resolveThroughExistingAncestor(root), 'data', 'triggers'));
250
+ const absoluteRoot = resolveThroughExistingAncestor(root);
251
+ addExistingTarget(targets, path.join(absoluteRoot, 'data', 'triggers'));
252
+ const agentsDir = path.join(absoluteRoot, 'agents');
253
+ try {
254
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
255
+ if (!entry.isDirectory())
256
+ continue;
257
+ addExistingTarget(targets, path.join(agentsDir, entry.name, 'roles'));
258
+ }
259
+ }
260
+ catch { }
251
261
  return [...targets.values()];
252
262
  }
253
263
  export function buildClaudeProtectedFilesystem(root = resolveRoot()) {
@@ -309,7 +319,6 @@ export function lClassGrantIncludesProtectedPath(value, options = {}) {
309
319
  if (!value.trim())
310
320
  return false;
311
321
  const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
312
- const protectedRoot = path.join(root, 'data', 'triggers');
313
322
  if (isLClassPath(value, { cwd: options.cwd, root }))
314
323
  return true;
315
324
  const normalized = value.replace(/\\/g, '/');
@@ -318,8 +327,24 @@ export function lClassGrantIncludesProtectedPath(value, options = {}) {
318
327
  ? value
319
328
  : normalized.slice(0, globIndex).replace(/\/+$/, '') || '.';
320
329
  const candidate = resolveProtectedCandidate(literalGrantRoot, options.cwd);
321
- return isSameOrDescendant(candidate, protectedRoot)
322
- || isSameOrDescendant(protectedRoot, candidate);
330
+ if (isSameOrDescendant(root, candidate))
331
+ return true;
332
+ if (!isSameOrDescendant(candidate, root))
333
+ return false;
334
+ const relative = path.relative(root, candidate);
335
+ const segments = relative.split(path.sep).filter(Boolean);
336
+ if (segments[0] === 'data') {
337
+ if (segments.length === 1)
338
+ return true;
339
+ return segments[1] === 'triggers';
340
+ }
341
+ if (segments[0] === 'agents') {
342
+ // The agents root and each individual Agent root contain a roles/ subtree.
343
+ if (segments.length <= 2)
344
+ return true;
345
+ return segments[2] === 'roles';
346
+ }
347
+ return false;
323
348
  }
324
349
  export function workspaceContainsHClassPaths(projectPath, root = resolveRoot()) {
325
350
  const project = resolveThroughExistingAncestor(projectPath);
@@ -9,6 +9,22 @@ import { isSystemControlChannel } from '../system-channels.js';
9
9
  import path from 'path';
10
10
  import fs from 'fs';
11
11
  import os from 'os';
12
+ function newSessionStableMetadata(chatType, ...sources) {
13
+ const keys = chatType === 'group'
14
+ ? ['channelKey', 'groupId', 'groupName', 'dispatchMode']
15
+ : ['channelKey', 'peerId', 'peerName', 'peerType'];
16
+ const metadata = {};
17
+ for (const source of sources) {
18
+ if (!source)
19
+ continue;
20
+ for (const key of keys) {
21
+ const value = source[key];
22
+ if (value !== undefined && value !== null && value !== '')
23
+ metadata[key] = value;
24
+ }
25
+ }
26
+ return metadata;
27
+ }
12
28
  /** 判定用户是否为指定渠道的 admin */
13
29
  /** 判定用户是否为指定渠道的 member */
14
30
  export class SessionManager {
@@ -1098,18 +1114,20 @@ export class SessionManager {
1098
1114
  }
1099
1115
  }
1100
1116
  }
1101
- async createNewSession(channel, channelId, projectPath, name, baseagent) {
1117
+ async createNewSession(channel, channelId, projectPath, name, baseagent, identityMetadata) {
1102
1118
  const inheritedChatType = this.getActiveChatType(channel, channelId);
1103
1119
  // Derive selfAID and channelType from existing sessions
1104
1120
  const existingDir = this.findExistingChatDir(channel, channelId);
1105
1121
  const identity = this.deriveChannelIdentity(channel, channelId);
1106
1122
  let channelType = identity.channelType;
1107
1123
  let selfAID = identity.selfAID;
1124
+ let activeMetadata;
1108
1125
  if (existingDir) {
1109
1126
  const active = readJsonFile(path.join(existingDir, 'active.json'));
1110
1127
  if (active) {
1111
1128
  channelType = active.channelType || channel;
1112
1129
  selfAID = active.selfAID || '';
1130
+ activeMetadata = active.metadata;
1113
1131
  // 兼容旧字段名
1114
1132
  const activeBaseagent = active.baseagent || active.agentType;
1115
1133
  if (!baseagent && activeBaseagent)
@@ -1130,7 +1148,7 @@ export class SessionManager {
1130
1148
  baseagent,
1131
1149
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1132
1150
  chatType: inheritedChatType,
1133
- metadata: {},
1151
+ metadata: newSessionStableMetadata(inheritedChatType, activeMetadata, identityMetadata),
1134
1152
  name: name || '默认会话',
1135
1153
  createdAt: Date.now(),
1136
1154
  updatedAt: Date.now(),
@@ -1258,7 +1276,7 @@ export class SessionManager {
1258
1276
  * 为超时后的独立话题创建新会话。保留聊天路由和稳定 metadata,
1259
1277
  * 但不继承任何底层 agent session 或临时任务状态。
1260
1278
  */
1261
- async createRenewedSession(sourceSession) {
1279
+ async createRenewedSession(sourceSession, context) {
1262
1280
  const metadata = { ...(sourceSession.metadata || {}) };
1263
1281
  delete metadata.agentSessions;
1264
1282
  delete metadata.resumeAt;
@@ -1289,7 +1307,11 @@ export class SessionManager {
1289
1307
  projectPath: session.projectPath,
1290
1308
  name: session.name,
1291
1309
  chatType: session.chatType,
1310
+ cause: 'renew',
1311
+ sourceSessionId: sourceSession.id,
1312
+ renewSource: context?.source,
1292
1313
  timestamp: Date.now(),
1314
+ causation: context?.causation,
1293
1315
  });
1294
1316
  return session;
1295
1317
  }
@@ -3,6 +3,8 @@ import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
3
3
  export function sessionToFile(session) {
4
4
  const metadata = {};
5
5
  if (session.metadata) {
6
+ if (session.metadata.channelKey)
7
+ metadata.channelKey = session.metadata.channelKey;
6
8
  if (session.metadata.peerId)
7
9
  metadata.peerId = session.metadata.peerId;
8
10
  if (session.metadata.peerName)