evolcore 0.0.10 → 0.0.11

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/gateway-config.js +26 -10
  20. package/dist/core/agent-reload-coordinator.js +53 -0
  21. package/dist/core/auth/operation-authorizer.js +32 -147
  22. package/dist/core/auth/operation-catalog.js +80 -0
  23. package/dist/core/bootstrap-messages.js +50 -0
  24. package/dist/core/bootstrap-service.js +85 -10
  25. package/dist/core/channel-loader.js +23 -6
  26. package/dist/core/command/agent-control.js +14 -11
  27. package/dist/core/command/menu-handler.js +67 -76
  28. package/dist/core/command/slash-handler.js +4 -4
  29. package/dist/core/evolagent-registry.js +125 -35
  30. package/dist/core/evolagent.js +8 -3
  31. package/dist/core/inference/text-inference.js +38 -4
  32. package/dist/core/message/message-bridge.js +1 -1
  33. package/dist/core/message/message-log.js +22 -0
  34. package/dist/core/message/message-queue.js +19 -4
  35. package/dist/core/model/model-catalog.js +143 -24
  36. package/dist/core/model/model-diagnostics.js +28 -10
  37. package/dist/core/permission/index.js +1 -0
  38. package/dist/core/permission/readonly-shell-query.js +532 -0
  39. package/dist/core/permission/shell-environment.js +46 -0
  40. package/dist/core/permission/tool-policy.js +231 -93
  41. package/dist/core/protected-paths.js +10 -7
  42. package/dist/core/runner-reload-transaction.js +57 -0
  43. package/dist/index.js +262 -84
  44. package/dist/ipc.js +29 -11
  45. package/dist/utils/aid-bind.js +3 -8
  46. package/dist/utils/log-writer.js +6 -10
  47. package/dist/utils/logger.js +5 -5
  48. package/kits/docs/evolcore/msg.md +13 -0
  49. package/kits/rules/01-overview.md +9 -0
  50. package/kits/schemas/agent-config.schema.3.json +1 -1
  51. package/kits/schemas/agent-config.schema.4.json +1 -1
  52. package/kits/schemas/relation-config.schema.2.json +1 -1
  53. package/kits/schemas/role-config.schema.1.json +1 -1
  54. package/kits/templates/roles/admin.json +5 -0
  55. package/kits/templates/roles/member.json +17 -0
  56. package/kits/templates/roles/visitor.json +8 -0
  57. package/kits/templates/system-fragments/bootstrap.md +12 -6
  58. package/kits/templates/system-fragments/channel.md +6 -0
  59. package/kits/templates/system-fragments/session.md +2 -0
  60. package/package.json +2 -1
  61. package/skills/eclink/SKILL.md +15 -3
  62. package/skills/eclink/agents/openai.yaml +3 -3
@@ -3,8 +3,9 @@ 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, containsLClassReference, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
6
+ import { containsHClassReference, containsLClassReference, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, } from '../protected-paths.js';
7
7
  import { classifyEvolcoreShellCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, } from './ec-command-parser.js';
8
+ import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
8
9
  // 绝对禁止命令(所有角色、所有权限模式下都禁止,不可授权)
9
10
  // 这些是系统级破坏操作,任何情况下都不应该允许
