evolcore 0.0.18 → 0.0.20

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 (51) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +2 -0
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/claude-runner.js +4 -3
  5. package/dist/agents/codex-runner.js +25 -20
  6. package/dist/agents/ecagent-runner.js +3 -2
  7. package/dist/aun/aid/agentmd.js +7 -0
  8. package/dist/aun/msg/group.js +14 -3
  9. package/dist/aun/msg/p2p.js +21 -11
  10. package/dist/aun/outbox.js +144 -19
  11. package/dist/channels/aun.js +621 -211
  12. package/dist/cli/daemon-commands.js +41 -8
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/init.js +55 -15
  15. package/dist/cli/restart-monitor.js +3 -3
  16. package/dist/config/aun-gateway-config.js +2 -0
  17. package/dist/config/config-manager.js +92 -8
  18. package/dist/config/config-operation-service.js +1 -2
  19. package/dist/config/gateway-config.js +9 -7
  20. package/dist/config/lifecycle.js +16 -5
  21. package/dist/config-store.js +13 -6
  22. package/dist/core/auth/authorization-audit.js +5 -2
  23. package/dist/core/bootstrap-messages.js +2 -2
  24. package/dist/core/bootstrap-service.js +21 -36
  25. package/dist/core/channel-loader.js +0 -2
  26. package/dist/core/data-migration.js +10 -4
  27. package/dist/core/evolagent.js +5 -4
  28. package/dist/core/message/message-bridge.js +6 -11
  29. package/dist/core/message/response-engine.js +62 -6
  30. package/dist/core/permission/ec-command-parser.js +203 -24
  31. package/dist/core/permission/sandbox-runtime.js +46 -12
  32. package/dist/core/permission/tool-policy.js +116 -47
  33. package/dist/core/relation/peer-identity.js +18 -0
  34. package/dist/eck/kit-renderer.js +17 -8
  35. package/dist/index.js +30 -19
  36. package/dist/ipc.js +6 -1
  37. package/dist/paths.js +0 -3
  38. package/dist/utils/stats.js +52 -18
  39. package/dist/utils/welcome.js +2 -2
  40. package/kits/docs/path-registry.md +1 -1
  41. package/kits/rules/01-overview.md +1 -1
  42. package/kits/rules/02-navigation.md +2 -2
  43. package/kits/rules/03-identity.md +1 -1
  44. package/kits/rules/05-venue.md +1 -1
  45. package/kits/schemas/_meta.json +7 -4
  46. package/kits/schemas/agent-config.schema.10.json +2 -1
  47. package/kits/schemas/agent-config.schema.11.json +408 -0
  48. package/kits/schemas/daemon.schema.5.json +136 -0
  49. package/kits/schemas/defaults.schema.5.json +107 -0
  50. package/package.json +2 -1
  51. package/dist/core/message/pause-controller.js +0 -53
@@ -285,6 +285,48 @@ function appendLClassReadOnlyBinds(args, root) {
285
285
  args.push('--ro-bind', target.path, target.path);
286
286
  }
287
287
  }
