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
@@ -0,0 +1,414 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { StringDecoder } from 'node:string_decoder';
6
+ import { isDelegationDiagnosticsEnabled } from '../../config-store.js';
7
+ import { resolvePaths } from '../../paths.js';
8
+ import { PRODUCT_DISPLAY_NAME } from '../../product.js';
9
+ import { stableWriteJson } from '../../utils/atomic-write.js';
10
+ import { logger } from '../../utils/logger.js';
11
+ import { LogWriter } from '../../utils/log-writer.js';
12
+ const contexts = new AsyncLocalStorage();
13
+ const DAY_MS = 86_400_000;
14
+ const EVENT_RETENTION_DAYS = 180;
15
+ const STATE_RETENTION_MS = 90 * DAY_MS;
16
+ const MESSAGES = {
17
+ missing_token: '当前请求未携带任务授权凭据。',
18
+ unknown_token: '在保留的诊断记录中无法识别授权凭据,无法确认其是否曾存在或已被清理。',
19
+ session_mismatch: '授权凭据不属于当前会话,请在正确的活动任务中重新发起命令。',
20
+ task_replaced: '当前任务授权已被替换,请在新的活动任务中重新发起命令。',
21
+ task_revoked: '任务授权已因任务结束、中断或策略变更被撤销,请重新发起任务。',
22
+ approval_not_found: '已识别会话凭据,但未找到当前命令的匹配审批。',
23
+ approval_expired: '当前命令的审批已过期,请在活动任务中重新申请审批。',
24
+ approval_consumed: '当前命令的审批使用次数已耗尽,请勿重放旧请求。',
25
+ command_hash_mismatch: '提交的命令参数与命令指纹不一致,当前请求已被拒绝。',
26
+ daemon_restarted: `授权来自先前的 ${PRODUCT_DISPLAY_NAME} 实例,请重新发起任务授权,不要重试旧请求。`,
27
+ };
28
+ export function delegationDiagnosticMessage(cause) {
29
+ return `(${cause}) ${MESSAGES[cause]}`;
30
+ }
31
+ export async function withDelegationDiagnosticContext(context, callback) {
32
+ return contexts.run(context, async () => {
33
+ const result = await callback();
34
+ if (!result || typeof result !== 'object' || !context.diagnostic)
35
+ return result;
36
+ const response = result;
37
+ if (response.ok !== false || typeof response.error !== 'string'
38
+ || !response.error.includes(context.diagnostic.reason))
39
+ return result;
40
+ const error = response.error;
41
+ return {
42
+ ...response,
43
+ error: error.startsWith(`(${context.diagnostic.diagnosticCause})`)
44
+ ? error
45
+ : `(${context.diagnostic.diagnosticCause}) ${error}`,
46
+ diagnosticCause: context.diagnostic.diagnosticCause,
47
+ ...(context.diagnostic.diagnosticEventId ? { diagnosticEventId: context.diagnostic.diagnosticEventId } : {}),
48
+ };
49
+ });
50
+ }
51
+ function stateKey(state) {
52
+ return JSON.stringify([state.kind, state.tokenHash, state.sessionId, state.commandHash ?? '']);
53
+ }
54
+ function fingerprint(value) {
55
+ return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value) ? value : undefined;
56
+ }
57
+ function reference(value) {
58
+ return typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\s\x00-\x1f\x7f]/.test(value)
59
+ ? value : undefined;
60
+ }
61
+ function readState(value) {
62
+ if (!value || typeof value !== 'object')
63
+ return undefined;
64
+ const state = value;
65
+ if (!['task', 'session', 'approval'].includes(state.kind) || !fingerprint(state.tokenHash)
66
+ || !reference(state.sessionId) || !reference(state.taskId) || !reference(state.runtimeEpoch)
67
+ || !Number.isFinite(state.updatedAt)
68
+ || !['active', 'replaced', 'revoked', 'expired', 'consumed'].includes(state.status)
69
+ || state.kind === 'approval' && (!fingerprint(state.commandHash) || !fingerprint(state.activeTokenHash)))
70
+ return undefined;
71
+ const sanitized = {
72
+ kind: state.kind, tokenHash: state.tokenHash, sessionId: state.sessionId, taskId: state.taskId,
73
+ runtimeEpoch: state.runtimeEpoch, status: state.status, updatedAt: state.updatedAt,
74
+ };
75
+ for (const field of ['selfAid', 'peerKey', 'actorId', 'issuedRole', 'permissionMode', 'threadId', 'messageId']) {
76
+ const safe = reference(state[field]);
77
+ if (safe)
78
+ sanitized[field] = safe;
79
+ }
80
+ for (const field of ['commandHash', 'activeTokenHash']) {
81
+ const safe = fingerprint(state[field]);
82
+ if (safe)
83
+ sanitized[field] = safe;
84
+ }
85
+ for (const field of ['expiresAt', 'remainingUses', 'sequence']) {
86
+ const numeric = state[field];
87
+ if (typeof numeric === 'number' && Number.isSafeInteger(numeric) && numeric >= 0)
88
+ sanitized[field] = numeric;
89
+ }
90
+ if (state.credentialType === 'task' || state.credentialType === 'codex-session')
91
+ sanitized.credentialType = state.credentialType;
92
+ if (state.revocationReason && ['completed', 'interrupted', 'replaced', 'policy_revoked', 'failed', 'revoked'].includes(state.revocationReason)) {
93
+ sanitized.revocationReason = state.revocationReason;
94
+ }
95
+ return sanitized;
96
+ }
97
+ export class DelegationDiagnostics {
98
+ runtimeEpoch;
99
+ states = new Map();
100
+ credentialsByHash = new Map();
101
+ indexPath;
102
+ logDir;
103
+ writer;
104
+ lastCleanup = 0;
105
+ lastArchiveCleanup = 0;
106
+ persistenceState;
107
+ sequence = 0;
108
+ lastWarnings = new Map();
109
+ constructor(runtimeEpoch) {
110
+ this.runtimeEpoch = runtimeEpoch;
111
+ const paths = resolvePaths();
112
+ this.logDir = paths.logs;
113
+ this.indexPath = path.join(paths.dataDir, 'delegation-diagnostics', 'index.json');
114
+ this.load();
115
+ this.record('runtime.started');
116
+ this.save();
117
+ }
118
+ close() {
119
+ this.writer?.close();
120
+ }
121
+ put(event, input) {
122
+ const state = readState({ ...input, updatedAt: Date.now(), sequence: ++this.sequence });
123
+ if (!state) {
124
+ this.warn('诊断状态格式无效,记录已跳过', new Error('invalid state'));
125
+ return;
126
+ }
127
+ this.store(state);
128
+ this.record(event, { state });
129
+ this.save();
130
+ }
131
+ find(kind, tokenHash, sessionId, commandHash) {
132
+ const recent = this.states.get(stateKey({ kind, tokenHash, sessionId, commandHash }));
133
+ if (recent && this.retained(recent))
134
+ return recent;
135
+ return undefined;
136
+ }
137
+ findCredential(tokenHash, sessionId) {
138
+ const matches = [...(this.credentialsByHash.get(tokenHash)?.values() ?? [])].filter(state => this.retained(state));
139
+ return matches.find(state => state.sessionId === sessionId)
140
+ ?? matches.sort((left, right) => right.updatedAt - left.updatedAt)[0];
141
+ }
142
+ classify(tokenHash, sessionId, commandHash) {
143
+ const credential = this.findCredential(tokenHash, sessionId);
144
+ if (!credential)
145
+ return 'unknown_token';
146
+ if (credential.sessionId !== sessionId)
147
+ return 'session_mismatch';
148
+ if (credential.kind === 'task')
149
+ return this.taskCause(credential);
150
+ const approval = commandHash ? this.find('approval', tokenHash, sessionId, commandHash) : undefined;
151
+ const activeTokenHash = approval?.activeTokenHash ?? credential.activeTokenHash;
152
+ const task = activeTokenHash
153
+ ? this.find('task', activeTokenHash, sessionId)
154
+ : undefined;
155
+ if (task && task.status !== 'active')
156
+ return this.taskCause(task);
157
+ if (credential.runtimeEpoch !== this.runtimeEpoch || approval && approval.runtimeEpoch !== this.runtimeEpoch) {
158
+ return 'daemon_restarted';
159
+ }
160
+ if (credential.status !== 'active')
161
+ return this.taskCause(credential);
162
+ if (!approval)
163
+ return 'approval_not_found';
164
+ if (approval.status === 'consumed')
165
+ return 'approval_consumed';
166
+ if (approval.status === 'expired' || approval.expiresAt !== undefined && approval.expiresAt <= Date.now()) {
167
+ return 'approval_expired';
168
+ }
169
+ if (approval.status !== 'active')
170
+ return this.taskCause(approval);
171
+ return 'approval_not_found';
172
+ }
173
+ classifyCommandHashMismatch(tokenHash, sessionId) {
174
+ return this.classifyCredential(tokenHash, sessionId) ?? 'command_hash_mismatch';
175
+ }
176
+ failure(cause, sessionId, tokenHash, commandHash) {
177
+ const reason = delegationDiagnosticMessage(cause);
178
+ const diagnosticEventId = this.record('validation.rejected', {
179
+ diagnosticCause: cause, sessionId: reference(sessionId), tokenHash: fingerprint(tokenHash), commandHash: fingerprint(commandHash),
180
+ });
181
+ const diagnostic = { diagnosticCause: cause, ...(diagnosticEventId ? { diagnosticEventId } : {}), reason };
182
+ const context = contexts.getStore();
183
+ if (context)
184
+ context.diagnostic = diagnostic;
185
+ return {
186
+ ok: false,
187
+ code: cause === 'missing_token' ? 'DELEGATION_REQUIRED'
188
+ : cause === 'daemon_restarted' ? 'DAEMON_RESTARTED' : 'INVALID_DELEGATION',
189
+ ...diagnostic,
190
+ };
191
+ }
192
+ accepted(sessionId, tokenHash, commandHash) {
193
+ this.record('validation.accepted', { sessionId: reference(sessionId), tokenHash: fingerprint(tokenHash), commandHash: fingerprint(commandHash) });
194
+ }
195
+ taskCause(state) {
196
+ if (state.status === 'replaced')
197
+ return 'task_replaced';
198
+ if (state.status !== 'active')
199
+ return 'task_revoked';
200
+ return state.runtimeEpoch !== this.runtimeEpoch ? 'daemon_restarted' : 'unknown_token';
201
+ }
202
+ classifyCredential(tokenHash, sessionId) {
203
+ const credential = this.findCredential(tokenHash, sessionId);
204
+ if (!credential)
205
+ return 'unknown_token';
206
+ if (credential.sessionId !== sessionId)
207
+ return 'session_mismatch';
208
+ if (credential.status === 'replaced')
209
+ return 'task_replaced';
210
+ if (credential.status !== 'active')
211
+ return 'task_revoked';
212
+ if (credential.runtimeEpoch !== this.runtimeEpoch)
213
+ return 'daemon_restarted';
214
+ if (credential.kind === 'task')
215
+ return undefined;
216
+ if (!credential.activeTokenHash)
217
+ return 'unknown_token';
218
+ const task = this.find('task', credential.activeTokenHash, sessionId);
219
+ if (!task)
220
+ return 'unknown_token';
221
+ if (task.status === 'replaced')
222
+ return 'task_replaced';
223
+ if (task.status !== 'active')
224
+ return 'task_revoked';
225
+ if (task.runtimeEpoch !== this.runtimeEpoch)
226
+ return 'daemon_restarted';
227
+ return undefined;
228
+ }
229
+ retained(state) {
230
+ if (state.runtimeEpoch === this.runtimeEpoch && state.status === 'active'
231
+ && (state.kind !== 'approval' || state.expiresAt !== undefined && state.expiresAt > Date.now()))
232
+ return true;
233
+ return state.updatedAt >= Date.now() - STATE_RETENTION_MS;
234
+ }
235
+ store(state) {
236
+ const key = stateKey(state);
237
+ this.states.set(key, state);
238
+ if (state.kind !== 'approval') {
239
+ let credentials = this.credentialsByHash.get(state.tokenHash);
240
+ if (!credentials) {
241
+ credentials = new Map();
242
+ this.credentialsByHash.set(state.tokenHash, credentials);
243
+ }
244
+ credentials.set(key, state);
245
+ }
246
+ this.sequence = Math.max(this.sequence, state.sequence ?? 0);
247
+ }
248
+ record(event, details = {}) {
249
+ if (!this.persistenceEnabled())
250
+ return undefined;
251
+ const context = contexts.getStore();
252
+ const record = {
253
+ version: 1, eventId: `del-audit-${crypto.randomUUID()}`, ts: Date.now(), runtimeEpoch: this.runtimeEpoch, event,
254
+ ...(reference(context?.requestId) ? { requestId: context.requestId } : {}),
255
+ ...(reference(context?.correlationId) ? { correlationId: context.correlationId } : {}),
256
+ ...(reference(context?.operation) ? { operation: context.operation } : {}),
257
+ ...details,
258
+ };
259
+ try {
260
+ if (!this.writer) {
261
+ this.writer = new LogWriter({
262
+ baseName: 'delegation-audit', logDir: this.logDir, rotation: 'daily',
263
+ retention: { days: EVENT_RETENTION_DAYS },
264
+ });
265
+ this.writer.close();
266
+ }
267
+ if (Date.now() - this.lastArchiveCleanup > 3_600_000) {
268
+ LogWriter.cleanupArchivesIn(this.logDir, 'delegation-audit', EVENT_RETENTION_DAYS * DAY_MS);
269
+ this.lastArchiveCleanup = Date.now();
270
+ }
271
+ this.writer.writeChecked(JSON.stringify(record));
272
+ return record.eventId;
273
+ }
274
+ catch (error) {
275
+ this.warn('审计事件写入失败', error);
276
+ return undefined;
277
+ }
278
+ }
279
+ load() {
280
+ try {
281
+ const index = JSON.parse(fs.readFileSync(this.indexPath, 'utf8'));
282
+ if (index.version !== 1 || !Array.isArray(index.states))
283
+ throw new Error('invalid diagnostic index');
284
+ for (const raw of index.states) {
285
+ const state = readState(raw);
286
+ if (state && this.retained(state))
287
+ this.store(state);
288
+ }
289
+ }
290
+ catch (error) {
291
+ if (error.code !== 'ENOENT')
292
+ this.warn('诊断索引读取失败,尝试从审计事件重建', error);
293
+ }
294
+ this.scanHistory(state => {
295
+ if (!this.retained(state))
296
+ return;
297
+ const previous = this.states.get(stateKey(state));
298
+ if (!previous || (state.sequence !== undefined && previous.sequence !== undefined
299
+ ? state.sequence > previous.sequence : state.updatedAt > previous.updatedAt))
300
+ this.store(state);
301
+ });
302
+ }
303
+ scanHistory(visit) {
304
+ try {
305
+ if (!fs.existsSync(this.logDir))
306
+ return;
307
+ const files = fs.readdirSync(this.logDir).filter(name => /^delegation-audit(?:-\d{8})?\.log$/.test(name)).sort();
308
+ for (const name of files) {
309
+ let descriptor;
310
+ const consume = (line) => {
311
+ if (!line.trim())
312
+ return;
313
+ try {
314
+ const event = JSON.parse(line);
315
+ const state = event.version === 1 ? readState(event.state) : undefined;
316
+ if (state)
317
+ visit(state);
318
+ }
319
+ catch (error) {
320
+ this.warn(`忽略损坏的审计记录 ${name}`, error);
321
+ }
322
+ };
323
+ try {
324
+ descriptor = fs.openSync(path.join(this.logDir, name), 'r');
325
+ const decoder = new StringDecoder('utf8');
326
+ const buffer = Buffer.alloc(64 * 1024);
327
+ let pending = '';
328
+ let skipping = false;
329
+ let bytes;
330
+ while ((bytes = fs.readSync(descriptor, buffer, 0, buffer.length, null)) > 0) {
331
+ pending += decoder.write(buffer.subarray(0, bytes));
332
+ let newline;
333
+ while ((newline = pending.indexOf('\n')) >= 0) {
334
+ if (!skipping && newline <= 64 * 1024)
335
+ consume(pending.slice(0, newline));
336
+ pending = pending.slice(newline + 1);
337
+ skipping = false;
338
+ }
339
+ if (pending.length > 64 * 1024) {
340
+ pending = '';
341
+ skipping = true;
342
+ this.warn(`忽略过长的审计记录 ${name}`, new Error('oversized record'));
343
+ }
344
+ }
345
+ if (!skipping)
346
+ consume(pending + decoder.end());
347
+ }
348
+ catch (error) {
349
+ this.warn(`历史审计读取失败 ${name}`, error);
350
+ }
351
+ finally {
352
+ if (descriptor !== undefined)
353
+ fs.closeSync(descriptor);
354
+ }
355
+ }
356
+ }
357
+ catch (error) {
358
+ this.warn('历史审计读取失败', error);
359
+ }
360
+ }
361
+ save() {
362
+ if (Date.now() - this.lastCleanup > 3_600_000) {
363
+ for (const [key, state] of this.states) {
364
+ if (this.retained(state))
365
+ continue;
366
+ this.states.delete(key);
367
+ const credentials = this.credentialsByHash.get(state.tokenHash);
368
+ credentials?.delete(key);
369
+ if (credentials?.size === 0)
370
+ this.credentialsByHash.delete(state.tokenHash);
371
+ }
372
+ this.lastCleanup = Date.now();
373
+ }
374
+ if (!this.persistenceEnabled())
375
+ return;
376
+ this.writeIndex();
377
+ }
378
+ writeIndex() {
379
+ try {
380
+ fs.mkdirSync(path.dirname(this.indexPath), { recursive: true, mode: 0o700 });
381
+ if (fs.existsSync(this.indexPath))
382
+ fs.chmodSync(this.indexPath, 0o600);
383
+ stableWriteJson(this.indexPath, { version: 1, states: [...this.states.values()].filter(state => this.retained(state)) });
384
+ }
385
+ catch (error) {
386
+ this.warn('诊断索引写入失败', error);
387
+ }
388
+ }
389
+ persistenceEnabled() {
390
+ let enabled = false;
391
+ try {
392
+ enabled = isDelegationDiagnosticsEnabled();
393
+ }
394
+ catch (error) {
395
+ this.warn('诊断开关读取失败,暂停持久化', error);
396
+ }
397
+ if (enabled && this.persistenceState === false)
398
+ this.writeIndex();
399
+ this.persistenceState = enabled;
400
+ if (enabled)
401
+ return true;
402
+ this.writer?.close();
403
+ this.writer = undefined;
404
+ return false;
405
+ }
406
+ warn(action, error) {
407
+ const code = error.code ?? 'storage_error';
408
+ const key = `${action}:${code}`;
409
+ if (Date.now() - (this.lastWarnings.get(key) ?? 0) < 60_000)
410
+ return;
411
+ this.lastWarnings.set(key, Date.now());
412
+ logger.warn(`[DelegationDiagnostics] ${action}: ${code}; index=${this.indexPath}; logs=${this.logDir}; 请检查文件权限、磁盘空间及诊断记录完整性;历史记录不参与授权。`);
413
+ }
414
+ }
@@ -331,14 +331,21 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
331
331
  };
