evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -7,14 +7,46 @@ let cachedBubblewrapPath;
7
7
  let sandboxInitializationCircuit;
8
8
  const sessionSandboxInitializationCircuits = new Map();
9
9
  let sandboxCircuitNoticeAt = 0;
10
+ let sandboxInitializationFailureCounters = emptySandboxInitializationFailureCounters();
10
11
  export const SANDBOX_INITIALIZATION_FAILED = 'sandbox_initialization_failed';
11
12
  export const SANDBOX_INITIALIZATION_COOLDOWN_MS = 5 * 60 * 1000;
12
13
  export const SANDBOX_CIRCUIT_NOTICE_INTERVAL_MS = 60 * 1000;
14
+ function emptySandboxInitializationFailureCounters() {
15
+ return {
16
+ total: 0,
17
+ applySeccomp: 0,
18
+ byKind: { uid_map: 0, gid_map: 0, seccomp: 0, user_namespace: 0, bubblewrap: 0, unknown: 0 },
19
+ };
20
+ }
21
+ /** Classify already recognized bootstrap evidence, never arbitrary tool output. */
22
+ function sandboxInitializationFailureKind(text) {
23
+ if (/\buid_map\b/i.test(text))
24
+ return 'uid_map';
25
+ if (/\bgid_map\b/i.test(text))
26
+ return 'gid_map';
27
+ if (/\bseccomp\b/i.test(text))
28
+ return 'seccomp';
29
+ if (/user namespace/i.test(text))
30
+ return 'user_namespace';
31
+ if (/\b(?:bwrap|bubblewrap)\b/i.test(text))
32
+ return 'bubblewrap';
33
+ return 'unknown';
34
+ }
35
+ export function getSandboxInitializationFailureCounters() {
36
+ return { ...sandboxInitializationFailureCounters, byKind: { ...sandboxInitializationFailureCounters.byKind } };
37
+ }
13
38
  const SANDBOX_DIAGNOSTIC_TARGET_LIMIT = 256;
14
39
  /** Host bootstrap failures open a process circuit; tool-result failures only
15
40
  * affect their originating session because tool output is not host attestation. */
16
41
  export function recordSandboxInitializationFailure(error, stderr = [], now = Date.now(), sessionId) {
17
- const detail = createSandboxInitializationError(error, stderr).message.slice(0, 512);
42
+ const wrapped = createSandboxInitializationError(error, stderr);
43
+ const evidence = [wrapped.message, ...(wrapped.stderr ?? [])].join('\n');
44
+ const kind = sandboxInitializationFailureKind(evidence);
45
+ sandboxInitializationFailureCounters.total++;
46
+ sandboxInitializationFailureCounters.byKind[kind]++;
47
+ if (/\bapply-seccomp\b/i.test(evidence))
48
+ sandboxInitializationFailureCounters.applySeccomp++;
49
+ const detail = wrapped.message.slice(0, 512);
18
50
  getActiveSandboxInitializationFailure(now);
19
51
  const previous = sessionId ? sessionSandboxInitializationCircuits.get(sessionId) : sandboxInitializationCircuit;
20
52
  const circuit = previous
@@ -23,8 +55,9 @@ export function recordSandboxInitializationFailure(error, stderr = [], now = Dat
23
55
  lastObservedAt: now,
24
56
  occurrences: previous.occurrences + 1,
25
57
  detail,
58
+ kind,
26
59
  }
27
- : { openedAt: now, lastObservedAt: now, occurrences: 1, detail };
60
+ : { openedAt: now, lastObservedAt: now, occurrences: 1, detail, kind };
28
61
  if (sessionId)
29
62
  sessionSandboxInitializationCircuits.set(sessionId, circuit);
30
63
  else
@@ -68,11 +101,13 @@ export function isSandboxInitializationFailure(error, stderr = []) {
68
101
  .join('\n')
69
102
  .toLowerCase();
70
103
  return text.includes(SANDBOX_INITIALIZATION_FAILED)
104
+ || /fs sandbox helper failed(?: with status)?\b/.test(text)
71
105
  || /(?:apply-seccomp|seccomp)[\s\S]*(?:uid_map|gid_map|operation not permitted)/.test(text)
72
106
  || /(?:uid_map|gid_map)[\s\S]*(?:operation not permitted|permission denied)/.test(text)
73
107
  || /user namespace[\s\S]*(?:not permitted|unavailable|failed)/.test(text)
74
108
  || /bubblewrap[\s\S]*namespace[\s\S]*(?:failed|not permitted|permission denied)/.test(text)
75
- || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text);
109
+ || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot|unable to)[\s\S]*(?:bind|mount|mount table)/.test(text)
110
+ || /(?:^|\n)\s*bwrap:\s*(?:can't|cannot) create (?:file|directory) at [^\n]+: read-only file system/.test(text);
76
111
  }