10
11
  const ABSOLUTE_FORBIDDEN = [
@@ -14,19 +15,6 @@ const ABSOLUTE_FORBIDDEN = [
14
15
  /\bformat\s+[a-zA-Z]:/i, // format C: (格式化磁盘)
15
16
  /\bdd\s+if=.*of=\/dev/, // dd 写入磁盘设备(读取操作允许)
16
17
  ];
17
- const READONLY_SHELL_EXECUTABLES = new Set([
18
- 'ls', '/bin/ls', '/usr/bin/ls',
19
- 'cat', '/bin/cat', '/usr/bin/cat',
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(?:=|$)/;
30
18
  // 危险操作(需要用户审批才能执行)
31
19
  // 这些操作有合理使用场景,但需要用户明确授权
32
20
  const DANGEROUS_PATTERNS = [
@@ -331,19 +319,163 @@ function containsNetStopCommand(command, depth = 0) {
331
319
  && containsNetStopCommand(parsed.args.slice(commandIndex + 1).join(' '), depth + 1);
332
320
  });
333
321
  }
322
+ function queryPathOperands(analysis) {
323
+ return analysis.kind === 'proven-readonly' ? analysis.ir.pathOperands : [];
324
+ }
325
+ function shellOperandMatchesClass(operand, className, options) {
326
+ const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
327
+ if (reference)
328
+ return true;
329
+ if (operand.access === 'recursive') {
330
+ return className === 'h'
331
+ ? hClassReadScopeIncludesProtectedPath(operand.value, options)
332
+ : lClassGrantIncludesProtectedPath(operand.value, options);
333
+ }
334
+ return className === 'h'
335
+ ? isHClassPath(operand.value, options)
336
+ : isLClassPath(operand.value, options);
337
+ }
334
338
  /**
335
- * 只读模式检查(用于 PreToolUse hook canUseTool callback)
336
- * 显式读工具自动允许;写工具和未知工具拒绝。
337
- * Bash 默认拒绝;只允许由严格字面量解析器证明为 ls/cat 的只读命令。
338
- * 通过 EC Shell 边界分类的整条字面量调用应由 runner 在调用本函数前放行。
339
+ * A read-only recursive query only needs to cover H-class targets that exist
340
+ * now. This deliberately differs from a filesystem *grant*: a grant below
341
+ * any directory could later create a protected backup name, while a proven
342
+ * query cannot write and should still be able to inspect an ordinary logs/
343
+ * directory.
344
+ */
345
+ function hClassReadScopeIncludesProtectedPath(value, options) {
346
+ const root = resolveProtectedCandidate(options.root ?? resolveRoot());
347
+ const scope = resolveProtectedCandidate(value, options.cwd);
348
+ if (isSameOrDescendant(root, scope))
349
+ return true;
350
+ if (!isSameOrDescendant(scope, root))
351
+ return false;
352
+ return getExistingHClassMaskTargets(root).some(target => isSameOrDescendant(target.path, scope));
353
+ }
354
+ function analyzeShellProtectedOperands(command, options) {
355
+ const analysis = analyzeReadonlyShellQuery(command);
356
+ if (analysis.kind === 'unproven') {
357
+ return {
358
+ analysis,
359
+ hClass: containsHClassReference(command),
360
+ lClass: containsLClassReference(command),
361
+ };
362
+ }
363
+ const operands = queryPathOperands(analysis);
364
+ return {
365
+ analysis,
366
+ hClass: operands.some(operand => shellOperandMatchesClass(operand, 'h', options)),
367
+ lClass: operands.some(operand => shellOperandMatchesClass(operand, 'l', options)),
368
+ };
369
+ }
370
+ function protectedReadToolPaths(toolName, input) {
371
+ if (toolName === 'Read') {
372
+ return typeof input.file_path === 'string' ? [input.file_path] : [];
373
+ }
374
+ if (toolName === 'Glob') {
375
+ return [input.pattern, input.path].filter((value) => typeof value === 'string' && value.length > 0);
376
+ }
377
+ if (toolName === 'Grep') {
378
+ return typeof input.path === 'string' ? [input.path] : [];
379
+ }
380
+ return [];
381
+ }
382
+ function protectedReadToolMatchesLClass(toolName, input, options) {
383
+ const paths = protectedReadToolPaths(toolName, input);
384
+ if (paths.some(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options))) {
385
+ return true;
386
+ }
387
+ if (toolName === 'Grep') {
388
+ const scope = typeof input.path === 'string' && input.path ? input.path : '.';
389
+ return lClassGrantIncludesProtectedPath(scope, options);
390
+ }
391
+ if (toolName === 'Glob') {
392
+ const scope = globReadScope(input, options);
393
+ return lClassGrantIncludesProtectedPath(scope, { root: options.root });
394
+ }
395
+ return false;
396
+ }
397
+ function protectedReadToolMatchesHClass(toolName, input, options) {
398
+ const paths = protectedReadToolPaths(toolName, input);
399
+ if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options))) {
400
+ return true;
401
+ }
402
+ if (toolName === 'Grep') {
403
+ const scope = typeof input.path === 'string' && input.path ? input.path : '.';
404
+ return hClassReadScopeIncludesProtectedPath(scope, options);
405
+ }
406
+ if (toolName === 'Glob') {
407
+ const scope = globReadScope(input, options);
408
+ return hClassReadScopeIncludesProtectedPath(scope, options);
409
+ }
410
+ return false;
411
+ }
412
+ function globLiteralPrefix(pattern) {
413
+ const normalized = pattern.replace(/\\/g, '/');
414
+ const globIndex = normalized.search(/[*?[{]/);
415
+ return globIndex < 0 ? normalized : normalized.slice(0, globIndex).replace(/\/+$/, '');
416
+ }
417
+ function globReadScope(input, options) {
418
+ const pattern = typeof input.pattern === 'string' && input.pattern ? input.pattern : '**/*';
419
+ const baseValue = typeof input.path === 'string' && input.path ? input.path : '.';
420
+ const base = resolveProtectedCandidate(baseValue, options.cwd);
421
+ const prefix = globLiteralPrefix(pattern);
422
+ return path.isAbsolute(pattern)
423
+ ? resolveProtectedCandidate(prefix || path.parse(pattern).root)
424
+ : resolveProtectedCandidate(prefix || '.', base);
425
+ }
426
+ function protectedReadToolStaysInWorkspace(toolName, input, projectPath) {
427
+ const workspace = resolveProtectedCandidate(projectPath);
428
+ if (toolName === 'Read') {
429
+ if (typeof input.file_path !== 'string' || !input.file_path)
430
+ return false;
431
+ return isSameOrDescendant(resolveProtectedCandidate(input.file_path, projectPath), workspace);
432
+ }
433
+ if (toolName === 'Grep') {
434
+ const scope = typeof input.path === 'string' && input.path ? input.path : '.';
435
+ return isSameOrDescendant(resolveProtectedCandidate(scope, projectPath), workspace);
436
+ }
437
+ if (toolName === 'Glob') {
438
+ const baseValue = typeof input.path === 'string' && input.path ? input.path : '.';
439
+ const base = resolveProtectedCandidate(baseValue, projectPath);
440
+ if (!isSameOrDescendant(base, workspace))
441
+ return false;
442
+ const scope = globReadScope(input, { cwd: projectPath });
443
+ return isSameOrDescendant(scope, workspace);
444
+ }
445
+ return true;
446
+ }
447
+ function readonlyShellStaysInWorkspace(analysis, projectPath) {
448
+ if (analysis.kind !== 'proven-readonly')
449
+ return false;
450
+ const workspace = resolveProtectedCandidate(projectPath);
451
+ return analysis.ir.pathOperands.every(operand => isSameOrDescendant(resolveProtectedCandidate(operand.value, projectPath), workspace));
452
+ }
453
+ /**
454
+ * 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)。普通
455
+ * 路径上的受限查询 Bash 可自动执行;H/L-class 和未建模 Shell 仍拒绝。
339
456
  */
340
457
  export function checkReadonly(toolName, input, projectPath, context) {
341
458
  const readOnlyTools = new Set([
342
459
  'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
343
460
  'TaskList', 'TaskGet', 'ToolSearch',
344
461
  ]);
345
- if (readOnlyTools.has(toolName))
462
+ if (readOnlyTools.has(toolName)) {
463
+ const paths = protectedReadToolPaths(toolName, input);
464
+ const pathOptions = { cwd: projectPath, root: context?.root };
465
+ if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
466
+ return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录' };
467
+ }
468
+ if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions))) {
469
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
470
+ }
471
+ if (protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
472
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
473
+ }
474
+ if (protectedReadToolMatchesLClass(toolName, input, pathOptions)) {
475
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
476
+ }
346
477
  return { behavior: 'allow' };
