evolcore 0.0.33 → 0.0.35-test.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +38 -4
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +93 -14
  6. package/dist/agents/codex/diagnostics.js +79 -22
  7. package/dist/agents/codex-runner.js +245 -47
  8. package/dist/agents/ecagent-runner.js +90 -27
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/agentmd.js +13 -2
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/identity.js +1 -1
  13. package/dist/aun/msg/managed-operation.js +16 -0
  14. package/dist/aun/msg/p2p.js +18 -2
  15. package/dist/channels/aun.js +123 -7
  16. package/dist/channels/daemon.js +6 -3
  17. package/dist/cli/aun-commands.js +39 -14
  18. package/dist/cli/ctl-command.js +3 -1
  19. package/dist/cli/daemon-commands.js +130 -23
  20. package/dist/cli/fs-command.js +2 -1
  21. package/dist/cli/getmeta-command.js +211 -0
  22. package/dist/cli/index.js +26 -6
  23. package/dist/cli/integrity-command.js +196 -0
  24. package/dist/cli/managed-command-guard.js +3 -0
  25. package/dist/cli/memory-command.js +200 -156
  26. package/dist/cli/thread-command.js +17 -7
  27. package/dist/cli/trigger-command.js +6 -1
  28. package/dist/config/config-field-policy.js +4 -0
  29. package/dist/config/config-manager.js +19 -0
  30. package/dist/config/contact-book-store.js +5 -0
  31. package/dist/config/resolved-config-op.js +25 -8
  32. package/dist/config/role-service.js +13 -0
  33. package/dist/config/runtime-env.js +16 -0
  34. package/dist/config-store.js +30 -0
  35. package/dist/core/audit/log-integrity.js +3 -1
  36. package/dist/core/auth/agent-delegation.js +108 -35
  37. package/dist/core/auth/delegation-diagnostics.js +414 -0
  38. package/dist/core/auth/operation-authorizer.js +22 -3
  39. package/dist/core/auth/operation-catalog.js +16 -0
  40. package/dist/core/channel-runtime.js +1 -1
  41. package/dist/core/command/agent-control.js +18 -26
  42. package/dist/core/command/command-handler.js +145 -5
  43. package/dist/core/command/daemon-diagnostics.js +1 -0
  44. package/dist/core/command/menu-catalog.js +3 -0
  45. package/dist/core/command/menu-handler.js +24 -7
  46. package/dist/core/command/menu-protocol.js +1 -0
  47. package/dist/core/command/slash-handler.js +2 -0
  48. package/dist/core/command/topic-slash.js +4 -7
  49. package/dist/core/daemon-bootstrap.js +4 -7
  50. package/dist/core/event-catalog.js +14 -0
  51. package/dist/core/file-integrity/read.js +61 -0
  52. package/dist/core/file-integrity/store.js +262 -0
  53. package/dist/core/ipc-registration.js +47 -2
  54. package/dist/core/memory/batch-meta.js +133 -0
  55. package/dist/core/memory/consolidation-context.js +132 -28
  56. package/dist/core/memory/consolidation-ops-records.js +14 -16
  57. package/dist/core/memory/consolidation-trigger.js +15 -7
  58. package/dist/core/memory/consolidation.js +146 -26
  59. package/dist/core/memory/index-manager.js +19 -28
  60. package/dist/core/memory/memory-admin.js +19 -14
  61. package/dist/core/memory/memory-document.js +53 -9
  62. package/dist/core/memory/memory-writer.js +50 -19
  63. package/dist/core/message/aun-ipc-sender.js +19 -1
  64. package/dist/core/message/im-renderer.js +20 -4
  65. package/dist/core/message/message-bridge.js +5 -4
  66. package/dist/core/message/message-log.js +1 -0
  67. package/dist/core/message/peer-mode.js +17 -0
  68. package/dist/core/message/response-engine.js +598 -50
  69. package/dist/core/permission/ec-command-parser.js +169 -44
  70. package/dist/core/permission/protected-paths.js +5 -1
  71. package/dist/core/permission/readonly-shell-query.js +43 -5
  72. package/dist/core/permission/session-path-grants.js +334 -0
  73. package/dist/core/permission/tool-canonical.js +226 -0
  74. package/dist/core/permission/tool-policy.js +60 -62
  75. package/dist/core/relation/peer-identity.js +87 -12
  76. package/dist/core/role/runtime-policy.js +21 -10
  77. package/dist/core/session/session-fs-store.js +123 -8
  78. package/dist/core/session/session-manager.js +131 -6
  79. package/dist/core/session/session-title.js +26 -1
  80. package/dist/core/skill/index-generator.js +17 -3
  81. package/dist/core/system-session/grants.js +40 -0
  82. package/dist/core/system-session/memory.js +289 -0
  83. package/dist/core/system-session/registry.js +74 -0
  84. package/dist/core/system-session/types.js +14 -0
  85. package/dist/core/thread/command-parser.js +6 -1
  86. package/dist/core/thread/ipc-operation.js +33 -16
  87. package/dist/core/thread/thread-operation.js +62 -24
  88. package/dist/core/thread/thread-service.js +258 -51
  89. package/dist/eck/kit-renderer.js +7 -2
  90. package/dist/eck/manifest-engine.js +24 -4
  91. package/dist/index.js +11 -2
  92. package/dist/ipc.js +51 -40
  93. package/dist/paths.js +70 -0
  94. package/dist/stats/billing.js +20 -8
  95. package/dist/trigger/feedback.js +6 -1
  96. package/dist/trigger/parser.js +8 -1
  97. package/dist/trigger/system-definitions.js +3 -3
  98. package/dist/utils/cross-platform.js +17 -5
  99. package/dist/utils/error-dict.json +18 -4
  100. package/dist/utils/error-utils.js +121 -0
  101. package/dist/utils/instance-registry.js +13 -8
  102. package/dist/utils/log-writer.js +12 -0
  103. package/dist/utils/process-introspect.js +11 -0
  104. package/kits/docs/context-assembly.md +0 -1
  105. package/kits/docs/evolcore/agent-create.md +5 -4
  106. package/kits/docs/evolcore/agent.md +2 -2
  107. package/kits/docs/evolcore/config.md +2 -2
  108. package/kits/docs/evolcore/ctl.md +2 -2
  109. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  110. package/kits/docs/evolcore/fs-architecture.md +9 -10
  111. package/kits/docs/evolcore/fs.md +2 -2
  112. package/kits/docs/evolcore/group-collaboration.md +3 -3
  113. package/kits/docs/evolcore/group.md +14 -12
  114. package/kits/docs/evolcore/memory-architecture.md +4 -5
  115. package/kits/docs/evolcore/memory-consolidation.md +19 -16
  116. package/kits/docs/evolcore/memory-tags.md +3 -1
  117. package/kits/docs/evolcore/memory.md +24 -12
  118. package/kits/docs/evolcore/model.md +16 -0
  119. package/kits/docs/evolcore/msg.md +7 -7
  120. package/kits/docs/evolcore/stats.md +1 -1
  121. package/kits/docs/evolcore/thread-chain.md +30 -14
  122. package/kits/docs/prompt-loading-architecture.md +0 -1
  123. package/kits/eck_manifest.auxiliary.json +9 -0
  124. package/kits/eck_manifest.json +20 -20
  125. package/kits/eck_manifest.memory.json +20 -12
  126. package/kits/rules/01-overview.md +11 -0
  127. package/kits/rules/03-identity.md +6 -3
  128. package/kits/rules/06-channel.md +1 -1
  129. package/kits/rules/07-memory-and-thread.md +13 -16
  130. package/kits/schemas/_meta.json +7 -1
  131. package/kits/schemas/agent-config.schema.13.json +1 -1
  132. package/kits/schemas/daemon.schema.7.json +140 -0
  133. package/kits/skills/ec-group/SKILL.md +8 -5
  134. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  135. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  136. package/kits/skills/thread-ops/SKILL.md +18 -18
  137. package/kits/templates/memory-consolidation/forget.md +2 -2
  138. package/kits/templates/memory-consolidation/generalize.md +4 -4
  139. package/kits/templates/memory-consolidation/notes.md +12 -12
  140. package/kits/templates/memory-consolidation/profile.md +3 -3
  141. package/kits/templates/memory-consolidation/promote.md +3 -3
  142. package/kits/templates/memory-consolidation/workflow.md +42 -7
  143. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  144. package/kits/templates/system-fragments/bash-description.md +9 -0
  145. package/kits/templates/system-fragments/bootstrap.md +4 -3
  146. package/kits/templates/system-fragments/channel.md +2 -2
  147. package/kits/templates/system-fragments/memory.md +16 -9
  148. package/kits/templates/system-fragments/reply.md +6 -6
  149. package/kits/templates/system-fragments/session.md +1 -1
  150. package/package.json +4 -3
  151. package/dist/core/memory/memory-session-policy.js +0 -41
  152. package/dist/core/memory/memory-signature.js +0 -257