77
112
  export function createSandboxInitializationError(error, stderr = []) {
78
113
  if (error instanceof Error
@@ -98,6 +133,39 @@ export function createSandboxInitializationError(error, stderr = []) {
98
133
  export function shouldFailIfClaudeSandboxUnavailable(platform = process.platform) {
99
134
  return platform !== 'win32';
100
135
  }
136
+ /**
137
+ * In an unprivileged user namespace (for example, root in an LXC container),
138
+ * dropping all capabilities before apply-seccomp creates its child namespace
139
+ * prevents that helper from writing a mapping for UID 0 (CAP_SETFCAP).
140
+ * Use the SDK's nested-sandbox compatibility mode only for confirmed remapped
141
+ * root. Unknown host state must not opt into weaker isolation.
142
+ */
143
+ export function shouldEnableClaudeWeakerNestedSandbox(platform = process.platform, effectiveUid = typeof process.geteuid === 'function' ? process.geteuid() : undefined) {
144
+ if (platform !== 'linux' || effectiveUid !== 0)
145
+ return false;
146
+ let uidMap;
147
+ try {
148
+ uidMap = fs.readFileSync('/proc/self/uid_map', 'utf8').trim();
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ if (!uidMap)
154
+ return false;
155
+ let remappedRoot = false;
156
+ for (const line of uidMap.split(/\r?\n/)) {
157
+ const fields = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)$/);
158
+ if (!fields)
159
+ return false;
160
+ const [inside, outside, count] = fields.slice(1).map(Number);
161
+ if (![inside, outside, count].every(Number.isSafeInteger)
162
+ || count === 0 || inside + count > 0xffffffff || outside + count > 0xffffffff)
163
+ return false;
164
+ if (inside === 0 && outside !== 0)
165
+ remappedRoot = true;
166
+ }
167
+ return remappedRoot;
168
+ }
101
169
  function isExecutableFile(candidate) {
102
170
  if (!candidate)
103
171
  return false;
@@ -586,4 +654,5 @@ export function _resetSandboxRuntimeCache() {
586
654
  cachedBubblewrapPath = undefined;
587
655
  sandboxInitializationCircuit = undefined;
588
656
  sandboxCircuitNoticeAt = 0;
657
+ sandboxInitializationFailureCounters = emptySandboxInitializationFailureCounters();
589
658
  }
@@ -20,6 +20,21 @@ export function classifyToolErrorCode(input) {
20
20
  return 'USER_DENIED';
21
21
  return 'EXECUTION_FAILED';
22
22
  }
23
+ /**
24
+ * Map the internal error taxonomy to the intentionally small AUN activity
25
+ * contract. The detailed code remains available to local audit consumers.
26
+ */
27
+ export function classifyAunToolErrorCode(input) {
28
+ const code = classifyToolErrorCode(input);
29
+ if (code === 'POLICY_DENIED'
30
+ || code === 'ROLE_DENIED'
31
+ || code === 'USER_DENIED'
32
+ || code === 'APPROVAL_TIMEOUT'
33
+ || code === 'DELEGATION_FAILED') {
34
+ return 'PERMISSION_DENIED';
35
+ }
36
+ return 'EXECUTION_FAILED';
37
+ }
23
38
  /**
24
39
  * Compatibility classifier for historical, unstructured logs only.
25
40
  * Production event producers must use classifyToolErrorCode() and explicit
@@ -5,7 +5,7 @@ import { randomUUID } from 'crypto';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { resolveRoot } from '../../paths.js';
7
7
  import { containsHClassReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
8
- import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellDelegationAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
8
+ import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, parseCodexToolCommand, parseBoundedOutputShellCommand, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
9
9
  import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
10
10
  /** Resolve the session-owned temporary root supplied by the execution host. */
11
11
  export function resolveManagedTempDir(env = process.env) {
@@ -2210,8 +2210,8 @@ export function evaluateToolPreflight(toolName, input, context) {
2210
2210
  const parserOptions = {
2211
2211
  verifyShellExecutable: process.platform === 'win32',
2212
2212
  managedTempDir: context.managedTempDir,
2213
- ...(context.delegationCarrier
2214
- ? { expectedDelegationToken: context.delegationCarrier }
2213
+ ...(context.sessionCredential
2214
+ ? { expectedCredentialToken: context.sessionCredential }
2215
2215
  : {}),
2216
2216
  };
2217
2217
  const parsedCodex = context.sessionId ? parseCodexToolCommand(input, parserOptions) : null;
@@ -2223,12 +2223,12 @@ export function evaluateToolPreflight(toolName, input, context) {
2223
2223
  policyCode: 'ec_shell_untrusted_executable',
2224
2224
  };
2225
2225
  }
2226
- if (context.sessionId && parsedCodex?.ok && parsedCodex.delegationToken
2227
- && !context.delegationCarrier) {
2226
+ if (context.sessionId && parsedCodex?.ok && parsedCodex.credentialToken
2227
+ && !context.sessionCredential) {
2228
2228
  return {
2229
2229
  behavior: 'deny',
2230
2230
  input,
2231
- message: '🔒 PowerShell 命令中的 EVOLCORE_DELEGATION_TOKEN 必须与当前托管 thread carrier 绑定;无法确认时拒绝执行。',
2231
+ message: '🔒 PowerShell 命令中的会话凭据必须与当前托管 Codex thread 绑定;无法确认时拒绝执行。',
2232
2232
  policyCode: 'delegation_token_unbound',
2233
2233
  };
2234
2234
  }
@@ -2293,7 +2293,7 @@ export function evaluateToolPreflight(toolName, input, context) {
2293
2293
  const shellDialect = stringShellCarrier?.dialect
2294
2294
  ?? (hasCodexCmdCarrierEcIntent(input) ? 'cmd' : undefined);
2295
2295
  const hasPowerShellCarrierAssignment = stringShellCarrier?.dialect === 'powershell'
2296
- && hasPowerShellDelegationAssignment(stringShellCarrier.command);
2296
+ && hasPowerShellCredentialAssignment(stringShellCarrier.command);
2297
2297
  const ecCommand = hasPowerShellCarrierAssignment && parsedCodex
2298
2298
  ? parsedCodex.ok && parsedCodex.argv[0] === 'ec'
2299
2299
  ? { kind: 'literal' }
@@ -2334,7 +2334,7 @@ export function evaluateToolPreflight(toolName, input, context) {
2334
2334
  }
2335
2335
  if (ecCommand.issue === 'invalid-quote') {
2336
2336
  const message = shellDialect === 'powershell'
2337
- ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 正文内的双引号请用反引号转义,或改用单引号包裹纯文字'
2337
+ ? '🔒 EC 命令引号未闭合或存在错误嵌套;PowerShell 也会解释中文弯引号。请按 PowerShell 规则转义,或把消息正文写入当前 $TMPDIR 后使用 --text-from-file'
2338
2338
  : shellDialect === 'cmd'
2339
2339
  ? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
2340
2340
  : '🔒 EC 命令引号未闭合或存在错误嵌套;正文中的双引号请写成 \\"';
@@ -37,6 +37,10 @@ export function parsePeerKey(key) {
37
37
  export class PeerIdentityCache {
38
38
  /** 缓存最大时效:30 天 */
39
39
  static CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
40
+ static invalidationRevisions = new Map();
41
+ static nextInvalidationRevision = 0;
42
+ /** Missing agent.md is recoverable; do not retain this negative result for 30 days. */
43
+ static UNKNOWN_CACHE_MAX_AGE_MS = 5 * 60 * 1000;
40
44
  /**
41
45
  * 获取 peer-identity.json 文件路径
42
46
  */
@@ -58,6 +62,22 @@ export class PeerIdentityCache {
58
62
  return null;
59
63
  }
60
64
  }
65
+ /** Expire only the freshness check; retain the last snapshot and its trust level. */
66
+ static invalidate(channelType, peerId, agentDir) {
67
+ const cached = this.get(channelType, peerId, agentDir);
68
+ if (!cached)
69
+ return;
70
+ const filePath = this.getFilePath(channelType, peerId, agentDir);
71
+ this.invalidationRevisions.set(filePath, ++this.nextInvalidationRevision);
72
+ if (cached.lastCheckedAt === 0)
73
+ return;
74
+ try {
75
+ fs.writeFileSync(filePath, JSON.stringify({ ...cached, lastCheckedAt: 0 }, null, 2), 'utf-8');
76
+ }
77
+ catch (error) {
78
+ logger.warn(`[PeerIdentityCache] Failed to invalidate ${channelType}#${peerId}: ${error}`);
79
+ }
80
+ }
61
81
  /**
62
82
  * 检查缓存是否需要刷新
63
83
  * @param maxAgeMs 最大缓存时间(默认 30 天)
@@ -67,7 +87,10 @@ export class PeerIdentityCache {
67
87
  const cached = this.get(channelType, peerId, agentDir);
68
88
  if (!cached)
69
89
  return true;
70
- return Date.now() - cached.lastCheckedAt > maxAgeMs;
90
+ const effectiveMaxAge = cached.source === 'unknown'
91
+ ? Math.min(maxAgeMs, this.UNKNOWN_CACHE_MAX_AGE_MS)
92
+ : maxAgeMs;
93
+ return cached.lastCheckedAt === 0 || Date.now() - cached.lastCheckedAt > effectiveMaxAge;
71
94
  }
72
95
  /**
73
96
  * 从 agent.md 更新身份信息
@@ -95,7 +118,7 @@ export class PeerIdentityCache {
95
118
  };
96
119
  const filePath = this.getFilePath(channelType, peerId, agentDir);
97
120
  try {
98
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
121
+ fs.mkdirSync(path.join(path.dirname(filePath), 'shared'), { recursive: true });
99
122
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
100
123
  logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent} source=${source}`);
101
124
  }
@@ -132,7 +155,7 @@ export class PeerIdentityCache {
132
155
  };
133
156
  const filePath = this.getFilePath(channelType, peerId, agentDir);
134
157
  try {
135
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
158
+ fs.mkdirSync(path.join(path.dirname(filePath), 'shared'), { recursive: true });
136
159
  fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
137
160
  logger.debug(`[PeerIdentityCache] Marked unknown: ${channelType}#${peerId}`);
138
161
  }
@@ -149,8 +172,13 @@ export class PeerIdentityCache {
149
172
  * @param agentDir agent 数据根目录
150
173
  * @param store AIDStore 实例(由调用方提供,负责 checkAgentMd + downloadAgentMd)
151
174
  * @param forceRefresh 强制刷新(忽略缓存时效)
175
+ * @param aunPath AUN SDK 数据根目录;与关系缓存所在的 agentDir 分开
152
176
  */
153
- static async resolve(channelType, peerId, agentDir, store, forceRefresh = false) {
177
+ static async resolve(channelType, peerId, agentDir, store, forceRefresh = false, aunPath) {
178
+ // AIDStore is authoritative for the SDK cache root. Callers that predate
179
+ // the explicit aunPath parameter must still fall back to that same root,
180
+ // otherwise the error path could read a different agent.md tree.
181
+ const resolvedAunPath = aunPath ?? store.aunPath;
154
182
  // A malformed transport envelope must never turn into a relation key such
155
183
  // as `aun#` or trigger an agent.md lookup for an empty AID. Keep the
156
184
  // fallback in-memory so callers can safely render an unknown peer without
@@ -177,15 +205,36 @@ export class PeerIdentityCache {
177
205
  return cached;
178
206
  }
179
207
  }
208
+ const cachePath = this.getFilePath(channelType, peerId, agentDir);
209
+ const revision = this.invalidationRevisions.get(cachePath) ?? 0;
210
+ const stale = this.get(channelType, peerId, agentDir);
180
211
  // 2. 通过 agentmdSync 拉取(内部走 store.checkAgentMd → store.downloadAgentMd)
181
212
  try {
182
213
  logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channelType}#${peerId}`);
183
214
  const { agentmdSync } = await import('../../aun/aid/agentmd.js');
184
- const result = await agentmdSync(peerId, { store });
215
+ const invalidated = stale?.lastCheckedAt === 0 || revision > 0;
216
+ const result = await agentmdSync(peerId, {
217
+ store,
218
+ aunPath: resolvedAunPath,
219
+ forceRefresh: invalidated,
220
+ ttlDays: stale?.source === 'unknown' ? 0 : undefined,
221
+ });
222
+ if ((this.invalidationRevisions.get(cachePath) ?? 0) !== revision) {
223
+ // A newer event arrived during this read. Its next lookup must refresh.
224
+ return this.get(channelType, peerId, agentDir) ?? {
225
+ aid: peerId, type: 'unknown', isAgent: true, agentMdHash: '', agentMdUpdatedAt: 0,
226
+ verifiedAt: 0, lastCheckedAt: 0, source: 'unknown',
227
+ };
228
+ }
229
+ // A failed refresh may return old local bytes. Keep the invalidated
230
+ // snapshot so the next lookup retries instead of caching it for 30 days.
231
+ if (invalidated && !result.verification && stale)
232
+ return stale;
185
233
  const content = result.content;
186
234
  if (!content) {
187
235
  throw new Error('agent.md content unavailable');
188
236
  }
237
+ this.invalidationRevisions.delete(cachePath);
189
238
  // 验签通过 → 可信(source=agentmd);否则 type 仍解析但标记未验证
190
239
  const verified = result.verification?.status === 'verified';
191
240
  const source = verified ? 'agentmd' : 'agentmd-unverified';
@@ -201,10 +250,12 @@ export class PeerIdentityCache {
201
250
  return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
202
251
  }
203
252
  catch (err) {
253
+ if (stale?.lastCheckedAt === 0)
254
+ return stale;
204
255
  // 4. agentmdSync 抛错(非网络失败——网络失败时它内部已 fallback 返回本地内容;
205
256
  // 这里通常是无内容或解析异常)。兜底读本地文件,但无法重新验签,
206
257
  // 故沿用缓存里已有的可信级别,绝不凭空升级为已验签。
207
- const localPath = agentMdPath(peerId);
258
+ const localPath = agentMdPath(peerId, resolvedAunPath);
208
259
  try {
209
260
  if (fs.existsSync(localPath)) {
210
261
  const localContent = fs.readFileSync(localPath, 'utf-8');
@@ -0,0 +1,106 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { agentRelationDir, agentRelationSharedDir, agentRelationsDir, resolvePaths } from '../../paths.js';
5
+ import { parsePeerKey } from './peer-identity.js';
6
+ import { logger } from '../../utils/logger.js';
7
+ export const RELATION_SHARED_FILE_ALLOWLIST = Object.freeze(['rules.md']);
8
+ function isAllowedSharedFile(relativePath) {
9
+ return RELATION_SHARED_FILE_ALLOWLIST.includes(relativePath);
10
+ }
11
+ function copyFileAtomically(source, target) {
12
+ const targetDir = path.dirname(target);
13
+ fs.mkdirSync(targetDir, { recursive: true });
14
+ const temporary = path.join(targetDir, `.${path.basename(target)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
15
+ try {
16
+ fs.copyFileSync(source, temporary);
17
+ const sourceStat = fs.statSync(source);
18
+ fs.utimesSync(temporary, sourceStat.atime, sourceStat.mtime);
19
+ fs.renameSync(temporary, target);
20
+ }
21
+ finally {
22
+ try {
23
+ fs.rmSync(temporary, { force: true });
24
+ }
25
+ catch { }
26
+ }
27
+ }
28
+ export function ensureRelationSharedDirectory(selfAid, peerKey) {
29
+ parsePeerKey(peerKey);
30
+ const sharedDir = agentRelationSharedDir(selfAid, peerKey);
31
+ fs.mkdirSync(sharedDir, { recursive: true });
32
+ return sharedDir;
33
+ }
34
+ export function syncRelationSharedFile(selfAid, peerKey, relativePath) {
35
+ if (!isAllowedSharedFile(relativePath)) {
36
+ throw Object.assign(new Error(`Relation shared file is not allowlisted: ${relativePath}`), {
37
+ code: 'SHARED_FILE_NOT_ALLOWED',
38
+ });
39
+ }
40
+ parsePeerKey(peerKey);
41
+ const relationDir = agentRelationDir(selfAid, peerKey);
42
+ const source = path.join(relationDir, relativePath);
43
+ const target = path.join(ensureRelationSharedDirectory(selfAid, peerKey), relativePath);
44
+ let sourceStat;
45
+ try {
46
+ sourceStat = fs.statSync(source);
47
+ }
48
+ catch {
49
+ return { copied: false, target };
50
+ }
51
+ if (!sourceStat.isFile())
52
+ return { copied: false, target };
53
+ try {
54
+ const targetStat = fs.statSync(target);
55
+ if (targetStat.isFile()
56
+ && targetStat.size === sourceStat.size
57
+ && Math.abs(targetStat.mtimeMs - sourceStat.mtimeMs) < 1) {
58
+ return { copied: false, target };
59
+ }
60
+ }
61
+ catch { }
62
+ copyFileAtomically(source, target);
63
+ return { copied: true, target };
64
+ }
65
+ export function migrateRelationSharedDirectories() {
66
+ const agentsDir = resolvePaths().agentsDir;
67
+ let relations = 0;
68
+ let copied = 0;
69
+ if (!fs.existsSync(agentsDir))
70
+ return { relations, copied };
71
+ for (const agentEntry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
72
+ if (!agentEntry.isDirectory())
73
+ continue;
74
+ const selfAid = agentEntry.name;
75
+ const relationsDir = agentRelationsDir(selfAid);
76
+ let relationEntries;
77
+ try {
78
+ relationEntries = fs.readdirSync(relationsDir, { withFileTypes: true });
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ for (const relationEntry of relationEntries) {
84
+ if (!relationEntry.isDirectory() || relationEntry.name.startsWith('_'))
85
+ continue;
86
+ try {
87
+ parsePeerKey(relationEntry.name);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ relations += 1;
93
+ try {
94
+ ensureRelationSharedDirectory(selfAid, relationEntry.name);
95
+ for (const file of RELATION_SHARED_FILE_ALLOWLIST) {
96
+ if (syncRelationSharedFile(selfAid, relationEntry.name, file).copied)
97
+ copied += 1;
98
+ }
99
+ }
100
+ catch (error) {
101
+ logger.warn(`[RelationShared] failed self=${selfAid} peer=${relationEntry.name}: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ }
104
+ }
105
+ return { relations, copied };
106
+ }
@@ -1,5 +1,9 @@
1
1
  import { getRoleDefinition } from '../../config/roles.js';
2
2
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
3
+ import { ConfigTarget, read } from '../../config/config-manager.js';
4
+ import { resolvePrincipal } from '../../config/contact-book.js';
5
+ import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
6
+ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
3
7
  const PERMISSION_MODE_AUTHORITY = {
4
8
  readonly: 0,
5
9
  auto: 1,
@@ -139,3 +143,63 @@ export function validateRuntimeStringFieldOverride(input) {
139
143
  }
140
144
  return { ok: true };
141
145
  }
146
+ /**
147
+ * Resolve the Agent-level host execution policy for one authenticated principal.
148
+ * The Agent flag is subordinate to daemon fullaccess and independent of role permissionMode values.
149
+ */
150
+ export function agentFullAccessStatus(agentAid, principalId) {
151
+ const config = read(ConfigTarget.Agent, { self: agentAid }, { cache: false });
152
+ const daemon = loadDaemonConfig();
153
+ const enabled = config?.fullaccess?.enabled === true;
154
+ const hostEnabled = isFullAccessEnabled(daemon);
155
+ const eligible = !!principalId
156
+ && !!config?.owners?.includes(principalId)
157
+ && !!daemon.owners?.includes(principalId);
158
+ return { enabled, hostEnabled, eligible, effective: enabled && hostEnabled && eligible };
159
+ }
160
+ /** Create a trusted per-call claim for a direct private message. */
161
+ export function createAgentPolicyAuthorization(input) {
162
+ if (input.chatType !== 'private'
163
+ || (input.source !== undefined && input.source !== 'user')
164
+ || ['agent', 'ai', 'bot', 'service', 'group'].includes((input.peerType ?? '').toLowerCase())
165
+ || !hasTrustedPrincipal(input.actor))
166
+ return undefined;
167
+ if (!agentFullAccessStatus(input.agentAid, input.actor.principalId).effective)
168
+ return undefined;
169
+ return {
170
+ permissionMode: 'fullaccess',
171
+ processRole: 'fullaccess-run',
172
+ dataScope: 'daemon',
173
+ source: 'agent-policy',
174
+ agentAid: input.agentAid,
175
+ authorizedBy: input.actor.principalId,
176
+ policyActor: { ...input.actor },
177
+ };
178
+ }
179
+ /** Revalidate a claim before Runner calls, retries, and daemon IPC. */
180
+ export function assertAgentPolicyAuthorization(authorization) {
181
+ const actor = authorization.policyActor;
182
+ if (!authorization.agentAid || !hasTrustedPrincipal(actor)) {
183
+ throw new Error('Agent fullaccess policy lacks authenticated origin');
184
+ }
185
+ const current = resolvePrincipal(authorization.agentAid, actor.operatorChannelType, actor.operatorId, actor.operatorChannelKey);
186
+ if (!hasTrustedPrincipal({ ...actor, ...current }) || current.principalId !== authorization.authorizedBy) {
187
+ throw new Error('Agent fullaccess policy origin identity changed');
188
+ }
189
+ if (!agentFullAccessStatus(authorization.agentAid, authorization.authorizedBy).effective) {
190
+ throw new Error('Agent fullaccess policy disabled or current owner is no longer eligible');
191
+ }
192
+ }
193
+ /** Only the authenticated EC Web Agent settings may mutate this policy. */
194
+ export function agentFullAccessWriteError(input) {
195
+ if (!input.ecwebManagement) {
196
+ return 'Agent fullaccess can only be configured from ECWeb';
197
+ }
198
+ if (!input.daemonOwner || input.delegated) {
199
+ return 'Only authenticated ECWeb management may configure Agent fullaccess';
200
+ }
201
+ if (input.enabled && !isFullAccessEnabled()) {
202
+ return 'Host fullaccess is disabled';
203
+ }
204
+ return undefined;
205
+ }
@@ -17,28 +17,21 @@ export function resolveManagedLockPath(namespace, identity, legacyPath) {
17
17
  }
18
18
  function isTrustedRuntimeLockDir(candidate) {
19
19
  const resolved = path.resolve(candidate);
20
- if (path.basename(resolved) !== 'evolcore-locks')
21
- return false;
22
20
  const tmpRoot = process.env.TMPDIR?.trim();
23
21
  if (!tmpRoot || !path.isAbsolute(tmpRoot))
24
22
  return false;
25
23
  const resolvedTmpRoot = path.resolve(tmpRoot);
26
- const relative = path.relative(resolvedTmpRoot, resolved);
27
- const isWithinTmp = relative !== '..'
28
- && !relative.startsWith(`..${path.sep}`)
29
- && !path.isAbsolute(relative);
30
- // The daemon supplies each task either a generic evolcore-runtime-* child
31
- // or Codex's private digest directory. Both share a sibling evolcore-locks
32
- // directory so mutations from separate sessions still serialize. Do not
33
- // accept arbitrary TMPDIR siblings supplied by a child process.
34
24
  const tmpBase = path.basename(resolvedTmpRoot);
35
- const tmpParent = path.dirname(resolvedTmpRoot);
36
- const isGenericSessionRuntime = /^evolcore-runtime-[a-f0-9]{24}$/.test(tmpBase);
37
- const isCodexSessionRuntime = /^[a-f0-9]{24}$/.test(tmpBase)
38
- && /^evolcore-codex-(?:\d+|user)$/.test(path.basename(tmpParent));
39
- const isManagedSessionSibling = (isGenericSessionRuntime || isCodexSessionRuntime)
40
- && path.dirname(resolved) === tmpParent;
41
- if (!isWithinTmp && !isManagedSessionSibling)
25
+ const isSessionLockDir = /^ec-session-[a-f0-9]{24}$/.test(tmpBase)
26
+ && path.basename(resolved) === 'locks'
27
+ && path.dirname(resolved) === resolvedTmpRoot;
28
+ // The app-server process has no EvolCore session yet and retains its own
29
+ // process-lifetime lock namespace. Both process and session roots use the
30
+ // same `$TMPDIR/locks` layout.
31
+ const isProcessLockDir = /^evolcore-managed-tmp-/.test(tmpBase)
32
+ && path.basename(resolved) === 'locks'
33
+ && path.dirname(resolved) === resolvedTmpRoot;
34
+ if (!isSessionLockDir && !isProcessLockDir)
42
35
  return false;
43
36
  try {
44
37
  // Windows Stats.mode does not describe ACL privacy. The managed runtime
@@ -53,15 +46,6 @@ function isTrustedRuntimeLockDir(candidate) {
53
46
  return false;
54
47
  if (!isWindows && (tmpStat.mode & 0o077) !== 0)
55
48
  return false;
56
- if (isManagedSessionSibling) {
57
- const parentStat = fs.lstatSync(tmpParent);
58
- if (!parentStat.isDirectory() || parentStat.isSymbolicLink())
59
- return false;
60
- if (!isWindows && typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
61
- return false;
62
- if (!isWindows && (parentStat.mode & 0o077) !== 0)
63
- return false;
64
- }
65
49
  const stat = fs.lstatSync(resolved);
66
50
  if (!stat.isDirectory() || stat.isSymbolicLink())
67
51
  return false;