evolcore 0.0.26 → 0.0.27

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 (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -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' }
@@ -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;
@@ -0,0 +1,170 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const RUNNER_TASK_EVENT_TYPES = new Set([
4
+ 'runner:task-started',
5
+ 'runner:task-progress',
6
+ 'runner:task-notification',
7
+ ]);
8
+ export const RUNNER_TASK_LOG_RE = /^events(?:-.+)?\.log$/;
9
+ const runnerLogCache = new Map();
10
+ function parseRunnerTaskLines(raw) {
11
+ const events = [];
12
+ for (const line of raw.split(/\r?\n/)) {
13
+ if (!line.includes('runner:task-'))
14
+ continue;
15
+ try {
16
+ const event = JSON.parse(line);
17
+ if (event && RUNNER_TASK_EVENT_TYPES.has(event.type) && typeof event.sessionId === 'string')
18
+ events.push(event);
19
+ }
20
+ catch { }
21
+ }
22
+ return events;
23
+ }
24
+ function readRunnerLogFile(file) {
25
+ let stat;
26
+ try {
27
+ stat = fs.statSync(file);
28
+ }
29
+ catch {
30
+ runnerLogCache.delete(file);
31
+ return [];
32
+ }
33
+ let cached = runnerLogCache.get(file);
34
+ if (!cached || stat.size < cached.offset || stat.ino !== cached.inode || stat.dev !== cached.device
35
+ || (stat.size === cached.offset && stat.mtimeMs !== cached.mtimeMs)) {
36
+ cached = { device: stat.dev, inode: stat.ino, offset: 0, mtimeMs: 0, events: [] };
37
+ }
38
+ if (stat.size === cached.offset) {
39
+ runnerLogCache.set(file, cached);
40
+ return cached.events;
41
+ }
42
+ const bytes = stat.size - cached.offset;
43
+ const buffer = Buffer.allocUnsafe(bytes);
44
+ let fd = null;
45
+ try {
46
+ fd = fs.openSync(file, 'r');
47
+ const read = fs.readSync(fd, buffer, 0, bytes, cached.offset);
48
+ if (read !== bytes)
49
+ return cached.events;
50
+ }
51
+ catch {
52
+ return cached.events;
53
+ }
54
+ finally {
55
+ if (fd != null)
56
+ try {
57
+ fs.closeSync(fd);
58
+ }
59
+ catch { }
60
+ }
61
+ const lastBreak = buffer.lastIndexOf(0x0a);
62
+ if (lastBreak < 0) {
63
+ runnerLogCache.set(file, cached);
64
+ return cached.events;
65
+ }
66
+ cached = {
67
+ device: stat.dev,
68
+ inode: stat.ino,
69
+ offset: cached.offset + lastBreak + 1,
70
+ mtimeMs: stat.mtimeMs,
71
+ events: cached.events.concat(parseRunnerTaskLines(buffer.subarray(0, lastBreak + 1).toString('utf8'))),
72
+ };
73
+ runnerLogCache.set(file, cached);
74
+ return cached.events;
75
+ }
76
+ function eventTimestamp(event) {
77
+ const numeric = Number(event.timestamp);
78
+ if (Number.isFinite(numeric) && numeric > 0)
79
+ return numeric;
80
+ const parsed = event.ts ? Date.parse(event.ts) : 0;
81
+ return Number.isFinite(parsed) ? parsed : 0;
82
+ }
83
+ function eventTaskId(event, aliases) {
84
+ if (typeof event.taskId === 'string' && event.taskId)
85
+ return event.taskId;
86
+ const useId = typeof event.toolUseId === 'string' ? event.toolUseId.trim() : '';
87
+ return useId ? aliases.get(useId) ?? `tool:${useId}` : null;
88
+ }
89
+ export function readRunnerTaskHistory(logsDir, evolSessionId) {
90
+ let files;
91
+ try {
92
+ files = fs.readdirSync(logsDir)
93
+ .filter(name => RUNNER_TASK_LOG_RE.test(name))
94
+ .sort()
95
+ .map(name => path.join(logsDir, name));
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ const validFiles = new Set(files);
101
+ for (const cachedFile of runnerLogCache.keys()) {
102
+ if (path.dirname(cachedFile) === logsDir && !validFiles.has(cachedFile))
103
+ runnerLogCache.delete(cachedFile);
104
+ }
105
+ const events = files.flatMap(readRunnerLogFile)
106
+ .filter(event => event.sessionId === evolSessionId)
107
+ .sort((a, b) => eventTimestamp(a) - eventTimestamp(b));
108
+ const tasks = new Map();
109
+ const aliases = new Map();
110
+ for (const event of events) {
111
+ if (typeof event.taskId === 'string' && event.taskId && typeof event.toolUseId === 'string' && event.toolUseId) {
112
+ aliases.set(event.toolUseId, event.taskId);
113
+ }
114
+ }
115
+ const hidden = new Set();
116
+ const terminal = new Set();
117
+ for (const event of events) {
118
+ const taskId = eventTaskId(event, aliases);
119
+ if (!taskId)
120
+ continue;
121
+ if (event.skipTranscript)
122
+ hidden.add(taskId);
123
+ const at = eventTimestamp(event);
124
+ let task = tasks.get(taskId);
125
+ if (!task) {
126
+ task = {
127
+ taskId,
128
+ taskKind: event.taskKind || 'agent',
129
+ toolUseId: event.toolUseId || null,
130
+ description: event.description || taskId,
131
+ subagentType: event.subagentType || null,
132
+ status: 'running',
133
+ terminalReason: '',
134
+ progress: '',
135
+ lastToolName: '',
136
+ totalTokens: 0,
137
+ toolUses: 0,
138
+ durationMs: 0,
139
+ startedAt: at,
140
+ updatedAt: at,
141
+ };
142
+ tasks.set(taskId, task);
143
+ }
144
+ if (event.type === 'runner:task-started') {
145
+ task.startedAt = task.startedAt ? Math.min(task.startedAt, at) : at;
146
+ if (!terminal.has(taskId))
147
+ task.status = 'running';
148
+ }
149
+ else if (event.type === 'runner:task-progress') {
150
+ task.progress = event.summary || event.description || task.progress;
151
+ }
152
+ else if (event.type === 'runner:task-notification') {
153
+ terminal.add(taskId);
154
+ task.status = event.status || 'completed';
155
+ task.terminalReason = event.terminalReason || task.terminalReason;
156
+ }
157
+ task.updatedAt = Math.max(task.updatedAt, at);
158
+ task.taskKind = event.taskKind || task.taskKind;
159
+ task.toolUseId = event.toolUseId || task.toolUseId;
160
+ if (event.type !== 'runner:task-progress')
161
+ task.description = event.description || task.description;
162
+ task.subagentType = event.subagentType || task.subagentType;
163
+ task.lastToolName = event.lastToolName || task.lastToolName;
164
+ task.totalTokens = event.usage?.totalTokens ?? event.totalTokens ?? task.totalTokens;
165
+ task.toolUses = event.usage?.toolUses ?? event.toolUses ?? task.toolUses;
166
+ task.durationMs = event.usage?.durationMs ?? event.durationMs ?? task.durationMs;
167
+ }
168
+ return [...tasks.values()].filter(task => !hidden.has(task.taskId))
169
+ .sort((a, b) => a.startedAt - b.startedAt || a.taskId.localeCompare(b.taskId));
170
+ }