@@ -10,7 +10,7 @@ import { DEFAULT_PERMISSION_MODE } from '../types.js';
10
10
  import { agentSessionsDir, resolveRoot } from '../paths.js';
11
11
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
12
12
  import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
13
- import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
13
+ import { checkBashDescription, checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
14
14
  import { logger } from '../utils/logger.js';
15
15
  import { ErrorType } from '../utils/error-utils.js';
16
16
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
@@ -18,6 +18,7 @@ import { auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
18
18
  import { resolveEcagentConfig } from './baseagent.js';
19
19
  import { buildModelRequestHeaders } from './request-identity.js';
20
20
  import { appendRequestPath } from './request-identity.js';
21
+ import { resolveModelSpec } from '../stats/billing.js';
21
22
  import { getManagedTaskTempDir } from '../cli/task-context.js';
22
23
  import { unsettledInterruptError, withInterruptDeadline, hasTrustedFullAccessContext } from './runner-types.js';
23
24
  const PROVIDER_ID = 'evolcore-gateway';
@@ -436,18 +437,23 @@ async function consumeOpenAiStream(output, body, model) {
436
437
  throw new Error('[network] Gateway stream ended without a terminal event');
437
438
  }
438
439
  function createModel(id, baseUrl) {
439
- const isClaude = id.toLowerCase().startsWith('claude-');
440
+ const modelId = stripContextWindowSuffix(id);
441
+ const isClaude = modelId.toLowerCase().startsWith('claude-');
442
+ const fallback = isClaude
443
+ ? { context_window: 200_000, max_output_tokens: 8_192 }
444
+ : { context_window: 128_000, max_output_tokens: 16_384 };
445
+ const spec = resolveModelSpec(resolveRoot(), id, undefined, fallback);
440
446
  return {
441
- id,
442
- name: id,
447
+ id: modelId,
448
+ name: modelId,
443
449
  api: 'openai-completions',
444
450
  provider: PROVIDER_ID,
445
451
  baseUrl,
446
452
  reasoning: true,
447
453
  input: ['text', 'image'],
448
454
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
449
- contextWindow: isClaude ? 200_000 : 128_000,
450
- maxTokens: isClaude ? 8_192 : 16_384,
455
+ contextWindow: spec.context_window,
456
+ maxTokens: spec.max_output_tokens,
451
457
  compat: {
452
458
  maxTokensField: 'max_tokens',
453
459
  supportsUsageInStreaming: true,
@@ -464,7 +470,11 @@ function isWithin(root, candidate) {
464
470
  const relative = path.relative(root, candidate);
465
471
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
466
472
  }
467
- export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
473
+ /** 路径是否落在任一显式声明的根之下(含根本身)。 */
474
+ function isWithinAny(candidate, roots) {
475
+ return roots.some(root => isWithin(path.resolve(root), candidate));
476
+ }
477
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false, personalMdPath, sessionGrants) {
468
478
  const root = fs.realpathSync(projectPath);
469
479
  const resolved = path.resolve(root, candidate || '.');
470
480
  const allowedBootstrapFile = bootstrapAgentMdPath
@@ -472,7 +482,15 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
472
482
  : undefined;
473
483
  const allowedPersonalFile = personalMdPath ? path.resolve(personalMdPath) : undefined;
474
484
  const isManagedIdentityFile = resolved === allowedBootstrapFile || resolved === allowedPersonalFile;
475
- if (!fullaccess && !isWithin(root, resolved) && !isManagedIdentityFile) {
485
+ // 系统会话:模块声明的可读根整体放行,可写文件逐个精确匹配(不含目录)。
486
+ // 声明之外、项目之外的路径仍然拒绝。声明里的路径可能在多个根之间借符号链接互串,
487
+ // 所以放行前统一按真实路径复核,确认真实位置仍落在同一个被授予的边界内。
488
+ // 写只看可写文件(逐个精确匹配),绝不由可读根推导;读只看可读根。
489
+ const sessionWrites = forWrite && (sessionGrants?.writeFiles ?? [])
490
+ .some(file => path.resolve(file) === resolved);
491
+ const sessionReads = !forWrite && isWithinAny(resolved, sessionGrants?.readRoots ?? []);
492
+ const isSessionTarget = sessionReads || sessionWrites;
493
+ if (!fullaccess && !isWithin(root, resolved) && !isManagedIdentityFile && !isSessionTarget) {
476
494
  throw new Error(`Path is outside the project: ${candidate}`);
477
495
  }
478
496
  if (isManagedIdentityFile) {
@@ -494,7 +512,34 @@ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false,
494
512
  while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
495
513
  cursor = path.dirname(cursor);
496
514
  const canonical = fs.realpathSync(cursor);
497
- if (!fullaccess && !isWithin(root, canonical))
515
+ if (fullaccess)
516
+ return resolved;
517
+ // 系统会话的写入目标与判定层一致:必须是普通文件,链接一律拒绝。
518
+ // 链接会让 resolved 与真实位置分离,放行等于把洞开到声明之外。
519
+ if (sessionWrites && fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
520
+ throw new Error(`Refusing to write through a symbolic link: ${candidate}`);
521
+ }
522
+ // 可写文件尚未创建时,以父目录的真实路径为准;已存在时以文件自身的真实路径为准。
523
+ const canonicalSelf = fs.existsSync(resolved) ? fs.realpathSync(resolved) : canonical;
524
+ if (isSessionTarget) {
525
+ const grantedRoots = [
526
+ ...(sessionReads ? sessionGrants?.readRoots ?? [] : []),
527
+ ...(sessionWrites ? (sessionGrants?.writeFiles ?? []).filter(file => path.resolve(file) === resolved) : []),
528
+ ];
529
+ const canonicalRoots = grantedRoots.flatMap(granted => {
530
+ const inside = fs.existsSync(granted) ? [fs.realpathSync(granted)] : [];
531
+ // 根自身可能尚未创建,或者位于尚未创建的上层目录中;先归到存在的祖先再比。
532
+ let ancestor = granted;
533
+ while (!fs.existsSync(ancestor) && ancestor !== path.dirname(ancestor))
534
+ ancestor = path.dirname(ancestor);
535
+ return [...inside, fs.realpathSync(ancestor)];
536
+ });
537
+ if (!canonicalRoots.some(granted => isWithin(granted, canonical) && isWithin(granted, canonicalSelf))) {
538
+ throw new Error(`Path resolves outside its declared grant: ${candidate}`);
539
+ }
540
+ return resolved;
541
+ }
542
+ if (!isWithin(root, canonical))
498
543
  throw new Error(`Path resolves outside the project: ${candidate}`);
499
544
  return resolved;
500
545
  }
@@ -547,7 +592,7 @@ async function walkFiles(root, signal) {
547
592
  function textResult(text, details = {}) {
548
593
  return { content: [{ type: 'text', text: truncate(text) }], details };
549
594
  }
550
- function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
595
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false, personalMdPath, sessionGrants) {
551
596
  const read = {
552
597
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
553
598
  parameters: Type.Object({
@@ -556,7 +601,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
556
601
  limit: Type.Optional(Type.Number({ minimum: 1 })),
557
602
  }),
558
603
  async execute(_id, params, signal) {
559
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
604
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
560
605
  const stat = await fs.promises.stat(file);
561
606
  if (stat.size > MAX_READ_BYTES)
562
607
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -573,7 +618,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
573
618
  name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
574
619
  parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
575
620
  async execute(_id, params, signal) {
576
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
621
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess, undefined, sessionGrants);
577
622
  const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
578
623
  const files = (await walkFiles(root, signal))
579
624
  .map(file => path.relative(root, file).replaceAll(path.sep, '/'))
@@ -589,7 +634,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
589
634
  case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
590
635
  }),
591
636
  async execute(_id, params, signal) {
592
- const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess);
637
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.', false, undefined, fullaccess, undefined, sessionGrants);
593
638
  const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
594
639
  const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
595
640
  const flags = params.case_insensitive ? 'i' : '';
@@ -622,7 +667,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
622
667
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
623
668
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
624
669
  async execute(_id, params, signal) {
625
- const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath);
670
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
626
671
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
627
672
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
628
673
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -635,7 +680,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
635
680
  replace_all: Type.Optional(Type.Boolean()),
636
681
  }), executionMode: 'sequential',
637
682
  async execute(_id, params, signal) {
638
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
683
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath, sessionGrants);
639
684
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
640
685
  if (!params.old_string)
641
686
  throw new Error('old_string must not be empty');
@@ -655,7 +700,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
655
700
  name: 'Bash', label: 'Bash', description: 'Run a shell command in the project.',
656
701
  parameters: Type.Object({
657
702
  command: Type.String(), timeout: Type.Optional(Type.Number({ minimum: 1, maximum: 600_000 })),
658
- description: Type.Optional(Type.String()),
703
+ description: Type.Optional(Type.String({ description: '<命令目标描述> [<风险等级>],风险等级为 低/中/高 之一,例如 "查看工作区未提交的改动 [低]"。缺失或格式不符时调用被拒。' })),
659
704
  }), executionMode: 'sequential',
660
705
  async execute(_id, params, signal) {
661
706
  const result = await env.exec(params.command, {
@@ -724,7 +769,7 @@ export class EcagentRunner {
724
769
  configuredHeaders: config.headers,
725
770
  }),
726
771
  };
727
- this.model = stripContextWindowSuffix(config.model);
772
+ this.model = config.model;
728
773
  this.effort = config.effort;
729
774
  this.agentAid = runtime?.agentAid;
730
775
  this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
@@ -734,8 +779,8 @@ export class EcagentRunner {
734
779
  throw new Error('ecagent requires an EvolCore Agent AID');
735
780
  return agentSessionsDir(this.agentAid);
736
781
  }
737
- setModel(model) { this.model = stripContextWindowSuffix(model); }
738
- getModel() { return this.model; }
782
+ setModel(model) { this.model = model; }
783
+ getModel() { return stripContextWindowSuffix(this.model); }
739
784
  resolveModelId(model) { return model; }
740
785
  setEffort(effort) { this.effort = effort; }
741
786
  getEffort() { return this.effort; }
@@ -885,7 +930,7 @@ export class EcagentRunner {
885
930
  models,
886
931
  model,
887
932
  thinkingLevel: normalizeThinkingLevel(effort),
888
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess, request.modelOverride?.personalMdPath),
933
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess, request.modelOverride?.personalMdPath, this.permissionContexts.get(request.sessionId)?.systemSessionGrants),
889
934
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
890
935
  streamOptions: {
891
936
  timeoutMs: this.config.timeoutMs,
@@ -1169,11 +1214,6 @@ export class EcagentRunner {
1169
1214
  }
1170
1215
  async authorizeTool(sessionId, projectPath, mode, toolName, input, turn) {
1171
1216
  const permissionContext = this.permissionContexts.get(sessionId);
1172
- if (mode === 'fullaccess') {
1173
- return hasTrustedFullAccessContext(permissionContext)
1174
- ? {}
1175
- : { block: true, reason: 'Untrusted fullaccess execution context' };
1176
- }
1177
1217
  const recordBlockedOperation = async (policyCode, operationInput = input, matchedPath) => {
1178
1218
  const summary = summarizeToolInputForAudit(toolName, operationInput).slice(0, 512);
1179
1219
  auditToolPreflightDenial({
@@ -1212,6 +1252,29 @@ export class EcagentRunner {
1212
1252
  logger.warn(`[EcagentRunner] failed to record blocked operation: session=${sessionId} tool=${toolName} code=${policyCode} error=${error instanceof Error ? error.message : String(error)}`);
1213
1253
  }
1214
1254
  };
1255
+ // Bash description 格式约定先于一切放行分支(含 fullaccess 与系统会话)。
1256
+ // TODO(bash-description): 与 claude-runner 同因——该约束缺少验证,先短路放行;
1257
+ // 恢复强制时把两个 runner 的开关一起打开。
1258
+ if (process.env.EVOLCORE_BASH_DESCRIPTION_ENFORCE === '1') {
1259
+ const descriptionCheck = checkBashDescription(toolName, input);
1260
+ if (descriptionCheck) {
1261
+ await recordBlockedOperation(descriptionCheck.policyCode);
1262
+ return { block: true, reason: descriptionCheck.message };
1263
+ }
1264
+ }
1265
+ if (mode === 'fullaccess') {
1266
+ return hasTrustedFullAccessContext(permissionContext)
1267
+ ? {}
1268
+ : { block: true, reason: 'Untrusted fullaccess execution context' };
1269
+ }
1270
+ // 系统会话:模块判定即最终结论,不进入 preflight / permissionMode 分支
1271
+ if (permissionContext?.systemSessionCheck) {
1272
+ const decision = permissionContext.systemSessionCheck(toolName, input);
1273
+ if (decision.allow)
1274
+ return {};
1275
+ await recordBlockedOperation(decision.code);
1276
+ return { block: true, reason: decision.reason };
1277
+ }
1215
1278
  const policy = permissionContext?.policyHook?.(toolName, input, { turn });
1216
1279
  if (policy?.block) {
1217
1280
  await recordBlockedOperation(policy.policyCode ?? 'session_policy_hook');
@@ -1239,7 +1302,7 @@ export class EcagentRunner {
1239
1302
  const checkedInput = preflight.input;
1240
1303
  if (mode === 'readonly') {
1241
1304
  const decision = checkReadonly(toolName, checkedInput, projectPath, {
1242
- sessionId, managedTempDir: permissionContext?.managedTempDir, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1305
+ sessionId, managedTempDir: permissionContext?.managedTempDir, grantedReadRoots: permissionContext?.grantedReadRoots, channel: permissionContext?.channel, peerId: permissionContext?.userId, role: permissionContext?.role,
1243
1306
  allowReadonlySourceDiagnostics: permissionContext?.allowReadonlySourceDiagnostics === true,
1244
1307
  });
1245
1308
  if (decision.behavior === 'deny') {
@@ -1351,7 +1414,7 @@ export class EcagentRunner {
1351
1414
  ? cached
1352
1415
  : await this.openExistingRuntime(agentSessionId, cwd);
1353
1416
  this.onCompactStart?.(sessionId);
1354
- const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
1417
+ const compactModel = modelOverride?.model || this.model;
1355
1418
  const compactEffort = modelOverride?.effortMode === 'model_default'
1356
1419
  ? undefined
1357
1420
  : modelOverride?.effort ?? this.effort;
@@ -136,10 +136,9 @@ export function isOneMillionContextModel(model) {
136
136
  return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => base === prefix || base.startsWith(`${prefix}-`));
137
137
  }
138
138
  /**
139
- * EvolCore's conservative local context-window catalog: 1M models = 1,000,000;
140
- * every other model falls back to 200,000 until provider metadata is available.
141
- * This is an operational guardrail, not a claim about an upstream provider's
142
- * advertised capacity.
139
+ * Hardcoded context-window fallback for models missing from the parameter table:
140
+ * known 1M models use 1,000,000; every other model falls back to 200,000.
141
+ * This is an operational guardrail, not a claim about advertised capacity.
143
142
  */
144
143
  export function realContextWindowForModel(model) {
145
144
  return isOneMillionContextModel(model) ? 1000000 : 200000;
@@ -159,8 +158,8 @@ export function autoCompactWindowForModel(_model) {
159
158
  * at the right point for small-context models (200k/128k), where EvolCore's
160
159
  * fixed 650k threshold would never trigger.
161
160
  */
162
- export function sdkAutoCompactWindowForModel(model) {
163
- const contextWindow = realContextWindowForModel(model);
161
+ export function sdkAutoCompactWindowForModel(model, resolvedContextWindow) {
162
+ const contextWindow = resolvedContextWindow ?? realContextWindowForModel(model);
164
163
  const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
165
164
  return Math.min(650_000, Math.max(1, contextWindow - reserve));
166
165
  }
@@ -179,8 +179,19 @@ export function parseAgentDisplayName(content) {
179
179
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
180
180
  if (!frontmatter)
181
181
  return undefined;
182
- const name = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m)?.[1]?.trim();
183
- return name || undefined;
182
+ try {
183
+ const document = parseDocument(frontmatter);
184
+ if (document.errors.length > 0)
185
+ return undefined;
186
+ const fields = document.toJS();
187
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields))
188
+ return undefined;
189
+ const name = fields.name;
190
+ return typeof name === 'string' ? name.trim() || undefined : undefined;
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
184
195
  }
185
196
  function displayNameKey(aid, aunPath) {
186
197
  const root = path.resolve(aunPath ?? resolveRoot());
@@ -39,7 +39,7 @@ export function suppressSdkLogs() {
39
39
  };
40
40
  }
41
41
  // ==================== SDK package ====================
42
- export const MIN_AUN_CORE_SDK = [0, 5, 17];
42
+ export const MIN_AUN_CORE_SDK = [0, 5, 20];
43
43
  export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
44
44
  function compareVersion(a, min) {
45
45
  const parts = a.split('.').map(n => parseInt(n, 10));
@@ -224,7 +224,7 @@ export async function aidCreate(aid, opts) {
224
224
  // 新注册流程:AIDStore.register → downloadCaRoot → load → authenticate
225
225
  const store = await getAidStore({ slotId: SLOT.cli, aunPath });
226
226
  try {
227
- const regResult = await store.register(aid);
227
+ const regResult = await store.register(aid, { type: 'agent' });
228
228
  if (!regResult.ok) {
229
229
  const e = new Error(regResult.error.message);
230
230
  e.code = regResult.error.code;
@@ -597,6 +597,9 @@ export class ManagedMsgOperationError extends Error {
597
597
  }
598
598
  const HISTORY_VALUE_FLAGS = new Set([
599
599
  '--session', '--limit', '--before', '--after', '--direction', '--format', '--channel-key', '--peer-id',
600
+ // 话题过滤是托管协作的常规用法(文档 `thread-chain.md` 明确写了
601
+ // `ec msg history --thread <id>`);只会收窄结果集,不扩大可读范围。
602
+ '--thread',
600
603
  ]);
601
604
  function failMsg(code, reason) {
602
605
  return { ok: false, code, reason };
@@ -720,6 +723,15 @@ export function resolveManagedMsgOperation(argv) {
720
723
  return failMsg('INVALID_ARGUMENT', '--after must be earlier than --before');
721
724
  }
722
725
  const target = channelKey === undefined ? String(positionals[0] || '').replace(/^@/, '') : undefined;
726
+ const threadId = values.get('--thread');
727
+ if (threadId !== undefined) {
728
+ if (kind !== 'history')
729
+ return failMsg('INVALID_ARGUMENT', '--thread is only supported for history');
730
+ if (channelKey !== undefined)
731
+ return failMsg('INVALID_ARGUMENT', '--thread cannot be combined with --channel-key/--peer-id');
732
+ if (!/^thread-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(threadId))
733
+ return failMsg('INVALID_ARGUMENT', 'invalid --thread id');
734
+ }
723
735
  const canonicalArgv = ['msg', kind, from];
724
736
  if (target !== undefined)
725
737
  canonicalArgv.push(target);
@@ -735,6 +747,8 @@ export function resolveManagedMsgOperation(argv) {
735
747
  canonicalArgv.push('--after', values.get('--after'));
736
748
  if (values.has('--direction'))
737
749
  canonicalArgv.push('--direction', values.get('--direction'));
750
+ if (threadId !== undefined)
751
+ canonicalArgv.push('--thread', threadId);
738
752
  return {
739
753
  ok: true,
740
754
  command: {
@@ -749,6 +763,7 @@ export function resolveManagedMsgOperation(argv) {
749
763
  before,
750
764
  after,
751
765
  direction,
766
+ ...(threadId !== undefined ? { threadId } : {}),
752
767
  },
753
768
  };
754
769
  }
@@ -766,6 +781,7 @@ export async function executeManagedMsgOperation(command) {
766
781
  before: command.before,
767
782
  after: command.after,
768
783
  direction: command.direction,
784
+ threadId: command.threadId,
769
785
  })
770
786
  : await msgOnline({
771
787
  from: command.from,
@@ -279,8 +279,24 @@ export async function msgSend(args) {
279
279
  catch { /* ignore */ }
280
280
  }
281
281
  // 2. 决定 chatmode(遵循来源1-3)
282
- // 私聊:非 human 对端 → proactive,human 对端 → interactive
283
- const chatmode = peerIdentity.isAgent ? 'proactive' : 'interactive';
282
+ // 私聊:非 human 对端 → proactive;人类对端只有 owner/admin 才 interactive,
283
+ // 其余(含角色未知)按 proactive 保守处理。判定逻辑集中在 peer-mode。
284
+ const { chatmodeForPeerIdentity } = await import('../../core/message/peer-mode.js');
285
+ const { resolvePeerRoleDetail } = await import('../../config/peer-role-resolver.js');
286
+ let peerRole = null;
287
+ try {
288
+ peerRole = resolvePeerRoleDetail({
289
+ selfAid: args.from,
290
+ channelKey: `aun#${args.from}#main`,
291
+ channelType: 'aun',
292
+ chatType: 'private',
293
+ actorId: args.to,
294
+ conversationId: args.to,
295
+ peerType: peerIdentity.type,
296
+ }).effectiveRole;
297
+ }
298
+ catch { /* 角色解析失败时按未知处理(proactive) */ }
299
+ const chatmode = chatmodeForPeerIdentity(peerIdentity, peerRole);
284
300
  // 3. 构建 payload
285
301
  if (!payload) {
286
302
  payload = buildSimplePayload(args.body);
@@ -557,6 +557,24 @@ function setIfDefined(target, key, value) {
557
557
  if (value !== undefined)
558
558
  target[key] = value;
559
559
  }
560
+ function truncateRawErrorForDisplay(raw) {
561
+ if (typeof raw !== 'string' || !raw.trim())
562
+ return undefined;
563
+ const text = raw.trim();
564
+ const htmlStart = text.toLowerCase().indexOf('<html');
565
+ return htmlStart >= 0 ? text.slice(0, htmlStart).trimEnd() : text;
566
+ }
567
+ function projectedRawError(raw, displayRaw) {
568
+ if (typeof raw !== 'string')
569
+ return raw;
570
+ return raw.trim() ? displayRaw : raw;
571
+ }
572
+ function appendRawErrorText(text, raw) {
573
+ const displayRaw = truncateRawErrorForDisplay(raw);
574
+ if (!displayRaw || text.includes(displayRaw))
575
+ return text;
576
+ return `${text}\n原因:${displayRaw}`;
577
+ }
560
578
  /**
561
579
  * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
562
580
  *
@@ -662,6 +680,8 @@ export class AUNChannel {
662
680
  traceWriter = null;
663
681
  eventBus = null;
664
682
  queuedHandler = null;
683
+ /** `topic-renamed` 消费器,由 runtime 注入(见 setThreadRenameHandler)。 */
684
+ threadRenameHandler = null;
665
685
  groupInviteHandler;
666
686
  groupInviteConnectedHandler;
667
687
  groupInviteDeliveryGate;
@@ -1391,6 +1411,44 @@ export class AUNChannel {
1391
1411
  return sanitized;
1392
1412
  return undefined;
1393
1413
  }
1414
+ /**
1415
+ * 处理对端 `topic-renamed` 通知:校验字段后交给注入的改名消费器落索引,
1416
+ * 成功后发 `thread:renamed` 让前端刷新。全程不进入 Agent 消息队列。
1417
+ * 通知是尽力而为的,任何失败只记日志,不影响后续消息处理。
1418
+ */
1419
+ applyInboundTopicRename(params) {
1420
+ const payload = params.payload && typeof params.payload === 'object' ? params.payload : null;
1421
+ const threadId = typeof payload?.threadId === 'string' ? payload.threadId : undefined;
1422
+ const name = this.sanitizeTopicName(payload?.topicName);
1423
+ const nameUpdatedAt = typeof payload?.nameUpdatedAt === 'number' && Number.isFinite(payload.nameUpdatedAt)
1424
+ ? payload.nameUpdatedAt : undefined;
1425
+ const applier = this.threadRenameHandler;
1426
+ if (!applier || !threadId || !name || nameUpdatedAt === undefined || !params.peer) {
1427
+ logger.debug(`${this.logPrefix()} topic-renamed ignored: peer=${params.peer || '<none>'} thread=${threadId || '<none>'} hasName=${!!name}`);
1428
+ return;
1429
+ }
1430
+ void applier({
1431
+ selfAid: params.selfAid, peer: params.peer, peerKind: params.peerKind,
1432
+ threadId, name, nameUpdatedAt,
1433
+ }).then(result => {
1434
+ if (!result.applied) {
1435
+ logger.debug(`${this.logPrefix()} topic-renamed not applied (${result.reason ?? 'unknown'}): thread=${threadId}`);
1436
+ return;
1437
+ }
1438
+ this.eventBus?.publish?.({
1439
+ type: 'thread:renamed',
1440
+ sessionId: result.sessionId,
1441
+ peer: params.peer,
1442
+ threadId,
1443
+ name,
1444
+ nameUpdatedAt,
1445
+ by: params.senderAid ?? '',
1446
+ });
1447
+ logger.info(`${this.logPrefix()} topic-renamed applied: thread=${threadId} name=${JSON.stringify(name)}`);
1448
+ }).catch(error => {
1449
+ logger.warn(`${this.logPrefix()} topic-renamed failed: ${error instanceof Error ? error.message : String(error)}`);
1450
+ });
1451
+ }
1394
1452
  buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
1395
1453
  if (!groupId)
1396
1454
  throw new Error('buildGroupReplyContext requires groupId');
@@ -2519,6 +2577,14 @@ export class AUNChannel {
2519
2577
  if (p2pPayloadType === 'text') {
2520
2578
  import('../utils/msg-diag.js').then(m => m.diagLog('aun_in', { aid: this.config.aid, from: fromAid, to: this.config.aid, msgId: messageId, text })).catch(() => { });
2521
2579
  }
2580
+ // topic-renamed:对端话题改名通知。只更新本端话题索引,不进 Agent 队列。
2581
+ if (p2pPayloadType === 'topic-renamed') {
2582
+ this.applyInboundTopicRename({
2583
+ payload, selfAid: this.config.aid, peer: fromAid, peerKind: 'private',
2584
+ senderAid: fromAid,
2585
+ });
2586
+ return;
2587
+ }
2522
2588
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2523
2589
  if (p2pPayloadType === 'action_card_reply') {
2524
2590
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
@@ -2650,6 +2716,14 @@ export class AUNChannel {
2650
2716
  logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
2651
2717
  return;
2652
2718
  }
2719
+ // topic-renamed:群内话题改名通知。只更新本端索引,不进 Agent 队列。
2720
+ if (inboundType === 'topic-renamed') {
2721
+ this.applyInboundTopicRename({
2722
+ payload, selfAid: this.config.aid, peer: groupId || senderAid, peerKind: 'group',
2723
+ senderAid,
2724
+ });
2725
+ return;
2726
+ }
2653
2727
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId);
2654
2728
  if (!payloadMentionEntries)
2655
2729
  return;
@@ -3760,6 +3834,13 @@ export class AUNChannel {
3760
3834
  }, delayMs);
3761
3835
  }
3762
3836
  // ── Public API (same interface as before) ───────────────────
3837
+ /**
3838
+ * 对端发来的话题改名通知(`topic-renamed`)。由 runtime 注入,交给
3839
+ * ThreadService.applyRemoteRename 落本地索引;不进入 Agent 消息队列。
3840
+ */
3841
+ setThreadRenameHandler(handler) {
3842
+ this.threadRenameHandler = handler;
3843
+ }
3763
3844
  setEventBus(bus) {
3764
3845
  // 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
3765
3846
  if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
@@ -4606,7 +4687,23 @@ export class AUNChannel {
4606
4687
  return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
4607
4688
  }
4608
4689
  const info = await this.fetchPeerInfo(args.to);
4609
- const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
4690
+ // 与 p2p 发送路径共用同一判定:非人类 → proactive;人类需 owner/admin 才 interactive。
4691
+ const { chatmodeForPeerIdentity } = await import('../core/message/peer-mode.js');
4692
+ let peerRole = null;
4693
+ try {
4694
+ const { resolvePeerRoleDetail } = await import('../config/peer-role-resolver.js');
4695
+ peerRole = resolvePeerRoleDetail({
4696
+ selfAid: this.config.aid,
4697
+ channelKey: `aun#${this.config.aid}#main`,
4698
+ channelType: 'aun',
4699
+ chatType: 'private',
4700
+ actorId: args.to,
4701
+ conversationId: args.to,
4702
+ peerType: info.type ?? undefined,
4703
+ }).effectiveRole;
4704
+ }
4705
+ catch { /* 角色解析失败按未知处理 */ }
4706
+ const chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
4610
4707
  if (!finalPayload.chatmode)
4611
4708
  finalPayload.chatmode = chatmode;
4612
4709
  const parentCausation = normalizeCausation(args.causation);
@@ -6415,32 +6512,41 @@ export class AUNChannelPlugin {
6415
6512
  text: payload.text,
6416
6513
  severity: 'info',
6417
6514
  };
6515
+ setIfDefined(noticePayload, 'raw', payload.raw);
6418
6516
  setIfDefined(noticePayload, 'subtype', payload.subtype);
6419
6517
  await channel.sendReliableStructured(channelId, noticePayload, replyCtx, payload.text);
6420
6518
  return;
6421
6519
  }
6422
6520
  case 'system.error': {
6521
+ const raw = truncateRawErrorForDisplay(payload.raw);
6522
+ const projectedRaw = projectedRawError(payload.raw, raw);
6423
6523
  const errorPayload = {
6424
6524
  type: payload.recoverable === true ? 'notice' : 'error',
6425
6525
  ...taskBase(),
6426
6526
  subtype: payload.subtype,
6427
- text: payload.text,
6527
+ text: payload.recoverable === true
6528
+ ? payload.text
6529
+ : appendRawErrorText(payload.text, raw),
6530
+ ...(projectedRaw !== undefined ? { raw: projectedRaw } : {}),
6531
+ ...(payload.reason !== undefined ? { reason: payload.reason } : {}),
6428
6532
  ...(payload.recoverable === true
6429
6533
  ? { severity: 'warn' }
6430
- : { message: payload.text, user_message: payload.text, recoverable: false }),
6534
+ : { recoverable: false }),
6431
6535
  };
6432
6536
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6433
6537
  return;
6434
6538
  }
6435
6539
  case 'result.error': {
6540
+ const raw = truncateRawErrorForDisplay(payload.raw);
6541
+ const projectedRaw = projectedRawError(payload.raw, raw);
6436
6542
  const errorPayload = {
6437
6543
  type: 'error',
6438
6544
  ...taskBase(),
6439
- reason: payload.reason,
6440
- text: payload.text,
6441
- message: payload.text,
6442
- user_message: payload.text,
6545
+ text: appendRawErrorText(payload.text, raw),
6443
6546
  };
6547
+ setIfDefined(errorPayload, 'subtype', payload.subtype);
6548
+ setIfDefined(errorPayload, 'reason', payload.reason);
6549
+ setIfDefined(errorPayload, 'raw', projectedRaw);
6444
6550
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6445
6551
  return;
6446
6552
  }
@@ -6766,6 +6872,16 @@ export class AUNChannelPlugin {
6766
6872
  },
6767
6873
  registerHooks(hookCtx) {
6768
6874
  channel.setEventBus(hookCtx.eventBus);
6875
+ // 话题改名通知消费器:直接改本端索引,不经过 Agent 队列。
6876
+ channel.setThreadRenameHandler(async (input) => {
6877
+ const { ThreadService } = await import('../core/thread/thread-service.js');
6878
+ const service = new ThreadService(hookCtx.sessionManager);
6879
+ const result = await service.applyRemoteRename(input);
6880
+ const session = result.applied
6881
+ ? service.findInIndex(input.selfAid, input.peer, input.threadId)?.sessionId
6882
+ : undefined;
6883
+ return { applied: result.applied, reason: result.reason, sessionId: session };
6884
+ });
6769
6885
  if (channel.setOnChannelDown) {
6770
6886
  channel.setOnChannelDown(() => {
6771
6887
  hookCtx.eventBus.publish({