332
332
  }
333
333
  let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
334
+ // `ec.thread.*` 是 agent-tool 专用的自数据操作:索引、会话、摘要都在
335
+ // `agents/<self>/data/threads/` 下,目标对端只是数据条目,不是访问边界。
336
+ // 因此只继承 `session.topic.list` 的"读"授权,不继承它的 currentRelationOnly /
337
+ // ownPeerOnly —— 那两条针对的是"当前关系下的会话视图"(menu/slash 场景),
338
+ // 套到话题链上会让 member 无法发现任何第三方话题。
334
339
  if (!matchResult && operation.startsWith('ec.thread.') && opMeta.category === 'read' && intent.scope === 'relation') {
335
340
  const permission = roleDef.commandPermissions?.['session.topic.list'];
336
- if (permission?.allow)
341
+ if (permission?.allow) {
342
+ const { currentRelationOnly: _relOnly, ownPeerOnly: _ownPeer, ...restConstraints } = permission.constraints ?? {};
337
343
  matchResult = {
338
- permission: { ...permission, scopes: ['relation'], constraints: { ...permission.constraints, currentRelationOnly: true, targetCurrentAgentOnly: true } },
344
+ permission: { ...permission, scopes: ['relation'], constraints: { ...restConstraints, targetCurrentAgentOnly: true } },
339
345
  matchedRule: 'session.topic.list',
340
346
  rank: 1000,
341
347
  };
348
+ }
342
349
  }