288
+ export function ensureCodexManagedMountpoint(systemConfigDirectory = '/etc/codex', effectiveUid = typeof process.getuid === 'function' ? process.getuid() : undefined) {
289
+ let systemConfigStat;
290
+ try {
291
+ systemConfigStat = fs.lstatSync(systemConfigDirectory);
292
+ }
293
+ catch (error) {
294
+ if (error.code !== 'ENOENT') {
295
+ throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
296
+ + `could not be inspected (${error instanceof Error ? error.message : String(error)})`);
297
+ }
298
+ }
299
+ if (!systemConfigStat && process.platform === 'linux' && effectiveUid === 0) {
300
+ let created = false;
301
+ try {
302
+ fs.mkdirSync(systemConfigDirectory, { mode: 0o755 });
303
+ created = true;
304
+ }
305
+ catch (error) {
306
+ if (error.code !== 'EEXIST') {
307
+ throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
308
+ + `could not be created by the root daemon (${error instanceof Error ? error.message : String(error)})`);
309
+ }
310
+ }
311
+ if (created)
312
+ fs.chmodSync(systemConfigDirectory, 0o755);
313
+ try {
314
+ systemConfigStat = fs.lstatSync(systemConfigDirectory);
315
+ }
316
+ catch (error) {
317
+ throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
318
+ + `could not be verified after creation (${error instanceof Error ? error.message : String(error)})`);
319
+ }
320
+ }
321
+ if (!systemConfigStat) {
322
+ throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
323
+ + 'must be created as a real directory during installation; run: sudo install -d -m 0755 /etc/codex');
324
+ }
325
+ if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
326
+ throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
327
+ + 'must be a real directory');
328
+ }
329
+ }
288
330
  export function buildHClassGuardCommand(executable, executableArgs, root = resolveRoot(), options = {}) {
289
331
  const bubblewrapPath = resolveBubblewrapPath();
290
332
  if (!bubblewrapPath)
@@ -306,18 +348,10 @@ export function buildHClassGuardCommand(executable, executableArgs, root = resol
306
348
  }
307
349
  const managedDirectory = path.dirname(requirements);
308
350
  const systemConfigDirectory = '/etc/codex';
309
- let systemConfigStat;
310
- try {
311
- systemConfigStat = fs.lstatSync(systemConfigDirectory);
312
- }
313
- catch {
314
- throw new Error(`Codex managed requirements mountpoint is unavailable: ${systemConfigDirectory} `
315
- + 'must be created as a real directory during installation');
316
- }
317
- if (!systemConfigStat.isDirectory() || systemConfigStat.isSymbolicLink()) {
318
- throw new Error(`Codex managed requirements mountpoint is unsafe: ${systemConfigDirectory} `
319
- + 'must be a real directory');
320
- }
351
+ // The npm lifecycle and public installer provision this path. Root-owned
352
+ // legacy daemons may repair only the missing-directory case; unsafe path
353
+ // types and unprivileged processes remain fail-closed.
354
+ ensureCodexManagedMountpoint(systemConfigDirectory);
321
355
  // Keep the copied hook immutable even when a session uses bypass mode.
322
356
  args.push('--ro-bind', managedDirectory, managedDirectory);
323
357
  // The guard starts from a writable root so owner-bypass workspaces keep
@@ -4,7 +4,7 @@ import { randomUUID } from 'crypto';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  import { resolveRoot } from '../../paths.js';
6
6
  import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from '../protected-paths.js';
7
- import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, unwrapCodexShellCommandArgv, } from './ec-command-parser.js';
7
+ import { classifyEvolcoreShellCommand, parseCodexToolCommandArgv, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, } from './ec-command-parser.js';
8
8
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
9
9
  /** Resolve the session-owned temporary root supplied by the execution host. */
10
10
  export function resolveManagedTempDir(env = process.env) {
@@ -425,7 +425,7 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
425
425
  const workspace = options.cwd ? resolveProtectedCandidate(options.cwd) : undefined;
426
426
  if (isSameOrDescendant(candidate, managedTemp)
427
427
  && (!workspace || !isSameOrDescendant(candidate, workspace))) {
428
- return false;
428
+ return undefined;
429
429
  }
430
430
  }
431
431
  if (className === 'h' && accessKind === 'metadata' && operand.access === 'exact') {
@@ -439,27 +439,31 @@ function shellOperandMatchesClass(operand, className, options, accessKind = oper
439
439
  followFinalSymlink: false,
440
440
  });
441
441
  if (options.allowProtectedMetadata === false) {
442
- return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory';
442
+ return metadata.pathClass === 'h-file' || metadata.pathClass === 'h-directory'
443
+ ? resolveProtectedCandidate(operand.value, options.cwd)
444
+ : undefined;
443
445
  }
444
446
  // The no-follow decision is authoritative for metadata. In particular,
445
447
  // an ordinary symlink whose target is H-class must remain inspectable as a
446
448
  // link; falling through to isHClassPath() would follow it and reject the
447
449
  // otherwise safe lstat/stat operation.
448
450
  if (metadata.pathClass !== 'unknown' && metadata.allowed) {
449
- return false;
451
+ return undefined;
450
452
  }
451
453
  }
