evolcore 0.0.33 → 0.0.34

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 (129) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +12 -1
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +82 -13
  6. package/dist/agents/codex/diagnostics.js +37 -11
  7. package/dist/agents/codex-runner.js +218 -44
  8. package/dist/agents/ecagent-runner.js +73 -20
  9. package/dist/agents/runner-types.js +5 -6
  10. package/dist/aun/aid/identity.js +1 -1
  11. package/dist/channels/aun.js +16 -6
  12. package/dist/channels/daemon.js +6 -3
  13. package/dist/cli/aun-commands.js +3 -2
  14. package/dist/cli/ctl-command.js +3 -1
  15. package/dist/cli/daemon-commands.js +120 -19
  16. package/dist/cli/fs-command.js +2 -1
  17. package/dist/cli/index.js +20 -6
  18. package/dist/cli/integrity-command.js +196 -0
  19. package/dist/cli/memory-command.js +158 -145
  20. package/dist/cli/thread-command.js +5 -3
  21. package/dist/cli/trigger-command.js +6 -1
  22. package/dist/config/config-field-policy.js +4 -0
  23. package/dist/config/config-manager.js +19 -0
  24. package/dist/config/contact-book-store.js +5 -0
  25. package/dist/config/role-service.js +13 -0
  26. package/dist/config/runtime-env.js +16 -0
  27. package/dist/config-store.js +30 -0
  28. package/dist/core/audit/log-integrity.js +3 -1
  29. package/dist/core/auth/agent-delegation.js +108 -35
  30. package/dist/core/auth/delegation-diagnostics.js +414 -0
  31. package/dist/core/auth/operation-authorizer.js +1 -1
  32. package/dist/core/auth/operation-catalog.js +16 -0
  33. package/dist/core/command/agent-control.js +18 -26
  34. package/dist/core/command/command-handler.js +138 -4
  35. package/dist/core/command/daemon-diagnostics.js +1 -0
  36. package/dist/core/command/menu-catalog.js +3 -0
  37. package/dist/core/command/menu-handler.js +22 -7
  38. package/dist/core/command/menu-protocol.js +1 -0
  39. package/dist/core/command/slash-handler.js +2 -0
  40. package/dist/core/daemon-bootstrap.js +4 -7
  41. package/dist/core/file-integrity/read.js +61 -0
  42. package/dist/core/file-integrity/store.js +262 -0
  43. package/dist/core/ipc-registration.js +36 -1
  44. package/dist/core/memory/batch-meta.js +132 -0
  45. package/dist/core/memory/consolidation-context.js +35 -14
  46. package/dist/core/memory/consolidation-ops-records.js +14 -16
  47. package/dist/core/memory/consolidation.js +91 -26
  48. package/dist/core/memory/index-manager.js +19 -28
  49. package/dist/core/memory/memory-admin.js +19 -14
  50. package/dist/core/memory/memory-document.js +53 -9
  51. package/dist/core/memory/memory-writer.js +30 -15
  52. package/dist/core/message/im-renderer.js +20 -4
  53. package/dist/core/message/message-bridge.js +2 -1
  54. package/dist/core/message/response-engine.js +584 -47
  55. package/dist/core/permission/ec-command-parser.js +93 -4
  56. package/dist/core/permission/protected-paths.js +5 -1
  57. package/dist/core/permission/readonly-shell-query.js +43 -5
  58. package/dist/core/permission/session-path-grants.js +334 -0
  59. package/dist/core/permission/tool-canonical.js +226 -0
  60. package/dist/core/permission/tool-policy.js +30 -62
  61. package/dist/core/role/runtime-policy.js +21 -10
  62. package/dist/core/session/session-fs-store.js +123 -8
  63. package/dist/core/session/session-manager.js +75 -5
  64. package/dist/core/skill/index-generator.js +17 -3
  65. package/dist/core/system-session/grants.js +40 -0
  66. package/dist/core/system-session/memory.js +222 -0
  67. package/dist/core/system-session/registry.js +74 -0
  68. package/dist/core/system-session/types.js +14 -0
  69. package/dist/core/thread/thread-service.js +13 -20
  70. package/dist/eck/kit-renderer.js +7 -2
  71. package/dist/eck/manifest-engine.js +24 -4
  72. package/dist/index.js +11 -2
  73. package/dist/ipc.js +51 -40
  74. package/dist/paths.js +70 -0
  75. package/dist/stats/billing.js +20 -8
  76. package/dist/trigger/feedback.js +6 -1
  77. package/dist/trigger/parser.js +8 -1
  78. package/dist/trigger/system-definitions.js +3 -3
  79. package/dist/utils/cross-platform.js +11 -2
  80. package/dist/utils/error-dict.json +18 -4
  81. package/dist/utils/error-utils.js +121 -0
  82. package/dist/utils/instance-registry.js +13 -8
  83. package/dist/utils/log-writer.js +12 -0
  84. package/dist/utils/process-introspect.js +11 -0
  85. package/kits/docs/context-assembly.md +0 -1
  86. package/kits/docs/evolcore/agent-create.md +5 -4
  87. package/kits/docs/evolcore/agent.md +2 -2
  88. package/kits/docs/evolcore/config.md +2 -2
  89. package/kits/docs/evolcore/ctl.md +2 -2
  90. package/kits/docs/evolcore/ec-capabilities.md +1 -1
  91. package/kits/docs/evolcore/fs-architecture.md +4 -5
  92. package/kits/docs/evolcore/fs.md +1 -1
  93. package/kits/docs/evolcore/group-collaboration.md +2 -2
  94. package/kits/docs/evolcore/group.md +10 -10
  95. package/kits/docs/evolcore/memory-architecture.md +4 -5
  96. package/kits/docs/evolcore/memory-consolidation.md +18 -14
  97. package/kits/docs/evolcore/memory-tags.md +3 -1
  98. package/kits/docs/evolcore/memory.md +24 -12
  99. package/kits/docs/evolcore/model.md +16 -0
  100. package/kits/docs/evolcore/msg.md +7 -7
  101. package/kits/docs/evolcore/thread-chain.md +5 -5
  102. package/kits/docs/prompt-loading-architecture.md +0 -1
  103. package/kits/eck_manifest.json +0 -20
  104. package/kits/eck_manifest.memory.json +0 -12
  105. package/kits/rules/01-overview.md +11 -0
  106. package/kits/rules/03-identity.md +6 -3
  107. package/kits/rules/06-channel.md +1 -1
  108. package/kits/rules/07-memory-and-thread.md +12 -15
  109. package/kits/schemas/_meta.json +7 -1
  110. package/kits/schemas/agent-config.schema.13.json +1 -1
  111. package/kits/schemas/daemon.schema.7.json +140 -0
  112. package/kits/skills/ec-group/SKILL.md +4 -4
  113. package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
  114. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  115. package/kits/templates/memory-consolidation/forget.md +2 -2
  116. package/kits/templates/memory-consolidation/generalize.md +4 -4
  117. package/kits/templates/memory-consolidation/notes.md +11 -11
  118. package/kits/templates/memory-consolidation/profile.md +3 -3
  119. package/kits/templates/memory-consolidation/promote.md +3 -3
  120. package/kits/templates/memory-consolidation/workflow.md +6 -6
  121. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  122. package/kits/templates/system-fragments/bootstrap.md +4 -3
  123. package/kits/templates/system-fragments/channel.md +2 -2
  124. package/kits/templates/system-fragments/memory.md +16 -9
  125. package/kits/templates/system-fragments/reply.md +6 -6
  126. package/kits/templates/system-fragments/session.md +1 -1
  127. package/package.json +4 -3
  128. package/dist/core/memory/memory-session-policy.js +0 -41
  129. 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
+ }
@@ -951,7 +951,7 @@ function ownPeerOnlyDenialReason(ctx, requestedPeer) {
951
951
  const currentPeer = ctx.actorId || peerIdFromArg(ctx.peerKey || '') || '当前私聊对端';
952
952
  const target = requestedPeer || '指定目标';
953
953
  return `当前托管会话只能联系当前私聊对端(${currentPeer}),不能联系第三方目标(${target})。`
954
- + ' 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> "<失败原因>"(已有命令)将失败原因反馈给原会话;'
954
+ + ` 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> '<失败原因>'(已有命令)将失败原因反馈给原会话;`
955
955
  + '否则请让目标先直接发起会话,或由 Agent owner 为当前角色配置允许 relay/第三方外联后再发送。';
956
956
  }
957
957
  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',
@@ -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)