evolcore 0.0.32 → 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.
- package/CHANGELOG.md +61 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +135 -37
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +111 -15
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +112 -54
- package/dist/agents/codex-runner.js +241 -63
- package/dist/agents/ecagent-runner.js +78 -33
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/channels/aun.js +44 -8
- package/dist/channels/daemon.js +261 -3
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +368 -31
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +197 -27
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +98 -67
- package/dist/cli/init.js +2 -2
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +166 -146
- package/dist/cli/queue-command.js +9 -4
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +78 -2
- package/dist/cli/task-context.js +1 -0
- package/dist/cli/thread-command.js +622 -0
- package/dist/cli/trigger-command.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +6 -3
- package/dist/config/config-manager.js +26 -2
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +45 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +133 -36
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +25 -2
- package/dist/core/auth/operation-catalog.js +49 -0
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +197 -27
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +51 -1
- package/dist/core/command/menu-handler.js +114 -23
- package/dist/core/command/menu-protocol.js +25 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +195 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +175 -3
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +83 -29
- package/dist/core/memory/consolidation-ops-records.js +49 -16
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +264 -205
- 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 +30 -15
- package/dist/core/message/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +169 -10
- package/dist/core/message/message-bridge.js +15 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +929 -110
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +217 -9
- 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 +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +129 -14
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +21 -4
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -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 +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +770 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +19 -7
- package/dist/ipc.js +140 -50
- package/dist/paths.js +93 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +13 -2
- package/dist/trigger/parser.js +70 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +143 -1
- 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 +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +40 -28
- package/kits/docs/evolcore/agent.md +21 -6
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +5 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +4 -6
- package/kits/docs/evolcore/group.md +11 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -15
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +8 -11
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -1
- package/kits/eck_manifest.json +55 -40
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +199 -0
- 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/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +7 -2
- package/kits/templates/memory-consolidation/generalize.md +9 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +8 -3
- package/kits/templates/memory-consolidation/promote.md +8 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +67 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +7 -6
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +6 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* debug-context.ts — 调试模式共享逻辑
|
|
3
|
+
*
|
|
4
|
+
* 供两端共用,不依赖 src/cli/:
|
|
5
|
+
* - CLI 侧:src/cli/debug-mode.ts(resolveDebugContext)
|
|
6
|
+
* - daemon 侧:src/ipc.ts(debug-issue-token IPC handler)
|
|
7
|
+
*
|
|
8
|
+
* 包含:
|
|
9
|
+
* - DebugDelegationRegistry:独立 grant 通道,token 带 dbg: 前缀,不影响主 registry
|
|
10
|
+
* - buildDebugTaskContext:从 Session 重建 TaskRuntimeContext(从 launch-consolidation.ts 迁移)
|
|
11
|
+
*/
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
14
|
+
import { logger } from '../../utils/logger.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// DebugDelegationRegistry
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** Token TTL for debug grants — longer than production (30 min vs 5 min). */
|
|
19
|
+
const DEBUG_TOKEN_TTL_MS = 30 * 60_000;
|
|
20
|
+
/** Prefix that identifies a debug delegation token at the validation entry point. */
|
|
21
|
+
export const DEBUG_TOKEN_PREFIX = 'dbg:';
|
|
22
|
+
function sha256(data) {
|
|
23
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Standalone delegation registry for debug mode.
|
|
27
|
+
*
|
|
28
|
+
* Uses a separate Map (debugGrantsByHash) so debug tokens never touch
|
|
29
|
+
* activeHashBySession in AgentDelegationRegistry, and issuing a debug token
|
|
30
|
+
* never triggers revokeSession on a live task.
|
|
31
|
+
*
|
|
32
|
+
* Token format: `dbg:<32-byte random hex>`
|
|
33
|
+
* Hash is computed over the raw part (after the prefix) only.
|
|
34
|
+
*/
|
|
35
|
+
export class DebugDelegationRegistry {
|
|
36
|
+
debugGrantsByHash = new Map();
|
|
37
|
+
issue(input) {
|
|
38
|
+
const raw = crypto.randomBytes(32).toString('hex');
|
|
39
|
+
const token = `${DEBUG_TOKEN_PREFIX}${raw}`;
|
|
40
|
+
const tokenHash = sha256(raw);
|
|
41
|
+
const grant = {
|
|
42
|
+
tokenHash,
|
|
43
|
+
sessionId: input.sessionId,
|
|
44
|
+
taskId: `debug-${Date.now()}`,
|
|
45
|
+
actorId: input.actorId ?? input.selfAid,
|
|
46
|
+
channel: input.channel,
|
|
47
|
+
channelType: input.channelType,
|
|
48
|
+
chatType: input.chatType,
|
|
49
|
+
selfAid: input.selfAid,
|
|
50
|
+
peerKey: input.peerKey,
|
|
51
|
+
issuedRole: input.issuedRole ?? 'debug',
|
|
52
|
+
expiresAt: Date.now() + DEBUG_TOKEN_TTL_MS,
|
|
53
|
+
};
|
|
54
|
+
this.debugGrantsByHash.set(tokenHash, grant);
|
|
55
|
+
logger.debug(`[DebugDelegation] issued token for session=${input.sessionId}`);
|
|
56
|
+
return token;
|
|
57
|
+
}
|
|
58
|
+
validate(token, sessionId) {
|
|
59
|
+
const raw = token.startsWith(DEBUG_TOKEN_PREFIX) ? token.slice(DEBUG_TOKEN_PREFIX.length) : token;
|
|
60
|
+
const tokenHash = sha256(raw);
|
|
61
|
+
const grant = this.debugGrantsByHash.get(tokenHash);
|
|
62
|
+
if (!grant) {
|
|
63
|
+
return { ok: false, code: 'INVALID_DELEGATION', reason: 'unknown debug token', diagnosticCause: 'unknown_token' };
|
|
64
|
+
}
|
|
65
|
+
if (grant.sessionId !== sessionId) {
|
|
66
|
+
return { ok: false, code: 'INVALID_DELEGATION', reason: 'session mismatch', diagnosticCause: 'session_mismatch' };
|
|
67
|
+
}
|
|
68
|
+
if (Date.now() > grant.expiresAt) {
|
|
69
|
+
this.debugGrantsByHash.delete(tokenHash);
|
|
70
|
+
return { ok: false, code: 'INVALID_DELEGATION', reason: 'debug token expired', diagnosticCause: 'task_replaced' };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, grant };
|
|
73
|
+
}
|
|
74
|
+
/** Remove all grants for a session (called when session is unbound). */
|
|
75
|
+
revokeSession(sessionId) {
|
|
76
|
+
for (const [hash, grant] of this.debugGrantsByHash) {
|
|
77
|
+
if (grant.sessionId === sessionId) {
|
|
78
|
+
this.debugGrantsByHash.delete(hash);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Prune expired grants. */
|
|
83
|
+
pruneExpired() {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
for (const [hash, grant] of this.debugGrantsByHash) {
|
|
86
|
+
if (now > grant.expiresAt)
|
|
87
|
+
this.debugGrantsByHash.delete(hash);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function buildDebugTaskContext(input) {
|
|
92
|
+
const { session } = input;
|
|
93
|
+
const meta = (session.metadata ?? {});
|
|
94
|
+
const sessionType = session.sessionType ?? meta.sessionType ?? 'main';
|
|
95
|
+
// channel = 类型名;channelKey = 完整实例 key。二者分开,
|
|
96
|
+
// 与 originFromActorSession 的 `session.metadata?.channelKey || session.channel` 同源。
|
|
97
|
+
let channel = session.channelType || session.channel;
|
|
98
|
+
let channelKey = meta.channelKey || session.channel;
|
|
99
|
+
let chatType = session.chatType || 'private';
|
|
100
|
+
let peerId = meta.peerId;
|
|
101
|
+
let peerKeyId = chatType === 'group'
|
|
102
|
+
? (meta.groupId || session.channelId)
|
|
103
|
+
: (meta.peerId || session.channelId);
|
|
104
|
+
let threadId = session.threadId || undefined;
|
|
105
|
+
if (session.channelId?.startsWith('trigger:') && session.selfAID && input.getTrigger) {
|
|
106
|
+
const triggerId = session.channelId.slice('trigger:'.length);
|
|
107
|
+
const origin = input.getTrigger(session.selfAID, triggerId)?.origin;
|
|
108
|
+
if (origin?.channelType) {
|
|
109
|
+
channel = origin.channelType;
|
|
110
|
+
if (origin.channelKey)
|
|
111
|
+
channelKey = origin.channelKey;
|
|
112
|
+
chatType = origin.delivery?.chatType ?? 'private';
|
|
113
|
+
peerId = origin.peerId;
|
|
114
|
+
peerKeyId = chatType === 'group' ? origin.channelId : (origin.peerId ?? origin.channelId);
|
|
115
|
+
threadId = origin.threadId;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
sessionId: session.id,
|
|
120
|
+
taskId: `debug-${Date.now()}`,
|
|
121
|
+
messageId: `debug-msg-${Date.now()}`,
|
|
122
|
+
baseagent: session.baseagent,
|
|
123
|
+
channel,
|
|
124
|
+
channelKey,
|
|
125
|
+
channelId: session.channelId,
|
|
126
|
+
chatType,
|
|
127
|
+
selfAid: session.selfAID,
|
|
128
|
+
peerId,
|
|
129
|
+
peerKey: channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined,
|
|
130
|
+
peerName: meta.peerName,
|
|
131
|
+
peerType: meta.peerType,
|
|
132
|
+
threadId,
|
|
133
|
+
sessionType,
|
|
134
|
+
memoryMode: meta.memoryMode,
|
|
135
|
+
memoryScope: meta.memoryScope,
|
|
136
|
+
consolidationBatchDir: meta.consolidationBatchDir ?? meta.consolidationLogBase,
|
|
137
|
+
sessionRuntimeDir: input.sessionRuntimeDir,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -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,6 +331,15 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
let matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
|
|
334
|
+
if (!matchResult && operation.startsWith('ec.thread.') && opMeta.category === 'read' && intent.scope === 'relation') {
|
|
335
|
+
const permission = roleDef.commandPermissions?.['session.topic.list'];
|
|
336
|
+
if (permission?.allow)
|
|
337
|
+
matchResult = {
|
|
338
|
+
permission: { ...permission, scopes: ['relation'], constraints: { ...permission.constraints, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
339
|
+
matchedRule: 'session.topic.list',
|
|
340
|
+
rank: 1000,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
334
343
|
// Agent self-service contact add deliberately reuses the same low-trust
|
|
335
344
|
// relation boundary as ec.msg.send. Older per-Agent role files may still
|
|
336
345
|
// contain the historical contact.* deny without an explicit contact.add
|
|
@@ -461,6 +470,14 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
|
|
|
461
470
|
if (operation === 'agent.reload') {
|
|
462
471
|
return !isCurrentAgentTarget(ctx);
|
|
463
472
|
}
|
|
473
|
+
// Same shape as agent.reload: the current Agent's owner/admin refresh their
|
|
474
|
+
// own skill index; refreshing another Agent's index needs the daemon owner.
|
|
475
|
+
if (operation === 'skill.refresh' && hasDaemonOwnerRole(ctx)) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
if (operation === 'skill.refresh') {
|
|
479
|
+
return !isCurrentAgentTarget(ctx);
|
|
480
|
+
}
|
|
464
481
|
if (operation.startsWith('trigger.')) {
|
|
465
482
|
if (ctx.fromControlChannel)
|
|
466
483
|
return true;
|
|
@@ -478,7 +495,7 @@ function hasDaemonOwnerRole(ctx) {
|
|
|
478
495
|
|| ctx.processRole === 'daemon-service';
|
|
479
496
|
}
|
|
480
497
|
function daemonOwnerDenialReasonCode(ctx) {
|
|
481
|
-
if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload')
|
|
498
|
+
if ((ctx.intent.operation === 'agent.show' || ctx.intent.operation === 'agent.reload' || ctx.intent.operation === 'skill.refresh')
|
|
482
499
|
&& ctx.intent.scope !== 'control'
|
|
483
500
|
&& !ctx.fromControlChannel
|
|
484
501
|
&& !isCurrentAgentTarget(ctx)) {
|
|
@@ -894,6 +911,12 @@ function checkOwnPeer(ctx) {
|
|
|
894
911
|
? { ok: true }
|
|
895
912
|
: { ok: false, reason: 'Only the current group relation can be targeted' };
|
|
896
913
|
}
|
|
914
|
+
// For ec.msg.send / ec.msg.file, args.peer is the recipient (to), not a
|
|
915
|
+
// self-targeting field. Cross-topic sends are checked against their indexed
|
|
916
|
+
// target session by the managed message operation.
|
|
917
|
+
const isMsgSend = ctx.intent.operation === 'ec.msg.send' || ctx.intent.operation === 'ec.msg.file';
|
|
918
|
+
if (isMsgSend)
|
|
919
|
+
return { ok: true };
|
|
897
920
|
if (ctx.actorId) {
|
|
898
921
|
if (requestedPeerId && requestedPeerId !== ctx.actorId) {
|
|
899
922
|
return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
|
|
@@ -928,7 +951,7 @@ function ownPeerOnlyDenialReason(ctx, requestedPeer) {
|
|
|
928
951
|
const currentPeer = ctx.actorId || peerIdFromArg(ctx.peerKey || '') || '当前私聊对端';
|
|
929
952
|
const target = requestedPeer || '指定目标';
|
|
930
953
|
return `当前托管会话只能联系当前私聊对端(${currentPeer}),不能联系第三方目标(${target})。`
|
|
931
|
-
+
|
|
954
|
+
+ ` 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> '<失败原因>'(已有命令)将失败原因反馈给原会话;`
|
|
932
955
|
+ '否则请让目标先直接发起会话,或由 Agent owner 为当前角色配置允许 relay/第三方外联后再发送。';
|
|
933
956
|
}
|
|
934
957
|
function peerIdFromArg(value) {
|
|
@@ -307,6 +307,22 @@ const OPERATIONS = [
|
|
|
307
307
|
description: '查看当前会话状态',
|
|
308
308
|
sources: ['slash', 'menu', 'agent-tool'],
|
|
309
309
|
},
|
|
310
|
+
{
|
|
311
|
+
id: 'session.loop.current',
|
|
312
|
+
category: 'read',
|
|
313
|
+
dangerous: false,
|
|
314
|
+
defaultScopes: ['relation'],
|
|
315
|
+
description: '查看当前主会话或话题的 idle 触发设置',
|
|
316
|
+
sources: ['slash', 'menu', 'ecweb', 'control'],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: 'session.loop.update',
|
|
320
|
+
category: 'write-own',
|
|
321
|
+
dangerous: false,
|
|
322
|
+
defaultScopes: ['relation'],
|
|
323
|
+
description: '更新当前主会话或话题的 idle 触发设置',
|
|
324
|
+
sources: ['slash', 'menu', 'ecweb', 'control'],
|
|
325
|
+
},
|
|
310
326
|
{
|
|
311
327
|
id: 'session.topic.current',
|
|
312
328
|
category: 'read',
|
|
@@ -767,6 +783,23 @@ const OPERATIONS = [
|
|
|
767
783
|
description: '重载 agent 配置',
|
|
768
784
|
sources: ['slash', 'menu', 'agent-tool', 'control'],
|
|
769
785
|
},
|
|
786
|
+
{
|
|
787
|
+
id: 'skill.refresh',
|
|
788
|
+
category: 'write-agent',
|
|
789
|
+
dangerous: false,
|
|
790
|
+
defaultScopes: ['agent', 'control'],
|
|
791
|
+
description: '重建技能索引文件',
|
|
792
|
+
sources: ['agent-tool', 'control'],
|
|
793
|
+
},
|
|
794
|
+
...['list', 'list-peers', 'show', 'history', 'check-index', 'summary-show', 'summary-log', 'create', 'rename', 'label', 'status', 'delete', 'summary-set', 'summary-clear', 'rebuild-index'].map(action => ({
|
|
795
|
+
id: `ec.thread.${action}`,
|
|
796
|
+
category: (['list', 'list-peers', 'show', 'history', 'check-index', 'summary-show', 'summary-log'].includes(action)
|
|
797
|
+
? 'read' : action === 'rebuild-index' ? 'write-agent' : 'write-own'),
|
|
798
|
+
dangerous: false,
|
|
799
|
+
defaultScopes: ['relation', 'agent'],
|
|
800
|
+
description: `${action} managed conversation threads`,
|
|
801
|
+
sources: ['agent-tool'],
|
|
802
|
+
})),
|
|
770
803
|
{
|
|
771
804
|
id: 'agent.bootstrapComplete',
|
|
772
805
|
category: 'write-agent',
|
|
@@ -1174,6 +1207,22 @@ const OPERATIONS = [
|
|
|
1174
1207
|
description: '通过 ec ctl file 向当前会话发送项目文件',
|
|
1175
1208
|
sources: ['agent-tool'],
|
|
1176
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
|
+
},
|
|
1177
1226
|
...['read', 'clear', 'cancel', 'interrupt'].map(action => ({
|
|
1178
1227
|
id: `ec.ctl.queue.${action}`,
|
|
1179
1228
|
category: action === 'read' ? 'read' : 'write-own',
|