452
454
  const reference = className === 'h' ? containsHClassReference(operand.value) : containsLClassReference(operand.value);
453
455
  if (reference)
454
- return true;
456
+ return resolveProtectedCandidate(operand.value, options.cwd);
455
457
  if (operand.access === 'recursive') {
456
- return className === 'h'
458
+ const matched = className === 'h'
457
459
  ? hClassReadScopeIncludesProtectedPath(operand.value, options)
458
460
  : lClassGrantIncludesProtectedPath(operand.value, options);
461
+ return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
459
462
  }
460
- return className === 'h'
463
+ const matched = className === 'h'
461
464
  ? isHClassPath(operand.value, options)
462
465
  : isLClassPath(operand.value, options);
466
+ return matched ? resolveProtectedCandidate(operand.value, options.cwd) : undefined;
463
467
  }
464
468
  /**
465
469
  * A read-only recursive query only needs to cover H-class targets that exist
@@ -489,16 +493,41 @@ function analyzeShellProtectedOperands(command, options) {
489
493
  };
490
494
  }
491
495
  const operands = queryPathOperands(analysis);
492
- const lockDiagnostic = /(?:^|[\/\s'"`])(?:[^\s'"`/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?=$|[\/\s'"`=])/i.test(command);
493
- const pathHClass = operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind));
496
+ // Only parsed path operands can establish a lock-file diagnostic. Searching
497
+ // the raw command would mistake a grep/rg pattern or report text for an
498
+ // actual protected filesystem target.
499
+ const lockDiagnostic = operands.some(({ operand }) => containsLockPathReference(operand.value))
500
+ || hasFindLockPattern(analysis);
501
+ const hClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)).find(Boolean);
502
+ const lClassPath = operands.map(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)).find(Boolean);
494
503
  return {
495
504
  analysis,
496
- hClass: lockDiagnostic || pathHClass,
497
- lClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)),
498
- pathHClass,
505
+ hClass: lockDiagnostic || !!hClassPath,
506
+ lClass: !!lClassPath,
507
+ pathHClass: !!hClassPath,
499
508
  lockDiagnostic,
509
+ hClassPath,
510
+ lClassPath,
500
511
  };
501
512
  }
513
+ /** Detect a find pattern such as `-name '*.lock'` from parsed argv only. */
514
+ function hasFindLockPattern(analysis) {
515
+ if (analysis.kind !== 'proven-readonly')
516
+ return false;
517
+ return analysis.ir.pipelines.some(pipeline => pipeline.some(command => {
518
+ if (command.executable !== 'find')
519
+ return false;
520
+ for (let index = 0; index < command.argv.length - 1; index++) {
521
+ if (!['-name', '-iname', '-path', '-ipath', '-wholename'].includes(command.argv[index]))
522
+ continue;
523
+ if (/(?:^|[\\/])[^\\/]*\.lock(?:$|[\\/])/i.test(command.argv[index + 1]))
524
+ return true;
525
+ if (/^EVOLCORE_RUNTIME_LOCK_DIR$/i.test(command.argv[index + 1]))
526
+ return true;
527
+ }
528
+ return false;
529
+ }));
530
+ }
502
531
  function protectedReadToolPaths(toolName, input) {
503
532
  if (toolName === 'Read') {
504
533
  return typeof input.file_path === 'string' ? [input.file_path] : [];
@@ -513,33 +542,43 @@ function protectedReadToolPaths(toolName, input) {
513
542
  }
514
543
  function protectedReadToolMatchesLClass(toolName, input, options) {
515
544
  const paths = protectedReadToolPaths(toolName, input);
516
- if (paths.some(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options))) {
517
- return true;
545
+ const direct = paths.find(filePath => containsLClassReference(filePath) || isLClassPath(filePath, options));
546
+ if (direct) {
547
+ return resolveProtectedCandidate(direct, options.cwd);
518
548
  }
519
549
  if (toolName === 'Grep') {
520
550
  const scope = typeof input.path === 'string' && input.path ? input.path : '.';
521
- return lClassGrantIncludesProtectedPath(scope, options);
551
+ return lClassGrantIncludesProtectedPath(scope, options)
552
+ ? resolveProtectedCandidate(scope, options.cwd)
553
+ : undefined;
522
554
  }
523
555
  if (toolName === 'Glob') {
524
556
  const scope = globReadScope(input, options);
525
- return lClassGrantIncludesProtectedPath(scope, { root: options.root });
557
+ return lClassGrantIncludesProtectedPath(scope, { root: options.root })
558
+ ? resolveProtectedCandidate(scope, options.cwd)
559
+ : undefined;
526
560
  }
527
- return false;
561
+ return undefined;
528
562
  }
529
563
  function protectedReadToolMatchesHClass(toolName, input, options) {
530
564
  const paths = protectedReadToolPaths(toolName, input);
531
- if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options))) {
532
- return true;
565
+ const direct = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, options));
566
+ if (direct) {
567
+ return resolveProtectedCandidate(direct, options.cwd);
533
568
  }