478
+ }
347
479
  if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
348
480
  const filePath = (input.file_path || input.notebook_path);
349
481
  logger.warn(`[ReadonlyCheck] 🔒 File write blocked: tool=${toolName} path=${filePath} project=${projectPath} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
@@ -351,25 +483,27 @@ export function checkReadonly(toolName, input, projectPath, context) {
351
483
  }
352
484
  if (toolName === 'Bash') {
353
485
  const cmd = input.command || '';
354
- const argv = parseLiteralShellArgv(cmd);
355
- const executable = argv?.[0];
356
- const readonlyExecutable = executable && READONLY_SHELL_EXECUTABLES.has(executable);
357
- if (context?.allowLiteralReadShell === true && argv && readonlyExecutable) {
358
- const protectedOperand = argv.slice(1).some(argument => {
359
- if (argument === '-' || argument === '--' || argument.startsWith('-'))
360
- return false;
361
- return containsHClassReference(argument)
362
- || isHClassPath(argument, { cwd: projectPath });
363
- });
364
- if (!containsHClassReference(cmd) && !protectedOperand) {
365
- return { behavior: 'allow' };
366
- }
367
- logger.warn(`[ReadonlyCheck] 🔒 Protected path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
368
- return { behavior: 'deny', message: '🔒 只读模式:ls/cat 不允许读取 H 类受保护路径' };
486
+ const protectedOperands = analyzeShellProtectedOperands(cmd, { cwd: projectPath, root: context?.root });
487
+ if (protectedOperands.analysis.kind === 'proven-readonly'
488
+ && !protectedOperands.hClass
489
+ && !protectedOperands.lClass
490
+ && readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath)) {
491
+ return { behavior: 'allow' };
492
+ }
493
+ if (protectedOperands.hClass) {
494
+ logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
495
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径' };
496
+ }
497
+ if (protectedOperands.lClass) {
498
+ logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
499
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
500
+ }
501
+ if (protectedOperands.analysis.kind === 'proven-readonly') {
502
+ return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录' };
369
503
  }