343
350
  // Agent self-service contact add deliberately reuses the same low-trust
344
351
  // relation boundary as ec.msg.send. Older per-Agent role files may still
@@ -768,6 +775,10 @@ function checkCurrentRelation(ctx, resolvedConfigCommand) {
768
775
  if (!ctx.selfAid || !requestedSelf || requestedSelf !== ctx.selfAid) {
769
776
  return { ok: false, reason: 'Only the current agent relation can be targeted' };
770
777
  }
778
+ // 话题作用域的历史读取(`ec msg history --thread <id>`)目标是话题本身,
779
+ // 不是当前关系的对端;消息日志仍落在调用方自己的会话目录里,因此不比对对端。
780
+ if (ctx.intent.operation === 'ec.msg.history' && stringArg(ctx.intent.args.threadId))
781
+ return { ok: true };
771
782
  const requestedPeer = resolvedConfigCommand?.peerKey || stringArg(ctx.intent.args.peerKey) || stringArg(ctx.intent.args.peer);
772
783
  if (!ctx.peerKey || !requestedPeer) {
773
784
  return { ok: false, reason: 'Current relation context is required' };
@@ -917,6 +928,14 @@ function checkOwnPeer(ctx) {
917
928
  const isMsgSend = ctx.intent.operation === 'ec.msg.send' || ctx.intent.operation === 'ec.msg.file';
918
929
  if (isMsgSend)
919
930
  return { ok: true };
931
+ // Same reasoning for `ec.thread.*`: args.peer is the *indexed peer whose local
932
+ // thread data is being read or written*. All of it lives under
933
+ // `agents/<self>/data/threads/`, so targeting a peer other than the current
934
+ // relation's counterpart does not cross a data boundary — it is the whole
935
+ // point of cross-topic routing. Ownership of the data stays with the caller,
936
+ // which `targetCurrentAgentOnly` already covers.
937
+ if (ctx.intent.operation.startsWith('ec.thread.'))
938
+ return { ok: true };
920
939
  if (ctx.actorId) {
921
940
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
922
941
  return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
@@ -951,7 +970,7 @@ function ownPeerOnlyDenialReason(ctx, requestedPeer) {
951
970
  const currentPeer = ctx.actorId || peerIdFromArg(ctx.peerKey || '') || '当前私聊对端';
952
971
  const target = requestedPeer || '指定目标';
953
972
  return `当前托管会话只能联系当前私聊对端(${currentPeer}),不能联系第三方目标(${target})。`
954
- + ' 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> "<失败原因>"(已有命令)将失败原因反馈给原会话;'
973
+ + ` 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> '<失败原因>'(已有命令)将失败原因反馈给原会话;`
955
974
  + '否则请让目标先直接发起会话,或由 Agent owner 为当前角色配置允许 relay/第三方外联后再发送。';
956
975
  }
957
976
  function peerIdFromArg(value) {
@@ -1207,6 +1207,22 @@ const OPERATIONS = [
1207
1207
  description: '通过 ec ctl file 向当前会话发送项目文件',
1208
1208
  sources: ['agent-tool'],
1209
1209
  },
1210
+ ...['grant', 'revoke'].map(action => ({
1211
+ id: `ec.ctl.path.${action}`,
1212
+ category: 'write-agent',
1213
+ dangerous: true,
1214
+ defaultScopes: ['relation', 'agent'],
1215
+ description: `${action} a runtime path grant for the current managed session (owner/admin only)`,
1216
+ sources: ['agent-tool'],
1217
+ })),
1218
+ {
1219
+ id: 'ec.ctl.path.list',
1220
+ category: 'read',
1221
+ dangerous: false,
1222
+ defaultScopes: ['relation'],
1223
+ description: 'List runtime path grants for the current managed session',
1224
+ sources: ['agent-tool'],
1225
+ },
1210
1226
  ...['read', 'clear', 'cancel', 'interrupt'].map(action => ({
1211
1227
  id: `ec.ctl.queue.${action}`,
1212
1228
  category: action === 'read' ? 'read' : 'write-own',
@@ -390,7 +390,7 @@ export function wrapChannelSend(inst, dependencies) {
390
390
  from: owningAgent?.aid ?? envelope.agentName ?? session.selfAID ?? 'self',
391
391
  to: String(target || envelope.channelId),
392
392
  sessionId: session.id,
393
- threadId: envelope.replyContext?.threadId || session.threadId || null,
393
+ threadId: envelope.replyContext?.threadId || session.threadId || '',
394
394
  chatType: isGroup ? 'group' : 'private',
395
395
  groupId: isGroup ? String(session.metadata?.groupId || session.channelId) : null,
396
396
  msgId: `${envelope.taskId || 'out'}_${Date.now()}`,
@@ -583,9 +583,24 @@ export async function execAgentUpdate(args, subject) {
583
583
  if (p.channels !== undefined && p.channelOwners !== undefined) {
584
584
  return { error: 'channels 与 channelOwners 不能同时提交', code: 'INVALID_ARGS' };
585
585
  }
586
- const config = cfgRead(ConfigTarget.Agent, { self: a.aid });
586
+ const config = cfgRead(ConfigTarget.Agent, { self: a.aid }, { cache: false });
587
587
  if (!config)
588
588
  return { error: `Agent "${a.aid}" not found`, code: 'NOT_FOUND' };
589
+ if (p.fullaccess !== undefined) {
590
+ if (!p.fullaccess || typeof p.fullaccess !== 'object' || Array.isArray(p.fullaccess)) {
591
+ return { error: 'fullaccess 必须是对象', code: 'INVALID_ARGS' };
592
+ }
593
+ const fullaccess = p.fullaccess;
594
+ if (Object.keys(fullaccess).some(key => key !== 'enabled')) {
595
+ return { error: 'fullaccess 只支持 enabled 字段', code: 'INVALID_ARGS' };
596
+ }
597
+ if (typeof fullaccess.enabled !== 'boolean') {
598
+ return { error: 'fullaccess.enabled 必须是布尔值', code: 'INVALID_ARGS' };
599
+ }
600
+ const denied = agentFullAccessWriteError(a.aid, subject);
601
+ if (denied)
602
+ return { error: denied, code: 'FORBIDDEN' };
603
+ }
589
604
  // Legacy App requests share the canonical validation, storage and agent.md publication flow.
590
605
  if (Object.hasOwn(p, 'avatar'))
591
606
  return execAidAvatar({ aid: a.aid, data: p.avatar });
@@ -640,27 +655,7 @@ export async function execAgentUpdate(args, subject) {
640
655
  touched = true;
641
656
  }
642
657
  if (p.fullaccess !== undefined) {
643
- if (!p.fullaccess || typeof p.fullaccess !== 'object' || Array.isArray(p.fullaccess)) {
644
- return { error: 'fullaccess 必须是对象', code: 'INVALID_ARGS' };
645
- }
646
- const fullaccess = p.fullaccess;
647
- if (Object.keys(fullaccess).some(key => key !== 'enabled')) {
648
- return { error: 'fullaccess 只支持 enabled 字段', code: 'INVALID_ARGS' };
649
- }
650
- if (fullaccess.enabled !== undefined && typeof fullaccess.enabled !== 'boolean') {
651
- return { error: 'fullaccess.enabled 必须是布尔值', code: 'INVALID_ARGS' };
652
- }
653
- if (typeof fullaccess.enabled !== 'boolean')
654
- return { error: 'fullaccess.enabled is required', code: 'INVALID_ARGS' };
655
- const denied = agentFullAccessWriteError({
656
- daemonOwner: subject?.isDaemonOwner === true,
657
- ecwebManagement: subject?.ecwebManagement === true,
658
- delegated: subject?.canApprovePersistentFullAccess === false || subject?.chatType === 'group',
659
- enabled: fullaccess.enabled,
660
- });
661
- if (denied)
662
- return { error: denied, code: 'FORBIDDEN' };
663
- config.fullaccess = { enabled: fullaccess.enabled };
658
+ config.fullaccess = { enabled: p.fullaccess.enabled };
664
659
  touched = true;
665
660
  }
666
661
  if (p.chatmode !== undefined) {
@@ -747,10 +742,7 @@ export async function execAgentQuery(args, service, subject) {
747
742
  safeConfig,
748
743
  fullaccessStatus: {
749
744
  ...fullaccessStatus,
750
- canConfigure: subject?.ecwebManagement === true
751
- && subject?.isDaemonOwner === true
752
- && subject.canApprovePersistentFullAccess !== false
753
- && subject.chatType !== 'group',
745
+ canConfigure: agentFullAccessWriteError(aid, subject) === undefined,
754
746
  },
755
747
  };
756
748
  // 叠加构建进度(create 受理后、ready 前可见;ready 后文件仍在,可反映软失败 warn)