534
569
  if (toolName === 'Grep') {
535
570
  const scope = typeof input.path === 'string' && input.path ? input.path : '.';
536
- return hClassReadScopeIncludesProtectedPath(scope, options);
571
+ return hClassReadScopeIncludesProtectedPath(scope, options)
572
+ ? resolveProtectedCandidate(scope, options.cwd)
573
+ : undefined;
537
574
  }
538
575
  if (toolName === 'Glob') {
539
576
  const scope = globReadScope(input, options);
540
- return hClassReadScopeIncludesProtectedPath(scope, options);
577
+ return hClassReadScopeIncludesProtectedPath(scope, options)
578
+ ? resolveProtectedCandidate(scope, options.cwd)
579
+ : undefined;
541
580
  }
542
- return false;
581
+ return undefined;
543
582
  }
544
583
  function globLiteralPrefix(pattern) {
545
584
  const normalized = pattern.replace(/\\/g, '/');
@@ -646,14 +685,17 @@ export function checkReadonly(toolName, input, projectPath, context) {
646
685
  if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
647
686
  return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
648
687
  }
649
- if (paths.some(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions))) {
650
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
688
+ const directHPath = paths.find(filePath => containsHClassReference(filePath) || isHClassPath(filePath, pathOptions));
689
+ if (directHPath) {
690
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: resolveProtectedCandidate(directHPath, projectPath) };
651
691
  }
652
- if (protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
653
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
692
+ const hPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
693
+ if (hPath) {
694
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: hPath };
654
695
  }
655
- if (protectedReadToolMatchesLClass(toolName, input, pathOptions)) {
656
- return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
696
+ const lPath = protectedReadToolMatchesLClass(toolName, input, pathOptions);
697
+ if (lPath) {
698
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: lPath };
657
699
  }
658
700
  return { behavior: 'allow' };
659
701
  }
@@ -691,11 +733,15 @@ export function checkReadonly(toolName, input, projectPath, context) {
691
733
  }
