evolcore 0.0.24 → 0.0.26
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 +12 -1
- package/bin/install-codex-managed-hooks.mjs +28 -1
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +54 -13
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +128 -42
- package/dist/agents/ecagent-runner.js +14 -18
- package/dist/agents/gemini-runner.js +24 -9
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +135 -10
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +53 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +125 -5
- package/dist/cli/agent-create.js +151 -11
- package/dist/cli/agent.js +119 -6
- package/dist/cli/aun-commands.js +74 -7
- package/dist/cli/config.js +27 -8
- package/dist/cli/contact.js +63 -6
- package/dist/cli/daemon-commands.js +90 -25
- package/dist/cli/diag.js +31 -0
- package/dist/cli/handoff-command.js +35 -3
- package/dist/cli/index.js +7 -1
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +38 -23
- package/dist/cli/restart-monitor.js +57 -10
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +165 -3
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +66 -8
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +87 -8
- package/dist/core/auth/operation-catalog.js +51 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/agent-control.js +54 -25
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +152 -20
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +13 -3
- package/dist/core/command/menu-handler.js +88 -31
- package/dist/core/command/menu-protocol.js +6 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +77 -12
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +3 -3
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +48 -23
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +47 -7
- package/dist/index.js +195 -31
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/price-resolver.js +28 -10
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/model-prices.jsonl +3 -3
- package/dist/utils/npm-ops.js +176 -35
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -1
- package/kits/docs/evolcore/agent-create.md +30 -11
- package/kits/docs/evolcore/agent.md +18 -8
- package/kits/docs/evolcore/aid.md +6 -4
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +7 -0
- package/kits/docs/evolcore/contact.md +12 -5
- package/kits/docs/evolcore/menu-client.md +11 -3
- package/kits/docs/evolcore/msg.md +1 -0
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/docs/venues/group.md +12 -0
- package/kits/eck_manifest.json +12 -0
- package/kits/rules/03-identity.md +3 -1
- package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
- package/kits/templates/roles/admin.json +14 -2
- package/kits/templates/roles/member.json +3 -2
- package/kits/templates/roles/visitor.json +4 -3
- package/kits/templates/system-fragments/bootstrap.md +16 -5
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +6 -4
- package/skills/eclink/SKILL.md +0 -84
- package/skills/eclink/agents/openai.yaml +0 -4
|
@@ -106,13 +106,13 @@ export function parseReturn(args) {
|
|
|
106
106
|
function printReturn(result) {
|
|
107
107
|
if (result.ok) {
|
|
108
108
|
if (result.code === 'HANDOFF_RETURN_ALREADY_APPLIED') {
|
|
109
|
-
console.log(`✓ handoff ${result.handoff_id}
|
|
109
|
+
console.log(`✓ handoff ${result.handoff_id} 已接收过相同结果,未重复处理${result.result_id ? ` result_id=${result.result_id}` : ''}`);
|
|
110
110
|
}
|
|
111
111
|
else if (result.state === 'completed') {
|
|
112
112
|
console.log(`✓ handoff ${result.handoff_id} 已确认完成,不回流来源会话`);
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
|
-
console.log(`✓ handoff ${result.handoff_id}
|
|
115
|
+
console.log(`✓ handoff ${result.handoff_id} 回流结果已接收${result.result_id ? ` result_id=${result.result_id}` : ''};后续仍可用同一 ID 追加结果`);
|
|
116
116
|
}
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
@@ -156,8 +156,10 @@ export async function cmdHandoff(args) {
|
|
|
156
156
|
console.log(`用法: ec handoff <command> [args]
|
|
157
157
|
|
|
158
158
|
Commands:
|
|
159
|
-
return [handoff-id] [content]
|
|
159
|
+
return [handoff-id] [content] 提交一个回流结果;required 可用同一 ID 追加多个结果
|
|
160
160
|
return [handoff-id] --text-from-file <path>
|
|
161
|
+
recover-return <handoff-id> [content] owner-authorized recovery after target session replacement
|
|
162
|
+
recover-return <handoff-id> --text-from-file <path>
|
|
161
163
|
status <handoff-id> 查询 handoff 状态
|
|
162
164
|
list [--state <state>] [--session <id>] [--limit <n>] [--agent <aid>]
|
|
163
165
|
trace <handoff-id> [--limit <n>] [--agent <aid>]`);
|
|
@@ -194,6 +196,36 @@ Commands:
|
|
|
194
196
|
printReturn(result);
|
|
195
197
|
return;
|
|
196
198
|
}
|
|
199
|
+
if (sub === 'recover-return') {
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = parseReturn(args.slice(1));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!parsed.handoffId) {
|
|
210
|
+
console.error('✗ recover-return 必须指定 handoff ID');
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const result = await ipcQuery(resolvePaths().socket, {
|
|
215
|
+
type: 'handoff-recover-return',
|
|
216
|
+
sessionId: process.env.EVOLCORE_SESSION_ID,
|
|
217
|
+
handoffId: parsed.handoffId,
|
|
218
|
+
content: parsed.content,
|
|
219
|
+
delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
|
|
220
|
+
}, 5000);
|
|
221
|
+
if (!result) {
|
|
222
|
+
console.error('✗ daemon 暂时不可用');
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
printReturn(result);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
197
229
|
if (sub === 'status') {
|
|
198
230
|
const handoffId = args[1];
|
|
199
231
|
if (!handoffId || !HANDOFF_ID_RE.test(handoffId)) {
|
package/dist/cli/index.js
CHANGED
|
@@ -47,7 +47,8 @@ AUN:
|
|
|
47
47
|
storage 底层对象存储命令
|
|
48
48
|
rpc 调用底层 AUN RPC
|
|
49
49
|
menu-client 向远程 daemon 发送标准 Menu 请求
|
|
50
|
-
|
|
50
|
+
diag 邀请远程诊断 Agent,或 stop 撤销诊断授权
|
|
51
|
+
contact 联系人、待审申请与黑名单管理
|
|
51
52
|
net AUN 网络链路诊断
|
|
52
53
|
|
|
53
54
|
配置与诊断:
|
|
@@ -351,6 +352,11 @@ export async function main(args) {
|
|
|
351
352
|
await cmdRpc(args.slice(1));
|
|
352
353
|
break;
|
|
353
354
|
}
|
|
355
|
+
case 'diag': {
|
|
356
|
+
const { cmdDiag } = await import('./diag.js');
|
|
357
|
+
await cmdDiag(args.slice(1));
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
354
360
|
case 'menu-client': {
|
|
355
361
|
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
356
362
|
suppressSdkLogs();
|
package/dist/cli/menu-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { getArgValue, wantsHelp } from './help.js';
|
|
3
3
|
import { executeMenuClientRequest, MenuClientError } from '../aun/menu-client.js';
|
|
4
|
+
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
4
5
|
function parseDurationMs(value) {
|
|
5
6
|
if (value === undefined)
|
|
6
7
|
return undefined;
|
|
@@ -11,14 +12,14 @@ function parseDurationMs(value) {
|
|
|
11
12
|
const unit = match[2].toLowerCase();
|
|
12
13
|
return amount * (unit === 'ms' ? 1 : unit === 's' ? 1_000 : unit === 'm' ? 60_000 : 3_600_000);
|
|
13
14
|
}
|
|
14
|
-
function readRequest(args) {
|
|
15
|
+
function readRequest(args, readFile = (file) => fs.readFileSync(file, 'utf8')) {
|
|
15
16
|
const file = getArgValue(args, '--request-file');
|
|
16
17
|
const inline = getArgValue(args, '--request-json');
|
|
17
18
|
if (!!file === !!inline)
|
|
18
19
|
throw new MenuClientError('INVALID_ARGUMENT', '--request-file and --request-json are mutually exclusive');
|
|
19
20
|
let raw;
|
|
20
21
|
try {
|
|
21
|
-
raw = file ?
|
|
22
|
+
raw = file ? readFile(file) : inline;
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
throw new MenuClientError('INVALID_ARGUMENT', `Unable to read request: ${error?.message ?? String(error)}`);
|
|
@@ -36,8 +37,12 @@ function readRequest(args) {
|
|
|
36
37
|
function validateOptions(args) {
|
|
37
38
|
const valueOptions = new Set(['--request-file', '--request-json', '--timeout', '--remote-token', '--ecweb-token', '--app', '--aun-path', '--format']);
|
|
38
39
|
const booleanOptions = new Set(['--encrypt', '--no-encrypt']);
|
|
40
|
+
const seen = new Set();
|
|
39
41
|
for (let index = 0; index < args.length; index += 1) {
|
|
40
42
|
const option = args[index];
|
|
43
|
+
if (seen.has(option))
|
|
44
|
+
throw new MenuClientError('INVALID_ARGUMENT', `Duplicate option: ${option}`);
|
|
45
|
+
seen.add(option);
|
|
41
46
|
if (booleanOptions.has(option))
|
|
42
47
|
continue;
|
|
43
48
|
if (!valueOptions.has(option))
|
|
@@ -54,6 +59,23 @@ function validateOptions(args) {
|
|
|
54
59
|
if (format !== undefined && format !== 'json')
|
|
55
60
|
throw new MenuClientError('INVALID_ARGUMENT', '--format currently supports only json');
|
|
56
61
|
}
|
|
62
|
+
export function parseMenuClientArgs(args, readFile) {
|
|
63
|
+
if (args[0] !== 'request')
|
|
64
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'menu-client currently supports only the request subcommand');
|
|
65
|
+
const fromAid = args[1]?.trim();
|
|
66
|
+
const targetAid = args[2]?.trim();
|
|
67
|
+
if (!fromAid || !targetAid)
|
|
68
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'request requires <from-aid> and <daemon-aid>');
|
|
69
|
+
const options = args.slice(3);
|
|
70
|
+
validateOptions(options);
|
|
71
|
+
return {
|
|
72
|
+
fromAid, targetAid, request: readRequest(options, readFile),
|
|
73
|
+
timeoutMs: parseDurationMs(getArgValue(options, '--timeout')),
|
|
74
|
+
encrypt: !options.includes('--no-encrypt'),
|
|
75
|
+
remoteAccessToken: getArgValue(options, '--remote-token') ?? getArgValue(options, '--ecweb-token'),
|
|
76
|
+
slotId: getArgValue(options, '--app'), aunPath: getArgValue(options, '--aun-path'),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
57
79
|
export async function cmdMenuClient(args) {
|
|
58
80
|
if (wantsHelp(args)) {
|
|
59
81
|
console.log(`用法: ec menu-client request <from-aid> <daemon-aid> [选项]
|
|
@@ -70,26 +92,19 @@ export async function cmdMenuClient(args) {
|
|
|
70
92
|
--format json 输出标准 menu.response JSON`);
|
|
71
93
|
return;
|
|
72
94
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
request,
|
|
88
|
-
timeoutMs,
|
|
89
|
-
encrypt,
|
|
90
|
-
remoteAccessToken: getArgValue(args, '--remote-token') ?? getArgValue(args, '--ecweb-token'),
|
|
91
|
-
slotId: getArgValue(args, '--app'),
|
|
92
|
-
aunPath: getArgValue(args, '--aun-path'),
|
|
93
|
-
});
|
|
95
|
+
let response;
|
|
96
|
+
if (process.env.EVOLCORE_SESSION_ID) {
|
|
97
|
+
const { ipcQuery } = await import('../ipc.js');
|
|
98
|
+
const { resolvePaths } = await import('../paths.js');
|
|
99
|
+
const result = await ipcQuery(resolvePaths().socket, {
|
|
100
|
+
type: 'menu-client.request', argv: args, sessionId: process.env.EVOLCORE_SESSION_ID,
|
|
101
|
+
delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
|
|
102
|
+
}, 250_000);
|
|
103
|
+
if (!result?.ok)
|
|
104
|
+
throw new MenuClientError(result?.code ?? 'DAEMON_UNAVAILABLE', result?.error ?? 'Managed Menu daemon is unavailable');
|
|
105
|
+
response = result.response;
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
response = await executeMenuClientRequest(parseMenuClientArgs(args));
|
|
94
109
|
console.log(JSON.stringify(response, null, 2));
|
|
95
110
|
}
|
|
@@ -15,6 +15,7 @@ import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
|
|
|
15
15
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
16
16
|
import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
17
17
|
import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
|
|
18
|
+
import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
/**
|
|
20
21
|
* The package this monitor operates on, which is not always the one it runs
|
|
@@ -43,6 +44,15 @@ function cleanEnv() {
|
|
|
43
44
|
delete process.env[key];
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
function isPidReused(pid, expectedStartedAt) {
|
|
48
|
+
if (expectedStartedAt === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
const actualStartedAt = getProcessStartTime(pid);
|
|
51
|
+
return actualStartedAt !== null && !startTimeMatches(expectedStartedAt, actualStartedAt);
|
|
52
|
+
}
|
|
53
|
+
function reportPidReused(pid, log) {
|
|
54
|
+
log(`Process ${pid} no longer refers to the recorded daemon; treating the original process as exited`);
|
|
55
|
+
}
|
|
46
56
|
export async function cmdRestartMonitor() {
|
|
47
57
|
// Defense in depth: a unit test that accidentally executes the real restart
|
|
48
58
|
// path must not detach a persistent daemon from the test worker.
|
|
@@ -117,9 +127,11 @@ export async function cmdRestartMonitor() {
|
|
|
117
127
|
}
|
|
118
128
|
// Test daemons live in one temporary HOME per test, so the current HOME's
|
|
119
129
|
// instance registry cannot see them. Their strict environment markers make
|
|
120
|
-
// them safe to reap globally on every real restart.
|
|
130
|
+
// them safe to reap globally on every real restart. Keep one process-table
|
|
131
|
+
// snapshot for both orphan categories; each Windows CIM scan is expensive.
|
|
132
|
+
const orphanSnapshot = findOrphanProcesses();
|
|
121
133
|
{
|
|
122
|
-
const leakedTestDaemons =
|
|
134
|
+
const leakedTestDaemons = orphanSnapshot.filter(o => o.confirmedTestDaemon);
|
|
123
135
|
if (leakedTestDaemons.length > 0) {
|
|
124
136
|
const killed = killOrphans(leakedTestDaemons);
|
|
125
137
|
log(`Cleaned ${killed.length} leaked test daemon(s): ${killed.join(', ')}`);
|
|
@@ -130,7 +142,7 @@ export async function cmdRestartMonitor() {
|
|
|
130
142
|
// still alive. Refuse to create a second daemon when an unregistered process
|
|
131
143
|
// from this package is found; a duplicate would connect AUN and kick the
|
|
132
144
|
// existing session before failing on IPC/port ownership.
|
|
133
|
-
const runtimeOrphans =
|
|
145
|
+
const runtimeOrphans = orphanSnapshot.filter(o => {
|
|
134
146
|
if (o.confirmedTestDaemon)
|
|
135
147
|
return false;
|
|
136
148
|
const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
|
|
@@ -165,6 +177,7 @@ export async function cmdRestartMonitor() {
|
|
|
165
177
|
const aliveMains = oldStatus.mains.filter(m => m.alive);
|
|
166
178
|
if (aliveMains.length > 0) {
|
|
167
179
|
const oldPids = aliveMains.map(m => m.record.pid);
|
|
180
|
+
const expectedStarts = new Map(aliveMains.map(m => [m.record.pid, m.record.startedAt]));
|
|
168
181
|
log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
|
|
169
182
|
const gracefulPid = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor', new Set(oldPids));
|
|
170
183
|
// 没有收到 IPC shutdown 确认的进程才走信号兜底
|
|
@@ -177,15 +190,39 @@ export async function cmdRestartMonitor() {
|
|
|
177
190
|
}
|
|
178
191
|
}
|
|
179
192
|
const exited = await Promise.all(oldPids.map(async (oldPid) => {
|
|
193
|
+
const expectedStartedAt = expectedStarts.get(oldPid);
|
|
194
|
+
if (isPidReused(oldPid, expectedStartedAt)) {
|
|
195
|
+
reportPidReused(oldPid, log);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
180
198
|
if (await platform.waitForProcessExit(oldPid, 30_000)) {
|
|
199
|
+
if (isPidReused(oldPid, expectedStartedAt)) {
|
|
200
|
+
reportPidReused(oldPid, log);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
181
203
|
log(`Process ${oldPid} has exited`);
|
|
182
204
|
return true;
|
|
183
205
|
}
|
|
184
|
-
|
|
206
|
+
if (isPidReused(oldPid, expectedStartedAt)) {
|
|
207
|
+
reportPidReused(oldPid, log);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const state = platform.getProcessState(oldPid);
|
|
211
|
+
log(`ERROR: Process ${oldPid} still running after 30s${state ? ` (state=${state})` : ''}, force killing`);
|
|
212
|
+
if (isPidReused(oldPid, expectedStartedAt)) {
|
|
213
|
+
reportPidReused(oldPid, log);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
185
216
|
platform.killProcess(oldPid, true);
|
|
186
217
|
const forced = await platform.waitForProcessExit(oldPid, 5_000);
|
|
187
|
-
if (
|
|
188
|
-
|
|
218
|
+
if (isPidReused(oldPid, expectedStartedAt)) {
|
|
219
|
+
reportPidReused(oldPid, log);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (!forced) {
|
|
223
|
+
const finalState = platform.getProcessState(oldPid);
|
|
224
|
+
log(`ERROR: Process ${oldPid} survived forced termination${finalState ? ` (state=${finalState})` : ''}`);
|
|
225
|
+
}
|
|
189
226
|
return forced;
|
|
190
227
|
}));
|
|
191
228
|
if (exited.some(result => !result)) {
|
|
@@ -194,7 +231,9 @@ export async function cmdRestartMonitor() {
|
|
|
194
231
|
cleanupPendingFile(pendingFile, log);
|
|
195
232
|
process.exit(1);
|
|
196
233
|
}
|
|
197
|
-
|
|
234
|
+
// waitForProcessExit above already observed the old daemon exit. A fixed
|
|
235
|
+
// three-second delay only serialized the restart path and was not needed
|
|
236
|
+
// for instance-file cleanup.
|
|
198
237
|
cleanupInstances();
|
|
199
238
|
}
|
|
200
239
|
// 版本检查与自动升级
|
|
@@ -202,7 +241,10 @@ export async function cmdRestartMonitor() {
|
|
|
202
241
|
// 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
|
|
203
242
|
// Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
|
|
204
243
|
const upgradeBackup = await backupDistForUpgrade(p, log);
|
|
205
|
-
const upgrade = await
|
|
244
|
+
const [upgrade, ecwebUpgrade] = await Promise.all([
|
|
245
|
+
tryUpgrade(),
|
|
246
|
+
tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME),
|
|
247
|
+
]);
|
|
206
248
|
let upgradeRolledBack;
|
|
207
249
|
switch (upgrade.status) {
|
|
208
250
|
case 'upgraded':
|
|
@@ -243,8 +285,8 @@ export async function cmdRestartMonitor() {
|
|
|
243
285
|
await notifyDaemonOwners(p, `⚠️ evolcore 升级失败,使用当前版本继续\n${upgrade.error ?? ''}`.trim(), log);
|
|
244
286
|
break;
|
|
245
287
|
}
|
|
246
|
-
// EC Web
|
|
247
|
-
|
|
288
|
+
// EC Web 是独立全局包,已安装时单独检查升级;与 EvolCore registry
|
|
289
|
+
// 查询并行执行,npm 安装阶段由 npm-ops 的全局锁串行化。
|
|
248
290
|
switch (ecwebUpgrade.status) {
|
|
249
291
|
case 'upgraded':
|
|
250
292
|
log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
|
|
@@ -710,6 +752,11 @@ async function revertToPreviousBuild(log) {
|
|
|
710
752
|
* back up or the copy failed — the caller then knows rollback is unavailable.
|
|
711
753
|
*/
|
|
712
754
|
async function backupDistForUpgrade(p, log) {
|
|
755
|
+
// Linked/source checkouts cannot be upgraded by npm. Copying dist/ here was
|
|
756
|
+
// pure restart overhead and made every source restart rewrite the artifact
|
|
757
|
+
// tree even when no build was needed.
|
|
758
|
+
if (fs.existsSync(path.join(targetPackageRoot(), 'src')))
|
|
759
|
+
return null;
|
|
713
760
|
const distDir = path.join(targetPackageRoot(), 'dist');
|
|
714
761
|
if (!distArtifactIsUsable(distDir))
|
|
715
762
|
return null;
|
|
@@ -34,6 +34,7 @@ export function getManagementCommandPermissions(role) {
|
|
|
34
34
|
'contact.read': { allow: true, scopes: ['agent'] },
|
|
35
35
|
'contact.add': { allow: true, scopes: ['relation'], constraints: { ownPeerOnly: true, privateOnly: true } },
|
|
36
36
|
'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
37
|
+
'contact.review': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
37
38
|
'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
38
39
|
'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
|
|
39
40
|
'role.assign': { allow: true, scopes: ['agent'] },
|
|
@@ -51,6 +52,7 @@ export function getManagementCommandPermissions(role) {
|
|
|
51
52
|
'ec.aid.lookup': { allow: true, scopes: ['agent'] },
|
|
52
53
|
'ec.aid.agentmd.get': { allow: true, scopes: ['agent'] },
|
|
53
54
|
'ec.aid.agentmd.put': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
|
|
55
|
+
'ec.aid.avatar': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentOwner: true } },
|
|
54
56
|
'ec.fs.mkdir': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
55
57
|
'ec.fs.upload': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
56
58
|
'ec.fs.copy': { allow: true, permissionMode: 'auto', scopes: ['relation'], constraints: { groupOnly: true, currentRelationOnly: true, targetCurrentAgentOnly: true } },
|
|
@@ -679,6 +679,21 @@ function groupFor(target, sel) {
|
|
|
679
679
|
function normalizeAgentConfigForWrite(value) {
|
|
680
680
|
const mutable = normalizeBaseagentEffortCompat({ ...value });
|
|
681
681
|
normalizeShowActivitiesCompat(mutable);
|
|
682
|
+
if (Object.prototype.hasOwnProperty.call(mutable, 'owners')) {
|
|
683
|
+
if (!Array.isArray(mutable.owners)) {
|
|
684
|
+
throw new ConfigError('VALIDATION_ERROR', 'owners must be an array');
|
|
685
|
+
}
|
|
686
|
+
const owners = [];
|
|
687
|
+
for (const rawOwner of mutable.owners) {
|
|
688
|
+
if (typeof rawOwner !== 'string' || !rawOwner.trim() || !isValidAid(rawOwner.trim())) {
|
|
689
|
+
throw new ConfigError('VALIDATION_ERROR', `Invalid owner AID: ${JSON.stringify(rawOwner)}`);
|
|
690
|
+
}
|
|
691
|
+
const owner = rawOwner.trim();
|
|
692
|
+
if (!owners.includes(owner))
|
|
693
|
+
owners.push(owner);
|
|
694
|
+
}
|
|
695
|
+
mutable.owners = owners;
|
|
696
|
+
}
|
|
682
697
|
// AID 格式校验
|
|
683
698
|
if (mutable.aid && !isValidAid(mutable.aid)) {
|
|
684
699
|
throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
|
|
@@ -5,6 +5,9 @@ import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, list
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { resolvePaths, agentDir, agentRelationConfig } from '../paths.js';
|
|
8
|
+
import { roleMenuQuery, roleMenuUpdate } from '../core/command/role-menu.js';
|
|
9
|
+
import { parsePeerKey } from '../core/relation/peer-identity.js';
|
|
10
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
8
11
|
export const AUTHENTICATED_CONFIG_ROLE_ENV = 'EVOLCORE_AUTHENTICATED_CONFIG_ROLE';
|
|
9
12
|
export function executeResolvedConfigCommand(command, options = {}) {
|
|
10
13
|
try {
|
|
@@ -23,6 +26,134 @@ export function executeResolvedConfigCommand(command, options = {}) {
|
|
|
23
26
|
return { ok: false, code: 'CONFIG_ERROR', error: error instanceof Error ? error.message : String(error) };
|
|
24
27
|
}
|
|
25
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Execute a config command that may use an asynchronous domain service.
|
|
31
|
+
* Relation role assignments are intentionally handled here instead of by
|
|
32
|
+
* mutateConfig(), so Role Menu remains the single write path for validation,
|
|
33
|
+
* revisions, journaling, and audit.
|
|
34
|
+
*/
|
|
35
|
+
export async function executeResolvedConfigCommandAsync(command, options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
if (isRelationRoleCommand(command)) {
|
|
38
|
+
return await executeRelationRoleCommand(command, options);
|
|
39
|
+
}
|
|
40
|
+
return executeResolvedConfigCommand(command, options);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof ConfigError)
|
|
44
|
+
return { ok: false, code: error.code, error: error.message, ...(error.data !== undefined ? { data: error.data } : {}) };
|
|
45
|
+
const roleError = error;
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: roleError?.code || 'CONFIG_ERROR',
|
|
49
|
+
error: roleError?.message || String(error),
|
|
50
|
+
...(roleError?.data !== undefined ? { data: roleError.data } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isRelationRoleCommand(command) {
|
|
55
|
+
return command.kind === 'field'
|
|
56
|
+
&& command.field === 'role'
|
|
57
|
+
&& command.configScope === 'relation'
|
|
58
|
+
&& (command.operationId === 'role.relation.read'
|
|
59
|
+
|| command.operationId === 'role.assign'
|
|
60
|
+
|| command.operationId === 'role.revoke');
|
|
61
|
+
}
|
|
62
|
+
async function executeRelationRoleCommand(op, options) {
|
|
63
|
+
if (!op.self || !op.peerKey) {
|
|
64
|
+
return { ok: false, code: 'PEER_REQUIRED', error: 'role requires --self <aid> --peer <peer-aid>' };
|
|
65
|
+
}
|
|
66
|
+
const target = roleTargetFromPeerKey(op.peerKey);
|
|
67
|
+
const context = options.roleContext ?? {
|
|
68
|
+
self: op.self,
|
|
69
|
+
actorRole: options.role || 'owner',
|
|
70
|
+
actorRoleSource: 'local-cli',
|
|
71
|
+
isDaemonOwner: false,
|
|
72
|
+
fromControlChannel: false,
|
|
73
|
+
aunAvailable: true,
|
|
74
|
+
};
|
|
75
|
+
if (context.self !== op.self) {
|
|
76
|
+
return { ok: false, code: 'SCOPE_MISMATCH', error: 'Role target does not match the receiving Agent' };
|
|
77
|
+
}
|
|
78
|
+
const targetArgs = { self: op.self, view: 'target', target };
|
|
79
|
+
const current = await roleMenuQuery(context, targetArgs);
|
|
80
|
+
const explicitRole = current?.assignment?.explicitRole
|
|
81
|
+
?? current?.assignment?.explicitDefaultRole
|
|
82
|
+
?? null;
|
|
83
|
+
const effectiveRole = current?.effectiveRole?.roleId
|
|
84
|
+
?? current?.baselineRole?.roleId
|
|
85
|
+
?? null;
|
|
86
|
+
const effectiveRoleSource = current?.effectiveRole?.source
|
|
87
|
+
?? current?.baselineRole?.source
|
|
88
|
+
?? 'none';
|
|
89
|
+
const roleDetails = { explicitRole, effectiveRole, effectiveRoleSource };
|
|
90
|
+
if (op.subcommand === 'get') {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
subcommand: 'get',
|
|
94
|
+
field: op.field,
|
|
95
|
+
value: explicitRole,
|
|
96
|
+
scope: op.configScope,
|
|
97
|
+
roleDetails,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const nextValue = op.subcommand === 'unset' ? null : op.value;
|
|
101
|
+
const updated = await roleMenuUpdate(context, {
|
|
102
|
+
self: op.self,
|
|
103
|
+
resource: 'assignment',
|
|
104
|
+
target,
|
|
105
|
+
expectedPolicyRevision: current.policyRevision,
|
|
106
|
+
expectedRelationRevision: current.relationRevision,
|
|
107
|
+
}, JSON.stringify(nextValue));
|
|
108
|
+
const updatedExplicitRole = updated?.assignment?.explicitRole
|
|
109
|
+
?? updated?.assignment?.explicitDefaultRole
|
|
110
|
+
?? null;
|
|
111
|
+
const updatedEffectiveRole = updated?.effectiveRole?.roleId
|
|
112
|
+
?? updated?.baselineRole?.roleId
|
|
113
|
+
?? null;
|
|
114
|
+
const updatedEffectiveRoleSource = updated?.effectiveRole?.source
|
|
115
|
+
?? updated?.baselineRole?.source
|
|
116
|
+
?? 'none';
|
|
117
|
+
const updatedDetails = {
|
|
118
|
+
explicitRole: updatedExplicitRole,
|
|
119
|
+
effectiveRole: updatedEffectiveRole,
|
|
120
|
+
effectiveRoleSource: updatedEffectiveRoleSource,
|
|
121
|
+
};
|
|
122
|
+
if (op.subcommand === 'unset') {
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
subcommand: 'unset',
|
|
126
|
+
field: op.field,
|
|
127
|
+
removed: updated.changed === true,
|
|
128
|
+
scope: op.configScope,
|
|
129
|
+
roleDetails: updatedDetails,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
subcommand: 'set',
|
|
135
|
+
field: op.field,
|
|
136
|
+
value: updatedExplicitRole,
|
|
137
|
+
scope: op.configScope,
|
|
138
|
+
file: 'relation-config',
|
|
139
|
+
roleDetails: updatedDetails,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function roleTargetFromPeerKey(peerKey) {
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = parsePeerKey(peerKey);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new ConfigError('INVALID_SELECTOR', `Invalid relation peer key: ${peerKey}`);
|
|
149
|
+
}
|
|
150
|
+
if (parsed.channelType !== 'aun') {
|
|
151
|
+
throw new ConfigError('NOT_SUPPORTED', 'Relation role assignments currently support AUN peers only');
|
|
152
|
+
}
|
|
153
|
+
return isExplicitGroupId(parsed.channelId)
|
|
154
|
+
? { kind: 'group', groupId: parsed.channelId }
|
|
155
|
+
: { kind: 'private', peerAid: parsed.channelId };
|
|
156
|
+
}
|
|
26
157
|
export function executeResolvedConfigOperation(op, options = {}) {
|
|
27
158
|
try {
|
|
28
159
|
const missingAgent = selectedAgentError(op);
|
|
@@ -298,10 +429,25 @@ function getWithSchemaDefault(op, absent, note) {
|
|
|
298
429
|
return { ok: true, subcommand: 'get', field: op.field, value: absent, scope: op.configScope, ...nt };
|
|
299
430
|
}
|
|
300
431
|
function executeSet(op, options) {
|
|
432
|
+
if (op.field === 'role') {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
code: 'ROLE_POLICY_MANAGED',
|
|
436
|
+
error: 'Relation role assignments require the asynchronous role assignment service',
|
|
437
|
+
};
|
|
438
|
+
}
|
|
301
439
|
if (!op.route)
|
|
302
440
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
303
441
|
const selector = selectorFor(op, options);
|
|
304
|
-
mutateConfig(op.route.target, selector, existing => {
|
|
442
|
+
const mutation = mutateConfig(op.route.target, selector, existing => {
|
|
443
|
+
if (op.field === 'owners' && op.route.target === ConfigTarget.Agent) {
|
|
444
|
+
const nextOwners = Array.isArray(op.value) ? op.value : [];
|
|
445
|
+
if (nextOwners.length === 0) {
|
|
446
|
+
throw new ConfigError('LAST_OWNER_PROTECTED', 'owners must contain at least one owner');
|
|
447
|
+
}
|
|
448
|
+
setNested(existing, op.field, nextOwners);
|
|
449
|
+
return nextOwners;
|
|
450
|
+
}
|
|
305
451
|
if (op.route.merge === 'list') {
|
|
306
452
|
const previous = getNested(existing, op.field);
|
|
307
453
|
const previousItems = Array.isArray(previous) ? previous : [];
|
|
@@ -316,25 +462,41 @@ function executeSet(op, options) {
|
|
|
316
462
|
}
|
|
317
463
|
});
|
|
318
464
|
setNested(existing, op.field, merged);
|
|
465
|
+
return merged;
|
|
319
466
|
}
|
|
320
467
|
else {
|
|
321
468
|
setNested(existing, op.field, op.value);
|
|
469
|
+
return op.value;
|
|
322
470
|
}
|
|
323
471
|
});
|
|
324
472
|
return {
|
|
325
473
|
ok: true,
|
|
326
474
|
subcommand: 'set',
|
|
327
475
|
field: op.field,
|
|
328
|
-
value:
|
|
476
|
+
value: mutation.value,
|
|
329
477
|
scope: op.configScope,
|
|
330
478
|
file: op.route.schema,
|
|
331
479
|
};
|
|
332
480
|
}
|
|
333
481
|
function executeUnset(op, options) {
|
|
482
|
+
if (op.field === 'role') {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
code: 'ROLE_POLICY_MANAGED',
|
|
486
|
+
error: 'Relation role assignments require the asynchronous role assignment service',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
334
489
|
if (!op.route)
|
|
335
490
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
336
491
|
const selector = selectorFor(op, options);
|
|
337
|
-
const
|
|
492
|
+
const target = op.route.target;
|
|
493
|
+
const { value: removed } = mutateConfig(target, selector, existing => {
|
|
494
|
+
if (op.field === 'owners' && target === ConfigTarget.Agent) {
|
|
495
|
+
const current = getNested(existing, op.field);
|
|
496
|
+
if (Array.isArray(current) && current.length > 0) {
|
|
497
|
+
throw new ConfigError('LAST_OWNER_PROTECTED', 'cannot unset owners while an Agent still has owners; use owners replace with another owner');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
338
500
|
const parts = op.field.split('.');
|
|
339
501
|
let current = existing;
|
|
340
502
|
for (let i = 0; i < parts.length - 1; i++) {
|