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.
- package/CHANGELOG.md +59 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +38 -4
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +93 -14
- package/dist/agents/codex/diagnostics.js +79 -22
- package/dist/agents/codex-runner.js +245 -47
- package/dist/agents/ecagent-runner.js +90 -27
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +123 -7
- package/dist/channels/daemon.js +6 -3
- package/dist/cli/aun-commands.js +39 -14
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +130 -23
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +26 -6
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +200 -156
- package/dist/cli/thread-command.js +17 -7
- package/dist/cli/trigger-command.js +6 -1
- package/dist/config/config-field-policy.js +4 -0
- package/dist/config/config-manager.js +19 -0
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +30 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +108 -35
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +22 -3
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +145 -5
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +24 -7
- package/dist/core/command/menu-protocol.js +1 -0
- package/dist/core/command/slash-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/ipc-registration.js +47 -2
- package/dist/core/memory/batch-meta.js +133 -0
- package/dist/core/memory/consolidation-context.js +132 -28
- package/dist/core/memory/consolidation-ops-records.js +14 -16
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +146 -26
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +50 -19
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/im-renderer.js +20 -4
- package/dist/core/message/message-bridge.js +5 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +598 -50
- package/dist/core/permission/ec-command-parser.js +169 -44
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +60 -62
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-manager.js +131 -6
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/skill/index-generator.js +17 -3
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +289 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +258 -51
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/index.js +11 -2
- package/dist/ipc.js +51 -40
- package/dist/paths.js +70 -0
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/feedback.js +6 -1
- package/dist/trigger/parser.js +8 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/utils/cross-platform.js +17 -5
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +0 -1
- package/kits/docs/evolcore/agent-create.md +5 -4
- package/kits/docs/evolcore/agent.md +2 -2
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +1 -1
- package/kits/docs/evolcore/fs-architecture.md +9 -10
- package/kits/docs/evolcore/fs.md +2 -2
- package/kits/docs/evolcore/group-collaboration.md +3 -3
- package/kits/docs/evolcore/group.md +14 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -16
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +7 -7
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +30 -14
- package/kits/docs/prompt-loading-architecture.md +0 -1
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -20
- package/kits/eck_manifest.memory.json +20 -12
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +13 -16
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/skills/ec-group/SKILL.md +8 -5
- package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/forget.md +2 -2
- package/kits/templates/memory-consolidation/generalize.md +4 -4
- package/kits/templates/memory-consolidation/notes.md +12 -12
- package/kits/templates/memory-consolidation/profile.md +3 -3
- package/kits/templates/memory-consolidation/promote.md +3 -3
- package/kits/templates/memory-consolidation/workflow.md +42 -7
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/bootstrap.md +4 -3
- package/kits/templates/system-fragments/channel.md +2 -2
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +6 -6
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +4 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- 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: { ...
|
|
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
|
-
+
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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)
|