370
504
  const cmdPreview = cmd.length > 80 ? cmd.substring(0, 80) + '...' : cmd;
371
505
  logger.warn(`[ReadonlyCheck] 🔒 Bash blocked by default: cmd="${cmdPreview}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
372
- return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许单个字面量 ls/cat 命令' };
506
+ return { behavior: 'deny', message: '🔒 只读模式:Shell 仅允许受限、可证明只读的普通路径查询' };
373
507
  }
374
508
  logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
375
509
  return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行` };
@@ -577,53 +711,6 @@ function collectFileChangePaths(value, output) {
577
711
  }
578
712
  }
579
713
  }
580
- function shellCommandReferencesLClass(command, options) {
581
- const normalized = command.replace(/\\/g, '/');
582
- if (containsLClassReference(normalized))
583
- return true;
584
- const argv = parseLiteralShellArgv(command);
585
- return argv?.slice(1).some(argument => {
586
- if (argument === '-' || argument === '--' || argument.startsWith('-'))
587
- return false;
588
- return isLClassPath(argument, options);
589
- }) ?? false;
590
- }
591
- function isVerifiedLClassReadOnlyShellCommand(command) {
592
- const argv = parseLiteralShellArgv(command);
593
- if (!argv || !L_CLASS_READONLY_SHELL_EXECUTABLES.has(argv[0]))
594
- return false;
595
- switch (argv[0].replace(/^.*\//, '')) {
596
- case 'grep':
597
- // grep never mutates its operands. Shell composition is rejected by the
598
- // literal parser before this point.
599
- return true;
600
- case 'rg':
601
- // ripgrep's --pre hooks may execute an arbitrary process.
602
- return !argv.slice(1).some(argument => RG_PROCESS_SPAWNING_OPTION_RE.test(argument));
603
- case 'find':
604
- // Keep only the predicates/actions which cannot create, mutate, or
605
- // execute files. Other find options remain fail-closed.
606
- return !argv.slice(1).some(argument => FIND_MUTATING_PRIMARY_RE.test(argument));
607
- case 'sed': {
608
- // sed can write via -i or a `w` command. Permit only the bounded
609
- // inspection form used for line-range reads: sed -n 'START[,END]p'.
610
- const quiet = argv[1] === '-n' || argv[1] === '--quiet' || argv[1] === '--silent';
611
- const program = quiet ? argv[2] : undefined;
612
- return !!program
613
- && /^(?:\d+(?:,\d+)?|\$)?p$/.test(program)
614
- // GNU sed accepts options after the script as well. Reject every
615
- // later option so -i/-e/-f cannot be smuggled into this read form.
616
- && !argv.slice(3).some(argument => argument.startsWith('-'));
617
- }
618
- default:
619
- return true;
620
- }
621
- }
622
- function isUnverifiedLClassShellCommand(command, options) {
623
- if (!shellCommandReferencesLClass(command, options))
624
- return false;
625
- return !isVerifiedLClassReadOnlyShellCommand(command);
626
- }
627
714
  function requestsFilesystemRoot(value) {
628
715
  if (Array.isArray(value))
629
716
  return value.some(requestsFilesystemRoot);
@@ -647,8 +734,11 @@ export function checkHClassWrite(toolName, input, context) {
647
734
  const paths = [];
648
735
  const grantPaths = [];
649
736
  const projectRootGrantSubpaths = [];
650
- if (['Read', 'Write', 'Edit', 'NotebookEdit', 'Glob', 'Grep'].includes(toolName)) {
651
- const filePath = input.file_path ?? input.notebook_path ?? input.path ?? input.pattern;
737
+ if (['Read', 'Glob', 'Grep'].includes(toolName)) {
738
+ paths.push(...protectedReadToolPaths(toolName, input));
739
+ }
740
+ else if (['Write', 'Edit', 'NotebookEdit'].includes(toolName)) {
741
+ const filePath = input.file_path ?? input.notebook_path ?? input.path;
652
742
  if (typeof filePath === 'string' && filePath)
653
743
  paths.push(filePath);
654
744
  }
@@ -661,9 +751,13 @@ export function checkHClassWrite(toolName, input, context) {
661
751
  collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
662
752
  }
663
753
  else if (toolName === 'Bash') {
664
- const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
665
- if (containsHClassReference(command)) {
666
- logger.warn(`[H-Class Protection] 🔒 Protected path referenced by shell command: tool=${toolName} ` +
754
+ const command = typeof input.command === 'string' ? input.command : '';
755
+ const protectedOperands = analyzeShellProtectedOperands(command, {
756
+ cwd: context?.projectPath,
757
+ root: context?.root,
758
+ });
759
+ if (protectedOperands.hClass) {
760
+ logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
667
761
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
668
762
  return {
669
763
  behavior: 'deny',
@@ -686,6 +780,15 @@ export function checkHClassWrite(toolName, input, context) {
686
780
  // filesystem operation is checked again when it reaches Read/Write/Bash,
687
781
  // FileChange, or PermissionGrant above.
688
782
  const pathOptions = { cwd: context?.projectPath, root: context?.root };
783
+ if (['Read', 'Glob', 'Grep'].includes(toolName)
784
+ && protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
785
+ logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
786
+ `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
787
+ return {
788
+ behavior: 'deny',
789
+ message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
790
+ };
791
+ }
689
792
  if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
690
793
  logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
691
794
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
@@ -771,8 +874,14 @@ export function checkHClassWrite(toolName, input, context) {
771
874
  * operations may mutate, delete, move, replace, or grant write access to them.
772
875
  */
773
876
  export function checkLClassWrite(toolName, input, context) {
774
- if (['Read', 'Glob', 'Grep'].includes(toolName))
877
+ if (['Read', 'Glob', 'Grep'].includes(toolName)) {
878
+ const pathOptions = { cwd: context?.projectPath, root: context?.root };
879
+ const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
880
+ if (lClassRead && context?.permissionMode === 'readonly') {
881
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
882
+ }
775
883
  return { behavior: 'allow' };
884
+ }
776
885
  const paths = [];
777
886
  const writeGrantPaths = [];
778
887
  const projectRootWriteSubpaths = [];
@@ -794,14 +903,19 @@ export function checkLClassWrite(toolName, input, context) {
794
903
  collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
795
904
  }
796
905
  else if (toolName === 'Bash') {
797
- const command = typeof input.command === 'string' ? input.command.replace(/\\/g, '/') : '';
906
+ const command = typeof input.command === 'string' ? input.command : '';
798
907
  const pathOptions = { cwd: context?.projectPath, root: context?.root };
799
- if (isUnverifiedLClassShellCommand(command, pathOptions)) {
800
- logger.warn(`[L-Class Protection] Unverified L-class shell command denied: tool=${toolName} ` +
908
+ const protectedOperands = analyzeShellProtectedOperands(command, pathOptions);
909
+ const lClassDenied = protectedOperands.lClass
910
+ && (context?.permissionMode === 'readonly' || protectedOperands.analysis.kind === 'unproven');
911
+ if (lClassDenied) {
912
+ logger.warn(`[L-Class Protection] L-class shell access denied: tool=${toolName} ` +
801
913
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
802
914
  return {
803
915
  behavior: 'deny',
804
- message: '🔒 L-class 路径仅允许单个可验证的只读命令。请拆分为单条 ls/cat/grep/rg/find 或 sed -n,或使用 Read;禁止管道、重定向、变量展开和命令拼接。修改必须通过受控的 EvolCore 命令',
916
+ message: context?.permissionMode === 'readonly'
917
+ ? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
918
+ : '🔒 L-class 路径仅允许受限、可证明只读的命令;未知 Shell 语法失败关闭。修改必须通过受控的 EvolCore 命令',
805
919
  };
806
920
  }
807
921
  collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
@@ -957,6 +1071,29 @@ function prepareBoundedOutputInput(input, command, kind, context) {
957
1071
  export function evaluateToolPreflight(toolName, input, context) {
958
1072
  if (toolName === 'Bash') {
959
1073
  const command = typeof input.command === 'string' ? input.command : '';
1074
+ if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(command)) {
1075
+ return {
1076
+ behavior: 'deny',
1077
+ input,
1078
+ message: '🔒 EvolCore 托管会话不需要探测 `ec` 是否存在;请不要调用 `command -v`/`which`/`type`,也不要拼接其它命令。通信必须走当前 session 的 delegated sender。',
1079
+ };
1080
+ }
1081
+ const argv = context.sessionId ? parseLiteralShellArgv(command) : null;
1082
+ const isOwnAgentMdPublish = !!argv
1083
+ && argv.length === 5
1084
+ && argv[0] === 'ec'
1085
+ && argv[1] === 'aid'
1086
+ && argv[2] === 'agentmd'
1087
+ && argv[3] === 'put'
1088
+ && !!context.selfAid
1089
+ && argv[4].replace(/^@/, '') === context.selfAid.replace(/^@/, '');
1090
+ if (context.sessionId && /^\s*ec\s+aid(?:\s|$)/i.test(command) && !isOwnAgentMdPublish) {
1091
+ return {
1092
+ behavior: 'deny',
1093
+ input,
1094
+ message: '🔒 EvolCore 托管会话仅允许通过 `ec aid agentmd put <当前 self AID>` 发布自身名片;禁止通过 `ec aid` 自行发现、切换或操作其他身份。',
1095
+ };
1096
+ }
960
1097
  const ecCommand = classifyEvolcoreShellCommand(command);
961
1098
  if (ecCommand.kind === 'literal') {
962
1099
  return { behavior: 'allow', input, reason: 'ec-command' };
@@ -982,7 +1119,7 @@ export function evaluateToolPreflight(toolName, input, context) {
982
1119
  return {
983
1120
  behavior: 'deny',
984
1121
  input,
985
- message: '🔒 EC 命令必须作为单个字面量进程调用;请移除管道、重定向、变量展开或命令拼接',
1122
+ message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
986
1123
  };
987
1124
  }
988
1125
  const boundedOutput = parseBoundedOutputShellCommand(command);
@@ -1020,6 +1157,7 @@ export function evaluateToolPreflight(toolName, input, context) {
1020
1157
  channel: context.channel,
1021
1158
  peerId: context.userId,
1022
1159
  role: context.role,
1160
+ permissionMode: context.permissionMode,
1023
1161
  projectPath: context.projectPath,
1024
1162
  workspacePath: context.workspacePath,
1025
1163
  root: context.root,
@@ -293,10 +293,11 @@ export function getExistingLClassReadOnlyTargets(root = resolveRoot()) {
293
293
  catch { }
294
294
  return [...targets.values()];
295
295
  }
296
- export function buildClaudeProtectedFilesystem(root = resolveRoot()) {
296
+ export function buildClaudeProtectedFilesystem(root = resolveRoot(), options = {}) {
297
297
  const hClassPaths = getExistingHClassMaskTargets(root).map(target => target.path);
298
298
  const lClassPaths = getExistingLClassReadOnlyTargets(root).map(target => target.path);
299
299
  const allPaths = [...new Set([...hClassPaths, ...lClassPaths])];
300
+ const denyRead = options.denyLClassRead ? allPaths : hClassPaths;
300
301
  if (allPaths.length > CLAUDE_PROTECTED_TARGET_LIMIT) {
301
302
  throw new Error(`[ClaudeSandbox] protected target limit exceeded: ${allPaths.length} > ${CLAUDE_PROTECTED_TARGET_LIMIT}`);
302
303
  }
@@ -309,7 +310,7 @@ export function buildClaudeProtectedFilesystem(root = resolveRoot()) {
309
310
  }
310
311
  }
311
312
  return {
312
- denyRead: [...new Set(hClassPaths)],
313
+ denyRead: [...new Set(denyRead)],
313
314
  denyWrite: allPaths,
314
315
  };
315
316
  }
@@ -321,16 +322,18 @@ export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
321
322
  ...Object.fromEntries(getHClassSandboxPatterns(root).map(pattern => [pattern, 'deny'])),
322
323
  };
323
324
  }
324
- export function buildCodexProtectedFilesystemRules(root = resolveRoot()) {
325
+ export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
325
326
  const lClassRules = {};
327
+ const lClassAccess = options.denyLClassRead ? 'deny' : 'read';
326
328
  for (const target of getExistingLClassReadOnlyTargets(root)) {
327
329
  if (target.kind === 'directory') {
328
- if (path.basename(target.path) === 'triggers')
329
- lClassRules[target.path] = 'read';
330
- lClassRules[path.join(target.path, '**')] = 'read';
330
+ if (options.denyLClassRead || path.basename(target.path) === 'triggers') {
331
+ lClassRules[target.path] = lClassAccess;
332
+ }
333
+ lClassRules[path.join(target.path, '**')] = lClassAccess;
331
334
  }
332
335
  else {
333
- lClassRules[target.path] = 'read';
336
+ lClassRules[target.path] = lClassAccess;
334
337
  }
335
338
  }
336
339
  return {
@@ -0,0 +1,57 @@
1
+ export async function disposeAgentInstances(instances, onError) {
2
+ await Promise.allSettled(instances.map(async (instance) => {
3
+ try {
4
+ await instance.agent.dispose?.();
5
+ }
6
+ catch (error) {
7
+ onError?.(instance, error);
8
+ }
9
+ }));
10
+ }
11
+ export function createRunnerReloadTransaction(input) {
12
+ const prefix = `${input.aid}::`;
13
+ const previous = [...input.agentMap.entries()]
14
+ .filter(([key]) => key.startsWith(prefix))
15
+ .map(([key, agent]) => {
16
+ const split = key.lastIndexOf('::');
17
+ return {
18
+ evolagentName: key.slice(0, split),
19
+ baseagent: key.slice(split + 2),
20
+ agent,
21
+ };
22
+ });
23
+ let committed = false;
24
+ let stagedDisposed = false;
25
+ const disposeStaged = async () => {
26
+ if (stagedDisposed)
27
+ return;
28
+ stagedDisposed = true;
29
+ await disposeAgentInstances(input.staged, input.onDisposeError);
30
+ };
31
+ return {
32
+ commit() {
33
+ if (committed)
34
+ return;
35
+ for (const instance of previous)
36
+ input.agentMap.delete(`${instance.evolagentName}::${instance.baseagent}`);
37
+ for (const instance of input.staged)
38
+ input.agentMap.set(`${instance.evolagentName}::${instance.baseagent}`, instance.agent);
39
+ committed = true;
40
+ },
41
+ async rollback() {
42
+ if (committed) {
43
+ for (const instance of input.staged)
44
+ input.agentMap.delete(`${instance.evolagentName}::${instance.baseagent}`);
45
+ for (const instance of previous)
46
+ input.agentMap.set(`${instance.evolagentName}::${instance.baseagent}`, instance.agent);
47
+ committed = false;
48
+ }
49
+ await disposeStaged();
50
+ },
51
+ async finalize() {
52
+ if (!committed)
53
+ return;
54
+ await disposeAgentInstances(previous, input.onDisposeError);
55
+ },
56
+ };
57
+ }