evolcore 0.0.22 → 0.0.24
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 +74 -14
- package/README.md +4 -0
- package/bin/codex-managed-acl.ps1 +160 -0
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +384 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +30 -2
- package/dist/agents/claude-runner.js +179 -240
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +369 -0
- package/dist/agents/codex-runner.js +839 -192
- package/dist/agents/ecagent-runner.js +27 -34
- package/dist/agents/gemini-runner.js +62 -8
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +43 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +57 -5
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +7 -1
- package/dist/channels/aun.js +178 -77
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wechat.js +81 -10
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +108 -24
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +289 -61
- package/dist/cli/index.js +22 -2
- package/dist/cli/init.js +2 -10
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +75 -18
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +14 -2
- package/dist/core/auth/remote-access-store.js +136 -0
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +128 -30
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +6 -1
- package/dist/core/command/menu-handler.js +410 -98
- package/dist/core/command/menu-protocol.js +123 -0
- package/dist/core/command/slash-gate.js +4 -6
- package/dist/core/command/slash-handler.js +199 -70
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +147 -94
- package/dist/core/model/catalog-service.js +310 -0
- package/dist/core/model/model-catalog.js +134 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +5 -2
- package/dist/core/permission/approval-gateway.js +183 -32
- package/dist/core/permission/protected-paths.js +63 -4
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-policy.js +257 -31
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +214 -57
- package/dist/ipc.js +191 -102
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +16 -2
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +70 -15
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +13 -0
- package/kits/docs/evolcore/group-collaboration.md +7 -2
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +16 -4
- package/dist/utils/codex-cli.js +0 -43
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { AgentHarness, JsonlSessionRepo, NodeExecutionEnv, } from '../../ecagent/dist/node.js';
|
|
@@ -20,13 +21,18 @@ const PROVIDER_ID = 'evolcore-gateway';
|
|
|
20
21
|
const DEFAULT_SYSTEM_PROMPT = `You are ecagent, the coding agent built into EvolCore.
|
|
21
22
|
Work directly in the current project. Inspect relevant files before changing them, keep edits scoped, and verify the result.
|
|
22
23
|
Use the provided tools for filesystem and shell work. Never claim a tool action succeeded unless its result confirms success.`;
|
|
23
|
-
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
24
|
-
const MODEL_LIST_TIMEOUT_MS = 8_000;
|
|
25
24
|
const MAX_READ_BYTES = 1024 * 1024;
|
|
26
25
|
const MAX_TOOL_OUTPUT = 200_000;
|
|
27
26
|
const MAX_SCAN_FILES = 5_000;
|
|
28
27
|
const MAX_SEARCH_RESULTS = 500;
|
|
29
28
|
const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
29
|
+
/**
|
|
30
|
+
* 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
|
|
31
|
+
* Ecagent/OpenAI API 不认识这些后缀,必须传入标准模型 ID
|
|
32
|
+
*/
|
|
33
|
+
function stripContextWindowSuffix(modelId) {
|
|
34
|
+
return modelId.replace(/\[(?:1m|200k)\]$/i, '');
|
|
35
|
+
}
|
|
30
36
|
class AsyncEventQueue {
|
|
31
37
|
values = [];
|
|
32
38
|
waiters = [];
|
|
@@ -705,7 +711,6 @@ export class EcagentRunner {
|
|
|
705
711
|
permissionGateway;
|
|
706
712
|
sendPromptFn;
|
|
707
713
|
onCompactStart;
|
|
708
|
-
modelCache;
|
|
709
714
|
constructor(config, callbacks, runtime) {
|
|
710
715
|
this.config = {
|
|
711
716
|
...config,
|
|
@@ -716,7 +721,7 @@ export class EcagentRunner {
|
|
|
716
721
|
configuredHeaders: config.headers,
|
|
717
722
|
}),
|
|
718
723
|
};
|
|
719
|
-
this.model = config.model;
|
|
724
|
+
this.model = stripContextWindowSuffix(config.model);
|
|
720
725
|
this.effort = config.effort;
|
|
721
726
|
this.agentAid = runtime?.agentAid;
|
|
722
727
|
this.onSessionIdUpdate = callbacks?.onSessionIdUpdate;
|
|
@@ -726,7 +731,7 @@ export class EcagentRunner {
|
|
|
726
731
|
throw new Error('ecagent requires an EvolCore Agent AID');
|
|
727
732
|
return agentSessionsDir(this.agentAid);
|
|
728
733
|
}
|
|
729
|
-
setModel(model) { this.model = model; }
|
|
734
|
+
setModel(model) { this.model = stripContextWindowSuffix(model); }
|
|
730
735
|
getModel() { return this.model; }
|
|
731
736
|
resolveModelId(model) { return model; }
|
|
732
737
|
setEffort(effort) { this.effort = effort; }
|
|
@@ -745,35 +750,16 @@ export class EcagentRunner {
|
|
|
745
750
|
{ key: 'bypass', nameZh: '放行', description: '普通审批清单自动执行,强危险操作仍需审批', available: true },
|
|
746
751
|
];
|
|
747
752
|
}
|
|
753
|
+
getModelCatalog() {
|
|
754
|
+
return getRunnerCatalog('ecagent', this.agentAid, this.config);
|
|
755
|
+
}
|
|
748
756
|
async listModels() {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
headers: {
|
|
756
|
-
authorization: `Bearer ${this.config.apiKey}`,
|
|
757
|
-
...this.config.headers,
|
|
758
|
-
},
|
|
759
|
-
signal,
|
|
760
|
-
});
|
|
761
|
-
if (!response.ok)
|
|
762
|
-
throw new Error(`Failed to list ecagent models: HTTP ${response.status}`);
|
|
763
|
-
const payload = await response.json();
|
|
764
|
-
const ids = [...new Set((payload.data ?? [])
|
|
765
|
-
.map(item => typeof item.id === 'string' ? item.id : '')
|
|
766
|
-
.filter(id => id.startsWith('claude-') || (id.startsWith('gpt-') && !id.startsWith('gpt-image-'))))];
|
|
767
|
-
if (ids.length === 0)
|
|
768
|
-
throw new Error('Gateway returned no supported Claude or GPT models');
|
|
769
|
-
this.modelCache = { ids, fetchedAt: Date.now() };
|
|
770
|
-
return [...ids];
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
if (stale && stale.length > 0)
|
|
774
|
-
return [...stale];
|
|
775
|
-
throw error;
|
|
776
|
-
}
|
|
757
|
+
const catalog = await this.getModelCatalog();
|
|
758
|
+
assertCatalogSelectable(catalog);
|
|
759
|
+
return catalog.models.map(m => m.id);
|
|
760
|
+
}
|
|
761
|
+
async getAvailableEfforts(model) {
|
|
762
|
+
return catalogEfforts(await this.getModelCatalog(), model, 'ecagent');
|
|
777
763
|
}
|
|
778
764
|
registerStream(key, stream) { this.activeStreams.set(key, stream); }
|
|
779
765
|
cleanupStream(key) {
|
|
@@ -1016,6 +1002,13 @@ export class EcagentRunner {
|
|
|
1016
1002
|
return runtime;
|
|
1017
1003
|
}
|
|
1018
1004
|
mapHarnessEvent(queue, event, stats, model, effort) {
|
|
1005
|
+
if (event.type === 'message_update'
|
|
1006
|
+
&& (event.assistantMessageEvent.type === 'thinking_start'
|
|
1007
|
+
|| event.assistantMessageEvent.type === 'thinking_delta'
|
|
1008
|
+
|| event.assistantMessageEvent.type === 'thinking_end')) {
|
|
1009
|
+
queue.push({ type: 'heartbeat', source: 'reasoning', turn: stats.numTurns + 1 });
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1019
1012
|
if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
|
|
1020
1013
|
if (!stats.firstTextAt)
|
|
1021
1014
|
stats.firstTextAt = Date.now();
|
|
@@ -1223,7 +1216,7 @@ export class EcagentRunner {
|
|
|
1223
1216
|
? cached
|
|
1224
1217
|
: await this.openExistingRuntime(agentSessionId, cwd);
|
|
1225
1218
|
this.onCompactStart?.(sessionId);
|
|
1226
|
-
const compactModel = modelOverride?.model || this.model;
|
|
1219
|
+
const compactModel = stripContextWindowSuffix(modelOverride?.model || this.model);
|
|
1227
1220
|
const compactEffort = modelOverride?.effortMode === 'model_default'
|
|
1228
1221
|
? undefined
|
|
1229
1222
|
: modelOverride?.effort ?? this.effort;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCatalog, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
1
2
|
/**
|
|
2
3
|
* Gemini Agent Runner
|
|
3
4
|
*
|
|
@@ -23,6 +24,7 @@ import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../co
|
|
|
23
24
|
import { workspaceContainsHClassPaths } from '../core/permission/protected-paths.js';
|
|
24
25
|
import { buildBubblewrapCommand } from '../core/permission/sandbox-runtime.js';
|
|
25
26
|
import { getManagedTaskTempDir } from '../cli/task-context.js';
|
|
27
|
+
import { auditPermissionLifecycle } from '../core/auth/authorization-audit.js';
|
|
26
28
|
// Strip ANSI escape codes from Gemini CLI text output.
|
|
27
29
|
// Gemini embeds raw terminal colors from tool stdout (e.g. vitest, npm)
|
|
28
30
|
// into its assistant text, unlike Claude SDK which strips them internally.
|
|
@@ -31,6 +33,13 @@ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x1b\\|\x07)/g;
|
|
|
31
33
|
function stripAnsi(text) {
|
|
32
34
|
return text.replace(ANSI_RE, '');
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
|
|
38
|
+
* Gemini API 不认识这些后缀,必须传入标准模型 ID
|
|
39
|
+
*/
|
|
40
|
+
function stripContextWindowSuffix(modelId) {
|
|
41
|
+
return modelId.replace(/\[(?:1m|200k)\]$/i, '');
|
|
42
|
+
}
|
|
34
43
|
// ── MIME → 扩展名映射 ──
|
|
35
44
|
const MIME_EXT = {
|
|
36
45
|
'image/png': '.png',
|
|
@@ -39,11 +48,6 @@ const MIME_EXT = {
|
|
|
39
48
|
'image/webp': '.webp',
|
|
40
49
|
};
|
|
41
50
|
// ── Gemini 模型列表 ──
|
|
42
|
-
const GEMINI_MODELS = [
|
|
43
|
-
'gemini-2.5-pro',
|
|
44
|
-
'gemini-2.5-flash',
|
|
45
|
-
'gemini-2.5-flash-lite',
|
|
46
|
-
];
|
|
47
51
|
const GEMINI_READONLY_TOOLS = [
|
|
48
52
|
'read_file', 'list_directory', 'glob', 'grep_search', 'google_web_search',
|
|
49
53
|
'codebase_investigator', 'cli_help', 'get_internal_docs', 'update_topic', 'complete_task',
|
|
@@ -290,13 +294,13 @@ export class GeminiRunner {
|
|
|
290
294
|
permissionContexts = new Map();
|
|
291
295
|
constructor(config, callbacks) {
|
|
292
296
|
this.resolved = resolveGoogleConfig(config);
|
|
293
|
-
this.model = this.resolved.model;
|
|
297
|
+
this.model = stripContextWindowSuffix(this.resolved.model);
|
|
294
298
|
this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
|
|
295
299
|
}
|
|
296
300
|
// ── ModelSwitcher ──
|
|
297
|
-
setModel(model) { this.model = model; }
|
|
301
|
+
setModel(model) { this.model = stripContextWindowSuffix(model); }
|
|
298
302
|
getModel() { return this.model; }
|
|
299
|
-
listModels() { return
|
|
303
|
+
async listModels() { const catalog = await getCatalog(undefined, 'gemini'); assertCatalogSelectable(catalog); return catalog.models.map(m => m.id); }
|
|
300
304
|
// ── Effort (not applicable) ──
|
|
301
305
|
setEffort(_effort) { }
|
|
302
306
|
getEffort() { return undefined; }
|
|
@@ -691,6 +695,56 @@ export class GeminiRunner {
|
|
|
691
695
|
logger.info(`[GeminiRunner] interaction.auto_resolved kind=${normalizeGeminiToolName(toolName)} session=${sessionId}`);
|
|
692
696
|
return;
|
|
693
697
|
}
|
|
698
|
+
const lifecycleRequestId = requestId ?? `gemini-approval-${crypto.randomUUID()}`;
|
|
699
|
+
const auditSummary = `${toolName} runtime authorization denied by Gemini headless policy`;
|
|
700
|
+
auditPermissionLifecycle({
|
|
701
|
+
phase: 'request',
|
|
702
|
+
requestId: lifecycleRequestId,
|
|
703
|
+
sessionId,
|
|
704
|
+
toolName,
|
|
705
|
+
decision: 'pending',
|
|
706
|
+
decisionSource: 'approval',
|
|
707
|
+
permissionMode: context.permissionMode,
|
|
708
|
+
agentAid: context.selfAid,
|
|
709
|
+
agentName: context.agentName,
|
|
710
|
+
role: context.role,
|
|
711
|
+
taskId: context.taskId,
|
|
712
|
+
summary: auditSummary,
|
|
713
|
+
});
|
|
714
|
+
auditPermissionLifecycle({
|
|
715
|
+
phase: 'response',
|
|
716
|
+
requestId: lifecycleRequestId,
|
|
717
|
+
sessionId,
|
|
718
|
+
toolName,
|
|
719
|
+
decision: 'deny',
|
|
720
|
+
decisionSource: 'policy',
|
|
721
|
+
policyCode: 'permission_mode_denied',
|
|
722
|
+
reason: auditSummary,
|
|
723
|
+
permissionMode: context.permissionMode,
|
|
724
|
+
agentAid: context.selfAid,
|
|
725
|
+
agentName: context.agentName,
|
|
726
|
+
role: context.role,
|
|
727
|
+
taskId: context.taskId,
|
|
728
|
+
summary: auditSummary,
|
|
729
|
+
response: 'deny',
|
|
730
|
+
});
|
|
731
|
+
auditPermissionLifecycle({
|
|
732
|
+
phase: 'final',
|
|
733
|
+
requestId: lifecycleRequestId,
|
|
734
|
+
sessionId,
|
|
735
|
+
toolName,
|
|
736
|
+
decision: 'deny',
|
|
737
|
+
decisionSource: 'policy',
|
|
738
|
+
policyCode: 'permission_mode_denied',
|
|
739
|
+
reason: auditSummary,
|
|
740
|
+
permissionMode: context.permissionMode,
|
|
741
|
+
agentAid: context.selfAid,
|
|
742
|
+
agentName: context.agentName,
|
|
743
|
+
role: context.role,
|
|
744
|
+
taskId: context.taskId,
|
|
745
|
+
summary: auditSummary,
|
|
746
|
+
response: 'deny',
|
|
747
|
+
});
|
|
694
748
|
try {
|
|
695
749
|
await context.recordExecutionAnomaly?.({
|
|
696
750
|
code: 'runtime_permission_denied',
|
|
@@ -63,7 +63,7 @@ export function usageForContext(usage) {
|
|
|
63
63
|
return lastIteration ?? usage;
|
|
64
64
|
}
|
|
65
65
|
/** Models whose base ID uses a 1M context window when sent to the SDK with [1m]. */
|
|
66
|
-
export const ONE_M_CONTEXT_MODEL_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
66
|
+
export const ONE_M_CONTEXT_MODEL_PREFIXES = ['claude-opus-5', 'claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
67
67
|
const ONE_M_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet'];
|
|
68
68
|
const CLAUDE_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet', 'haiku'];
|
|
69
69
|
export function isClaudeContextUsageModel(model) {
|
|
@@ -29,8 +29,9 @@ function parseAid(value) {
|
|
|
29
29
|
return { ok: true, aid };
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* The daemon accepts only the
|
|
33
|
-
* tasks.
|
|
32
|
+
* The daemon accepts only the narrowly scoped AID commands exposed to managed
|
|
33
|
+
* tasks. Public lookup/get may name any valid AID; local show/publish remain
|
|
34
|
+
* subject to the self-AID boundary enforced by the command handler.
|
|
34
35
|
*/
|
|
35
36
|
export function resolveManagedAidOperation(argv) {
|
|
36
37
|
const args = argv[0] === 'aid' ? argv.slice(1) : argv;
|
|
@@ -67,7 +68,21 @@ export function resolveManagedAidOperation(argv) {
|
|
|
67
68
|
return {
|
|
68
69
|
ok: false,
|
|
69
70
|
code: 'NOT_ALLOWED',
|
|
70
|
-
reason: 'Managed AID operations only allow exact
|
|
71
|
+
reason: 'Managed AID operations only allow exact show, lookup, agentmd get, or agentmd put commands with an optional --format json suffix',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function managedAidShowViewForRole(role) {
|
|
75
|
+
return role === 'owner' || role === 'admin' ? 'full' : 'redacted';
|
|
76
|
+
}
|
|
77
|
+
export function redactAidShowResult(value) {
|
|
78
|
+
return {
|
|
79
|
+
aid: value.aid,
|
|
80
|
+
redacted: true,
|
|
81
|
+
hasAgentMd: value.hasAgentMd,
|
|
82
|
+
certExpiresAt: value.certExpiresAt,
|
|
83
|
+
certSubject: value.certSubject,
|
|
84
|
+
certExpired: value.certExpired,
|
|
85
|
+
agentMdSignature: value.agentMdSignature,
|
|
71
86
|
};
|
|
72
87
|
}
|
|
73
88
|
const defaultDependencies = () => ({
|
|
@@ -80,12 +95,19 @@ const defaultDependencies = () => ({
|
|
|
80
95
|
readFileSync: fs.readFileSync,
|
|
81
96
|
});
|
|
82
97
|
/** Execute a validated AID operation from the daemon-owned host filesystem. */
|
|
83
|
-
export async function executeManagedAidOperation(command,
|
|
98
|
+
export async function executeManagedAidOperation(command, options = {}) {
|
|
99
|
+
const { showView = 'full', ...overrides } = options;
|
|
84
100
|
const deps = { ...defaultDependencies(), ...overrides };
|
|
85
101
|
const aunPath = deps.root;
|
|
86
102
|
switch (command.kind) {
|
|
87
|
-
case 'show':
|
|
88
|
-
|
|
103
|
+
case 'show': {
|
|
104
|
+
const value = await deps.aidShow(command.aid, { aunPath });
|
|
105
|
+
return {
|
|
106
|
+
kind: command.kind,
|
|
107
|
+
aid: command.aid,
|
|
108
|
+
value: showView === 'redacted' ? redactAidShowResult(value) : value,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
89
111
|
case 'lookup':
|
|
90
112
|
return { kind: command.kind, aid: command.aid, value: await deps.aidLookup(command.aid) };
|
|
91
113
|
case 'agentmd.get':
|
package/dist/aun/aid/store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
|
|
|
7
7
|
// pages so a large backlog is drained without monopolizing the sync loop.
|
|
8
8
|
const FULL_GROUP_PULL_MAX_PAGES = 10;
|
|
9
9
|
const GROUP_PULL_PAGINATION_PATCH = Symbol('evolcore.groupPullPaginationPatch');
|
|
10
|
+
const RELIABLE_MESSAGE_DELIVERY_PATCH = Symbol('evolcore.reliableMessageDeliveryPatch');
|
|
10
11
|
/**
|
|
11
12
|
* Slot 命名约定(基于 fastaun 0.4.3 隔离键语义)。
|
|
12
13
|
*
|
|
@@ -24,6 +25,7 @@ const GROUP_PULL_PAGINATION_PATCH = Symbol('evolcore.groupPullPaginationPatch');
|
|
|
24
25
|
* - CLI / ecweb 查询使用新的独立 slot,不能共享 daemon 的 token 或 seq 游标。
|
|
25
26
|
* - netcheck 使用独立消费者,不得因诊断连接影响 daemon 的入站顺序。
|
|
26
27
|
* - 群规则同步是短连接 RPC,也必须使用独立 slot,避免触碰 daemon 消费游标。
|
|
28
|
+
* - menu-client 是远程 Control AID 的短连接请求器,使用独立 slot,避免触碰 daemon 消费游标。
|
|
27
29
|
* - bench 各并发单元用 evolcore-bench-<N>,各自独立隔离键,互不干扰
|
|
28
30
|
*/
|
|
29
31
|
export const SLOT = {
|
|
@@ -32,6 +34,7 @@ export const SLOT = {
|
|
|
32
34
|
daemon: 'evolcore daemon',
|
|
33
35
|
cli: 'evolcore-cli',
|
|
34
36
|
groupRules: 'evolcore-group-rules',
|
|
37
|
+
menuClient: 'evolcore-menu-client',
|
|
35
38
|
bench: 'evolcore-bench',
|
|
36
39
|
netcheck: 'evolcore-netcheck',
|
|
37
40
|
};
|
|
@@ -104,6 +107,46 @@ export function enableFullGroupPullPagination(client) {
|
|
|
104
107
|
target._pullGroupV2 = wrappedPull;
|
|
105
108
|
return true;
|
|
106
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Make SDK pull delivery wait until EvolCore's application handlers finish.
|
|
112
|
+
*
|
|
113
|
+
* fastaun 0.5.13 publishes message events with dispatcher.enqueue(), then
|
|
114
|
+
* advances its sequence tracker immediately. That is a transport watermark,
|
|
115
|
+
* so a daemon restart can acknowledge a message before the application has
|
|
116
|
+
* durably claimed it. The SDK keeps _publishAppEvent private, but the public
|
|
117
|
+
* client is intentionally adapted here in one place: preserve the SDK's
|
|
118
|
+
* normal publication path and wait for its dispatcher queue for pulled
|
|
119
|
+
* message events before the pull operation commits the sequence.
|
|
120
|
+
*/
|
|
121
|
+
export function enableReliableMessageDelivery(client) {
|
|
122
|
+
const target = client;
|
|
123
|
+
if (target[RELIABLE_MESSAGE_DELIVERY_PATCH])
|
|
124
|
+
return false;
|
|
125
|
+
const publish = target._publishAppEvent;
|
|
126
|
+
const dispatcher = target._dispatcher;
|
|
127
|
+
if (typeof publish !== 'function' || !dispatcher || typeof dispatcher.flush !== 'function')
|
|
128
|
+
return false;
|
|
129
|
+
const wrappedPublish = async function (event, payload, source, ...rest) {
|
|
130
|
+
const result = await publish.call(this, event, payload, source, ...rest);
|
|
131
|
+
// Pull/recovery paths must not advance SeqTracker until the application
|
|
132
|
+
// handlers have completed. Push delivery has no pull commit to gate and
|
|
133
|
+
// remains asynchronous as intended by the SDK.
|
|
134
|
+
const reliable = (event === 'message.received' || event === 'group.message_created')
|
|
135
|
+
&& source !== 'push'
|
|
136
|
+
&& source !== 'inline-push';
|
|
137
|
+
if (reliable)
|
|
138
|
+
await dispatcher.flush();
|
|
139
|
+
return result;
|
|
140
|
+
};
|
|
141
|
+
Object.defineProperty(target, RELIABLE_MESSAGE_DELIVERY_PATCH, {
|
|
142
|
+
configurable: false,
|
|
143
|
+
enumerable: false,
|
|
144
|
+
value: true,
|
|
145
|
+
writable: false,
|
|
146
|
+
});
|
|
147
|
+
target._publishAppEvent = wrappedPublish;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
107
150
|
/** 加载本地 AID 值对象(用于离线签名/验签,无需连接)。load 失败抛 AidLoadError。 */
|
|
108
151
|
export function loadAid(store, aid) {
|
|
109
152
|
const r = store.load(aid);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { createShortConnection } from './rpc/client.js';
|
|
5
|
+
import { SLOT } from './aid/store.js';
|
|
6
|
+
import { isValidAid } from './aid/validation.js';
|
|
7
|
+
import { resolveRoot } from '../paths.js';
|
|
8
|
+
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
9
|
+
import { buildMenuClientProtectedHeaders, parseMenuResponse, validateMenuRequest, } from '../core/command/menu-protocol.js';
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
11
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
12
|
+
const POLL_INTERVAL_MS = 200;
|
|
13
|
+
const PULL_LIMIT = 50;
|
|
14
|
+
const LOCK_STALE_MS = 5 * 60_000;
|
|
15
|
+
export class MenuClientError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
data;
|
|
18
|
+
constructor(code, message, data) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.data = data;
|
|
22
|
+
this.name = 'MenuClientError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function callClient(conn, method, params) {
|
|
26
|
+
try {
|
|
27
|
+
return await conn.call(method, params);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof MenuClientError)
|
|
31
|
+
throw error;
|
|
32
|
+
const code = String(error?.code ?? '').toLowerCase();
|
|
33
|
+
const message = error?.message ?? String(error);
|
|
34
|
+
const timedOut = code.includes('timeout') || code.includes('timed_out') || /timed?\s*out|timeout/i.test(String(message));
|
|
35
|
+
throw new MenuClientError(timedOut ? 'TIMEOUT' : 'RPC_FAILED', String(message));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function normalizeTimeout(value) {
|
|
39
|
+
const timeout = value ?? DEFAULT_TIMEOUT_MS;
|
|
40
|
+
if (!Number.isFinite(timeout) || timeout < 1_000 || timeout > MAX_TIMEOUT_MS) {
|
|
41
|
+
throw new MenuClientError('INVALID_ARGUMENT', `timeout must be between 1s and ${MAX_TIMEOUT_MS / 1000}s`);
|
|
42
|
+
}
|
|
43
|
+
return Math.round(timeout);
|
|
44
|
+
}
|
|
45
|
+
function lockPath(aunPath, fromAid) {
|
|
46
|
+
const identity = crypto.createHash('sha256').update(fromAid).digest('hex').slice(0, 16);
|
|
47
|
+
return path.join(aunPath ?? resolveRoot(), 'data', 'instance', `menu-client-${identity}.lock`);
|
|
48
|
+
}
|
|
49
|
+
function acquireClientLock(aunPath, fromAid) {
|
|
50
|
+
const file = lockPath(aunPath, fromAid);
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
const token = `${process.pid}:${crypto.randomUUID()}`;
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.statSync(file);
|
|
55
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(file);
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
let fd;
|
|
64
|
+
try {
|
|
65
|
+
fd = fs.openSync(file, 'wx', 0o600);
|
|
66
|
+
fs.writeFileSync(fd, token, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
try {
|
|
70
|
+
if (fd !== undefined)
|
|
71
|
+
fs.closeSync(fd);
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(file))
|
|
76
|
+
fs.unlinkSync(file);
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
if (error?.code === 'EEXIST')
|
|
80
|
+
throw new MenuClientError('CLIENT_BUSY', 'Another ec menu-client request is using the configured slot');
|
|
81
|
+
throw new MenuClientError('CLIENT_LOCK_FAILED', `Unable to acquire menu-client lock: ${error?.message ?? String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
try {
|
|
85
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
86
|
+
if (current === token)
|
|
87
|
+
fs.unlinkSync(file);
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
if (fd !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
fs.closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function normalizeRequest(value) {
|
|
99
|
+
const request = { ...value };
|
|
100
|
+
if (typeof request.id !== 'string' || !request.id.trim())
|
|
101
|
+
request.id = crypto.randomUUID();
|
|
102
|
+
const error = validateMenuRequest(request);
|
|
103
|
+
if (error)
|
|
104
|
+
throw new MenuClientError(error.code, error.message, error.data);
|
|
105
|
+
return request;
|
|
106
|
+
}
|
|
107
|
+
function responseFromMessage(message) {
|
|
108
|
+
const from = typeof message?.from === 'string'
|
|
109
|
+
? message.from.trim()
|
|
110
|
+
: typeof message?.envelope?.from === 'string' ? message.envelope.from.trim() : '';
|
|
111
|
+
let payload = message?.payload;
|
|
112
|
+
if (typeof payload === 'string') {
|
|
113
|
+
try {
|
|
114
|
+
payload = JSON.parse(payload);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
121
|
+
return undefined;
|
|
122
|
+
if (from && typeof payload.type === 'string' && payload.type === 'menu.response') {
|
|
123
|
+
try {
|
|
124
|
+
return parseMenuResponse(payload);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
async function pollResponse(conn, targetAid, requestId, timeoutMs, initialAfterSeq) {
|
|
133
|
+
const deadline = Date.now() + timeoutMs;
|
|
134
|
+
let afterSeq = initialAfterSeq;
|
|
135
|
+
while (Date.now() < deadline) {
|
|
136
|
+
const result = await callClient(conn, 'message.pull', { after_seq: afterSeq, limit: PULL_LIMIT });
|
|
137
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
138
|
+
let maxSeq = afterSeq;
|
|
139
|
+
for (const message of messages) {
|
|
140
|
+
const seq = Number(message?.seq);
|
|
141
|
+
if (Number.isSafeInteger(seq) && seq > maxSeq)
|
|
142
|
+
maxSeq = seq;
|
|
143
|
+
const response = responseFromMessage(message);
|
|
144
|
+
const messageFrom = typeof message?.from === 'string'
|
|
145
|
+
? message.from.trim()
|
|
146
|
+
: typeof message?.envelope?.from === 'string' ? message.envelope.from.trim() : '';
|
|
147
|
+
if (response && messageFrom === targetAid && response.id === requestId) {
|
|
148
|
+
if (Number.isSafeInteger(maxSeq) && maxSeq > afterSeq) {
|
|
149
|
+
try {
|
|
150
|
+
await callClient(conn, 'message.ack', { seq: maxSeq });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (Number.isSafeInteger(maxSeq) && maxSeq > afterSeq) {
|
|
160
|
+
afterSeq = maxSeq;
|
|
161
|
+
try {
|
|
162
|
+
await callClient(conn, 'message.ack', { seq: afterSeq });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const waitMs = Math.min(POLL_INTERVAL_MS, Math.max(1, deadline - Date.now()));
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
170
|
+
}
|
|
171
|
+
throw new MenuClientError('TIMEOUT', `Timed out waiting for Menu response ${requestId}`);
|
|
172
|
+
}
|
|
173
|
+
async function drainBaseline(conn) {
|
|
174
|
+
let afterSeq = 0;
|
|
175
|
+
for (let page = 0; page < 200; page += 1) {
|
|
176
|
+
const result = await callClient(conn, 'message.pull', { after_seq: afterSeq, limit: PULL_LIMIT });
|
|
177
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
178
|
+
let maxSeq = Math.max(afterSeq, Number(result?.server_ack_seq) || 0);
|
|
179
|
+
for (const message of messages) {
|
|
180
|
+
const seq = Number(message?.seq);
|
|
181
|
+
if (Number.isSafeInteger(seq) && seq > maxSeq)
|
|
182
|
+
maxSeq = seq;
|
|
183
|
+
}
|
|
184
|
+
if (maxSeq > afterSeq) {
|
|
185
|
+
try {
|
|
186
|
+
await callClient(conn, 'message.ack', { seq: maxSeq });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new MenuClientError('ACK_FAILED', error?.message ?? String(error));
|
|
190
|
+
}
|
|
191
|
+
afterSeq = maxSeq;
|
|
192
|
+
}
|
|
193
|
+
if (messages.length < PULL_LIMIT)
|
|
194
|
+
return afterSeq;
|
|
195
|
+
}
|
|
196
|
+
throw new MenuClientError('BASELINE_TOO_LARGE', 'menu-client backlog exceeds the bounded baseline drain');
|
|
197
|
+
}
|
|
198
|
+
async function sendOnce(options, request, timeoutMs) {
|
|
199
|
+
const slotId = options.slotId ?? SLOT.menuClient;
|
|
200
|
+
const encrypt = options.encrypt ?? true;
|
|
201
|
+
if (!encrypt) {
|
|
202
|
+
throw new MenuClientError('INVALID_CLIENT', 'ec-menu-client requires E2EE protected headers; remove --no-encrypt');
|
|
203
|
+
}
|
|
204
|
+
const headers = buildMenuClientProtectedHeaders({
|
|
205
|
+
clientName: 'ec-menu-client',
|
|
206
|
+
clientVersion: readInstalledEvolcoreVersion(),
|
|
207
|
+
remoteAccessToken: options.remoteAccessToken ?? options.ecwebToken,
|
|
208
|
+
});
|
|
209
|
+
const conn = await createShortConnection(options.fromAid, {
|
|
210
|
+
aunPath: options.aunPath,
|
|
211
|
+
slotId,
|
|
212
|
+
shortTtlMs: timeoutMs * (request.type === 'menu.query' || request.type === 'menu.options' ? 2 : 1) + 10_000,
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
let baselineSeq = await drainBaseline(conn);
|
|
216
|
+
const attempts = request.type === 'menu.query' || request.type === 'menu.options' ? 2 : 1;
|
|
217
|
+
let lastError;
|
|
218
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
219
|
+
const sent = await callClient(conn, 'message.send', {
|
|
220
|
+
to: options.targetAid,
|
|
221
|
+
payload: request,
|
|
222
|
+
encrypt,
|
|
223
|
+
...(encrypt ? { protected_headers: headers } : {}),
|
|
224
|
+
});
|
|
225
|
+
const messageId = sent?.message_id ?? sent?.message?.message_id;
|
|
226
|
+
if (typeof messageId !== 'string' || !messageId.trim()) {
|
|
227
|
+
throw new MenuClientError('SEND_FAILED', 'message.send returned no message_id');
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return await pollResponse(conn, options.targetAid, request.id, timeoutMs, baselineSeq);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
lastError = error;
|
|
234
|
+
if (attempt + 1 < attempts && error instanceof MenuClientError && error.code === 'TIMEOUT') {
|
|
235
|
+
baselineSeq = await drainBaseline(conn);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw lastError ?? new MenuClientError('INTERNAL_ERROR', 'Menu request failed');
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
await conn.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function executeMenuClientRequest(options) {
|
|
248
|
+
if (!isValidAid(options.fromAid) || !isValidAid(options.targetAid)) {
|
|
249
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'fromAid and targetAid must be valid AIDs');
|
|
250
|
+
}
|
|
251
|
+
if (options.slotId !== undefined && (typeof options.slotId !== 'string' || !options.slotId.trim())) {
|
|
252
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'slotId must be a non-empty string');
|
|
253
|
+
}
|
|
254
|
+
const slotId = options.slotId ?? SLOT.menuClient;
|
|
255
|
+
if (slotId.length > 128 || !/^[A-Za-z0-9._-][A-Za-z0-9._/ :\-]*$/.test(slotId)) {
|
|
256
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'slotId contains unsupported characters');
|
|
257
|
+
}
|
|
258
|
+
if (options.aunPath !== undefined && (typeof options.aunPath !== 'string' || !options.aunPath.trim())) {
|
|
259
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'aunPath must be a non-empty path');
|
|
260
|
+
}
|
|
261
|
+
const request = normalizeRequest(options.request);
|
|
262
|
+
const timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
263
|
+
const release = options.slotId && options.slotId !== SLOT.menuClient
|
|
264
|
+
? () => { }
|
|
265
|
+
: acquireClientLock(options.aunPath, options.fromAid);
|
|
266
|
+
try {
|
|
267
|
+
return await sendOnce(options, request, timeoutMs);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
release();
|
|
271
|
+
}
|
|
272
|
+
}
|