692
734
  if (protectedOperands.hClass) {
693
735
  logger.warn(`[ReadonlyCheck] 🔒 H-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
694
- return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read' };
736
+ if (protectedOperands.analysis.kind === 'proven-readonly') {
737
+ return { behavior: 'deny', message: '🔒 只读模式:不允许读取 H 类受保护路径', policyCode: 'readonly_h_class_read', matchedPath: protectedOperands.hClassPath };
738
+ }
695
739
  }
696
740
  if (protectedOperands.lClass) {
697
741
  logger.warn(`[ReadonlyCheck] 🔒 L-class path blocked in readonly shell: cmd="${cmd}" session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
698
- return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read' };
742
+ if (protectedOperands.analysis.kind === 'proven-readonly') {
743
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', policyCode: 'readonly_l_class_read', matchedPath: protectedOperands.lClassPath };
744
+ }
699
745
  }
700
746
  if (protectedOperands.analysis.kind === 'proven-readonly') {
701
747
  return { behavior: 'deny', message: '🔒 只读模式:Shell 查询范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
@@ -967,6 +1013,7 @@ export function checkHClassWrite(toolName, input, context) {
967
1013
  return {
968
1014
  behavior: 'deny',
969
1015
  message: '🔒 临时文件必须位于当前会话的 $TMPDIR 内',
1016
+ matchedPath: tempCheck.paths[0],
970
1017
  };
971
1018
  }
972
1019
  // A managed temp write is intentionally allowed to contain names such as
@@ -992,6 +1039,7 @@ export function checkHClassWrite(toolName, input, context) {
992
1039
  return {
993
1040
  behavior: 'deny',
994
1041
  message: '🔒 Shell 命令涉及受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
1042
+ matchedPath: protectedOperands.hClassPath,
995
1043
  };
996
1044
  }
997
1045
  collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
@@ -1010,14 +1058,17 @@ export function checkHClassWrite(toolName, input, context) {
1010
1058
  // filesystem operation is checked again when it reaches Read/Write/Bash,
1011
1059
  // FileChange, or PermissionGrant above.
1012
1060
  const pathOptions = { cwd: context?.projectPath, root: context?.root };
1013
- if (['Read', 'Glob', 'Grep'].includes(toolName)
1014
- && protectedReadToolMatchesHClass(toolName, input, pathOptions)) {
1015
- logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
1016
- `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
1017
- return {
1018
- behavior: 'deny',
1019
- message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
1020
- };
1061
+ if (['Read', 'Glob', 'Grep'].includes(toolName)) {
1062
+ const matchedReadPath = protectedReadToolMatchesHClass(toolName, input, pathOptions);
1063
+ if (matchedReadPath) {
1064
+ logger.warn(`[H-Class Protection] 🔒 Protected filesystem read scope blocked: tool=${toolName} ` +
1065
+ `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
1066
+ return {
1067
+ behavior: 'deny',
1068
+ message: '🔒 文件读取范围包含受保护的 H 类配置/证书/快照路径,agent 不可直接操作',
1069
+ matchedPath: matchedReadPath,
1070
+ };
1071
+ }
1021
1072
  }
1022
1073
  if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
1023
1074
  logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
@@ -1036,6 +1087,7 @@ export function checkHClassWrite(toolName, input, context) {
1036
1087
  return {
1037
1088
  behavior: 'deny',
1038
1089
  message: '🔒 请求的文件系统权限范围包含受保护的 H 类路径,已拒绝授权',
1090
+ matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
1039
1091
  };
1040
1092
  }
1041
1093
  }
@@ -1059,6 +1111,7 @@ export function checkHClassWrite(toolName, input, context) {
1059
1111
  return {
1060
1112
  behavior: 'deny',
1061
1113
  message: '🔒 请求的 project_roots 权限范围包含受保护的 H 类路径,已拒绝授权',
1114
+ matchedPath: resolveProtectedCandidate(candidate, permissionWorkspace),
1062
1115
  };
1063
1116
  }
1064
1117
  }
@@ -1093,7 +1146,8 @@ export function checkHClassWrite(toolName, input, context) {
1093
1146
  behavior: 'deny',
1094
1147
  message: `🔒 此文件受保护,agent 不可直接写入\n\n` +
1095
1148
  `文件:${filePath}\n` +
1096
- `类型:配置/快照/证书等系统关键文件${suggestion}`
1149
+ `类型:配置/快照/证书等系统关键文件${suggestion}`,
1150
+ matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
1097
1151
  };
1098
1152
  }
1099
1153
  }
@@ -1108,7 +1162,7 @@ export function checkLClassWrite(toolName, input, context) {
1108
1162
  const pathOptions = { cwd: context?.projectPath, root: context?.root };
1109
1163
  const lClassRead = protectedReadToolMatchesLClass(toolName, input, pathOptions);
1110
1164
  if (lClassRead && context?.permissionMode === 'readonly') {
1111
- return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径' };
1165
+ return { behavior: 'deny', message: '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径', matchedPath: lClassRead };
1112
1166
  }
1113
1167
  return { behavior: 'allow' };
1114
1168
  }
@@ -1149,6 +1203,7 @@ export function checkLClassWrite(toolName, input, context) {
1149
1203
  message: context?.permissionMode === 'readonly'
1150
1204
  ? '🔒 只读模式:visitor 最低权限不允许读取 L-class 路径'
1151
1205
  : '🔒 L-class 路径允许读取,但当前 Bash 复合命令无法证明为只读,已拒绝执行。请改用受控的 EvolCore 读取命令或白名单只读命令',
1206
+ matchedPath: protectedOperands.lClassPath,
1152
1207
  };
1153
1208
  }
1154
1209
  collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
@@ -1167,6 +1222,7 @@ export function checkLClassWrite(toolName, input, context) {
1167
1222
  return {
1168
1223
  behavior: 'deny',
1169
1224
  message: '🔒 请求的文件系统写权限包含 L-class 只读路径,已拒绝授权',
1225
+ matchedPath: resolveProtectedCandidate(grantPath, pathOptions.cwd),
1170
1226
  };
1171
1227
  }
1172
1228
  }
@@ -1188,6 +1244,7 @@ export function checkLClassWrite(toolName, input, context) {
1188
1244
  return {
1189
1245
  behavior: 'deny',
1190
1246
  message: `🔒 L-class 路径允许读取但禁止 Agent 直接修改、删除或移动\n\n文件:${filePath}\n\n💡 请使用对应的 EvolCore 受控命令修改`,
1247
+ matchedPath: resolveProtectedCandidate(filePath, pathOptions.cwd),
1191
1248
  };
1192
1249
  }
1193
1250
  return { behavior: 'allow' };
@@ -1352,7 +1409,8 @@ export function evaluateToolPreflight(toolName, input, context) {
1352
1409
  ? input.commandArgv
1353
1410
  : undefined;
1354
1411
  const outerArgv = explicitCommandArgv ?? parseLiteralShellArgv(command) ?? undefined;
1355
- const wrappedCommand = outerArgv ? unwrapCodexShellCommandArgv(outerArgv) : undefined;
1412
+ const shellCarrier = outerArgv ? resolveCodexShellCarrierArgv(outerArgv) : undefined;
1413
+ const wrappedCommand = shellCarrier?.command;
1356
1414
  const policyCommand = wrappedCommand ?? command;
1357
1415
  if (context.sessionId && /^\s*(?:command\s+-v|which|type)\s+ec\s*$/i.test(policyCommand)) {
1358
1416
  return {
@@ -1397,7 +1455,9 @@ export function evaluateToolPreflight(toolName, input, context) {
1397
1455
  policyCode: 'managed_ec_aid_scope_forbidden',
1398
1456
  };
1399
1457
  }
1400
- const ecCommand = classifyEvolcoreShellCommand(policyCommand);
1458
+ const ecCommand = classifyEvolcoreShellCommand(policyCommand, shellCarrier
1459
+ ? { dialect: shellCarrier.dialect }
1460
+ : {});
1401
1461
  if (ecCommand.kind === 'literal') {
1402
1462
  return { behavior: 'allow', input, reason: 'ec-command' };
1403
1463
  }
@@ -1406,25 +1466,34 @@ export function evaluateToolPreflight(toolName, input, context) {
1406
1466
  }
1407
1467
  if (ecCommand.kind === 'composite') {
1408
1468
  if (ecCommand.issue === 'unsafe-expansion') {
1469
+ const message = shellCarrier?.dialect === 'powershell'
1470
+ ? '🔒 EC 双引号正文包含未转义的 PowerShell 展开;纯文字请优先使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。PowerShell 传给 ec 时会还原为原文字面量'
1471
+ : '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量';
1409
1472
  return {
1410
1473
  behavior: 'deny',
1411
1474
  input,
1412
- message: '🔒 EC 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 ec 时会还原为原文字面量',
1475
+ message,
1413
1476
  policyCode: 'ec_shell_unsafe_expansion',
1414
1477
  };
1415
1478
  }
1416
1479
  if (ecCommand.issue === 'invalid-quote') {
1480
+ const message = shellCarrier?.dialect === 'powershell'
1481
+ ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
1482
+ : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
1417
1483
  return {
1418
1484
  behavior: 'deny',
1419
1485
  input,
1420
- message: '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"',
1486
+ message,
1421
1487
  policyCode: 'ec_shell_invalid_quote',
1422
1488
  };
1423
1489
  }
1490
+ const message = shellCarrier?.dialect === 'powershell'
1491
+ ? '🔒 EC 命令调用被拒绝:一次 PowerShell/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要拼接探测命令,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开或子表达式追加其它命令。'
1492
+ : '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。';
1424
1493
  return {
1425
1494
  behavior: 'deny',
1426
1495
  input,
1427
- message: '🔒 EC 命令调用被拒绝:一次 Bash/exec_command 只能执行一条完整的 `ec` 命令。请直接调用 `ec ...`,不要先用 `command -v`/`ec aid` 探测,也不要使用 `&&`、`||`、`;`、`|`、重定向、变量展开、子 shell、`sh -c` 或 `bash -lc` 拼接其它命令。',
1496
+ message,
1428
1497
  policyCode: 'ec_shell_composite_command',
1429
1498
  };
1430
1499
  }
@@ -1460,7 +1529,7 @@ export function evaluateToolPreflight(toolName, input, context) {
1460
1529
  allowProtectedMetadata: context.allowProtectedMetadata,
1461
1530
  });
1462
1531
  if (hClass.behavior === 'deny') {
1463
- return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection' };
1532
+ return { behavior: 'deny', input: checkedInput, message: hClass.message, policyCode: 'h_class_protection', matchedPath: hClass.matchedPath };
1464
1533
  }
1465
1534
  const lClass = checkLClassWrite(toolName, checkedInput, {
1466
1535
  sessionId: context.sessionId,
@@ -1474,7 +1543,7 @@ export function evaluateToolPreflight(toolName, input, context) {
1474
1543
  managedTempDir: context.managedTempDir,
1475
1544
  });
1476
1545
  if (lClass.behavior === 'deny') {
1477
- return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection' };
1546
+ return { behavior: 'deny', input: checkedInput, message: lClass.message, policyCode: 'l_class_protection', matchedPath: lClass.matchedPath };
1478
1547
  }
1479
1548
  const dangerous = checkDangerousCommand(toolName, checkedInput);
1480
1549
  if (dangerous.isDangerous
@@ -151,6 +151,24 @@ export class PeerIdentityCache {
151
151
  * @param forceRefresh 强制刷新(忽略缓存时效)
152
152
  */
153
153
  static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
154
+ // A malformed transport envelope must never turn into a relation key such
155
+ // as `aun#` or trigger an agent.md lookup for an empty AID. Keep the
156
+ // fallback in-memory so callers can safely render an unknown peer without
157
+ // creating a bogus relation directory.
158
+ if (typeof peerId !== 'string' || !peerId.trim()) {
159
+ logger.debug(`[PeerIdentityCache] Ignored empty peer id: channel=${channelType}`);
160
+ return {
161
+ aid: '',
162
+ type: 'unknown',
163
+ isAgent: true,
164
+ agentMdHash: '',
165
+ agentMdUpdatedAt: 0,
166
+ verifiedAt: 0,
167
+ lastCheckedAt: Date.now(),
168
+ source: 'unknown',
169
+ };
170
+ }
171
+ peerId = peerId.trim();
154
172
  // 1. 缓存检查
155
173
  if (!forceRefresh && !this.needsRefresh(channelType, peerId, agentDir)) {
156
174
  const cached = this.get(channelType, peerId, agentDir);
@@ -4,6 +4,7 @@ import { isEckSnapshotsEnabled } from '../config-store.js';
4
4
  import { eckDebugDir } from '../paths.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { clearRoleStoreCache } from '../config/role-store.js';
7
+ import { resolveAgentLifecycle } from '../config/lifecycle.js';
7
8
  import { loadManifest, loadManifestMeta, invalidateManifestCache, evaluateWhen, renderTemplate, renderLoopSection, resolvePathWithDiag, loadSectionFiles, loadChildTemplate, buildPathMappings, shortenPath, } from './manifest-engine.js';
8
9
  const DEFAULT_MANIFEST_FILE = 'eck_manifest.json';
9
10
  // ── Caches ──
@@ -58,7 +59,15 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
58
59
  const meta = loadManifestMeta(manifestFile);
59
60
  const fileParts = [];
60
61
  const fragmentParts = [];
61
- const pathMappings = buildPathMappings(ctx.vars);
62
+ // Legacy callers may omit lifecycle; preserve the same active default used
63
+ // by agent configuration normalization while leaving explicit invalid
64
+ // values untouched for fail-closed manifest conditions.
65
+ const resolvedLifecycle = resolveAgentLifecycle({ lifecycle: ctx.vars.lifecycle });
66
+ const vars = resolvedLifecycle === null
67
+ ? ctx.vars
68
+ : { ...ctx.vars, lifecycle: resolvedLifecycle };
69
+ const renderCtx = { ...ctx, vars };
70
+ const pathMappings = buildPathMappings(vars);
62
71
  const sessionCache = getSessionCache(ctx.sessionId);
63
72
  const diagnostics = [];
64
73
  // 总闸计数(跨所有段累计)
@@ -78,7 +87,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
78
87
  if (capReached) {
79
88
  diag.skippedByTotalCap = true;
80
89
  // 仅当该段本会命中时才计入"未加载"集合(enabled 且 when 通过)
81
- if (section.enabled !== false && evaluateWhen(section.when, ctx.vars)) {
90
+ if (section.enabled !== false && evaluateWhen(section.when, vars)) {
82
91
  diag.whenPassed = true;
83
92
  skippedByCap.push(section.id);
84
93
  }
@@ -90,21 +99,21 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
90
99
  diagnostics.push(diag);
91
100
  continue;
92
101
  }
93
- diag.whenPassed = evaluateWhen(section.when, ctx.vars);
102
+ diag.whenPassed = evaluateWhen(section.when, vars);
94
103
  if (!diag.whenPassed) {
95
104
  diag.resolveStatus = 'skipped-when';
96
105
  diagnostics.push(diag);
97
106
  continue;
98
107
  }
99
108
  if (rawPath) {
100
- const r = resolvePathWithDiag(rawPath, ctx.vars);
109
+ const r = resolvePathWithDiag(rawPath, vars);
101
110
  diag.resolvedPath = r.resolved;
102
111
  diag.resolveStatus = r.status;
103
112
  if (r.unresolvedTokens.length > 0)
104
113
  diag.unresolvedTokens = r.unresolvedTokens;
105
114
  }
106
115
  const overflowOut = {};
107
- const files = loadSectionFiles(section, ctx.vars, sessionCache, overflowOut);
116
+ const files = loadSectionFiles(section, vars, sessionCache, overflowOut);
108
117
  diag.fileCount = files.length;
109
118
  if (overflowOut.value)
110
119
  diag.dirOverflow = overflowOut.value;
@@ -124,7 +133,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
124
133
  capReached = true;
125
134
  break;
126
135
  }
127
- const content = renderSectionContent(section, rawContent, ctx.vars);
136
+ const content = renderSectionContent(section, rawContent, vars);
128
137
  if (!content.trim()) {
129
138
  diag.emptyContent = true;
130
139
  continue;
@@ -151,7 +160,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
151
160
  }
152
161
  diag.used = anyUsed;
153
162
  // 本段触发总闸但一个文件都没用上 → 也算"未加载"
154
- if (capReached && !anyUsed && evaluateWhen(section.when, ctx.vars)) {
163
+ if (capReached && !anyUsed && evaluateWhen(section.when, vars)) {
155
164
  diag.skippedByTotalCap = true;
156
165
  diag.whenPassed = true;
157
166
  skippedByCap.push(section.id);
@@ -169,7 +178,7 @@ export function renderKitSections(ctx, manifestFile = DEFAULT_MANIFEST_FILE) {
169
178
  ? `<system-reminder>\nEvolCore Context Kit documents are shown below.\n\n${body}\n\nIMPORTANT: Use this context when it affects the current interaction.\n</system-reminder>`
170
179
  : '';
171
180
  const fragmentsOutput = fragmentParts.length > 0 ? fragmentParts.join('\n\n') : '';
172
- writeDebugFiles(ctx, output, fragmentsOutput, diagnostics);
181
+ writeDebugFiles(renderCtx, output, fragmentsOutput, diagnostics);
173
182
  return output;
174
183
  }
175
184
  export function cleanEckDebug() {