evolcore 0.0.12 → 0.0.14
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 +37 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/ec-safe-output.js +26 -7
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +105 -11
- package/dist/agents/codex-app-server-client.js +176 -7
- package/dist/agents/codex-runner.js +248 -84
- package/dist/agents/ecagent-runner.js +21 -2
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +176 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +514 -150
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/agent.js +9 -96
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +65 -19
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/task-context.js +172 -0
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +7 -0
- package/dist/config/config-manager.js +27 -3
- package/dist/config/contact-book-store.js +7 -2
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +30 -1
- package/dist/core/auth/authorization-audit.js +47 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +10 -1
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +8 -1
- package/dist/core/command/command-handler.js +201 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +34 -1
- package/dist/core/message/message-bridge.js +68 -13
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +242 -48
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +649 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +266 -56
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/runtime-lock.js +101 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +191 -89
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/history.js +42 -7
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +17 -0
- package/dist/utils/logger.js +37 -4
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +7 -0
- package/kits/templates/roles/member.json +8 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
package/dist/channels/daemon.js
CHANGED
|
@@ -413,6 +413,7 @@ function normalizeExecutionAnomaly(value) {
|
|
|
413
413
|
const occurredAt = typeof value.occurredAt === 'number' && Number.isFinite(value.occurredAt)
|
|
414
414
|
? value.occurredAt
|
|
415
415
|
: Date.now();
|
|
416
|
+
const correlationId = nonEmptyString(value.correlationId);
|
|
416
417
|
const toolName = nonEmptyString(value.toolName);
|
|
417
418
|
const requestId = nonEmptyString(value.requestId);
|
|
418
419
|
const policyCode = nonEmptyString(value.policyCode);
|
|
@@ -426,6 +427,7 @@ function normalizeExecutionAnomaly(value) {
|
|
|
426
427
|
severity: 'warning',
|
|
427
428
|
phase: 'execution',
|
|
428
429
|
occurredAt,
|
|
430
|
+
...(correlationId ? { correlationId } : {}),
|
|
429
431
|
effect,
|
|
430
432
|
...(toolName ? { toolName } : {}),
|
|
431
433
|
...(requestId ? { requestId } : {}),
|
package/dist/cli/agent.js
CHANGED
|
@@ -8,18 +8,14 @@ import { resolveConfigCommand } from '../config/resolved-config-op.js';
|
|
|
8
8
|
import { executeResolvedConfigCommand } from '../config/config-operation-service.js';
|
|
9
9
|
import { ipcQuery } from '../ipc.js';
|
|
10
10
|
import { CONFIG_SCHEMA_VERSION } from '../types.js';
|
|
11
|
-
import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
|
|
12
|
-
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
13
11
|
import { ensureInitialRoleRegistry } from '../config/role-service.js';
|
|
14
|
-
import {
|
|
12
|
+
import { isValidChannelName } from '../core/channel-loader.js';
|
|
15
13
|
import { commandExists } from '../utils/cross-platform.js';
|
|
16
14
|
import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
|
|
17
15
|
import { resolveEcagentConfig } from '../agents/baseagent.js';
|
|
18
16
|
import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../utils/project-path.js';
|
|
19
17
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
20
18
|
import { readTaskRuntimeContextFromEnv } from './task-context.js';
|
|
21
|
-
import { EventBus } from '../core/event-bus.js';
|
|
22
|
-
import { SessionManager } from '../core/session/session-manager.js';
|
|
23
19
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
24
20
|
function withStaticOwner(config, owner) {
|
|
25
21
|
const value = owner?.trim();
|
|
@@ -1196,33 +1192,9 @@ export async function agentRename(aid, name) {
|
|
|
1196
1192
|
return { ok: true, aid, name };
|
|
1197
1193
|
}
|
|
1198
1194
|
// ==================== agentReady ====================
|
|
1199
|
-
async function ensureOfflinePostBootstrapMainSession(aid, config) {
|
|
1200
|
-
const owner = config.owners?.[0];
|
|
1201
|
-
if (!owner)
|
|
1202
|
-
throw new Error(`Agent "${aid}" has no owner for the post-bootstrap main session`);
|
|
1203
|
-
const p = resolvePaths();
|
|
1204
|
-
const aunName = config.channels?.find(channel => channel.type === 'aun')?.name || 'main';
|
|
1205
|
-
const channel = formatChannelKey({ type: 'aun', selfAID: aid, name: aunName });
|
|
1206
|
-
const manager = new SessionManager(p.sessionsDir, new EventBus());
|
|
1207
|
-
const active = await manager.getActiveSession(channel, owner);
|
|
1208
|
-
const ensured = await manager.ensureNewMainSessionForOperation({
|
|
1209
|
-
channel,
|
|
1210
|
-
channelId: owner,
|
|
1211
|
-
projectPath: active?.projectPath
|
|
1212
|
-
|| config.projects?.defaultPath
|
|
1213
|
-
|| path.join(p.root, 'projects', aid.split('.')[0]),
|
|
1214
|
-
baseagent: config.active_baseagent || active?.baseagent || 'claude',
|
|
1215
|
-
operationId: postBootstrapWelcomeOperationId(aid),
|
|
1216
|
-
identityMetadata: {
|
|
1217
|
-
...(active?.metadata ?? {}),
|
|
1218
|
-
channelKey: channel,
|
|
1219
|
-
peerId: owner,
|
|
1220
|
-
},
|
|
1221
|
-
});
|
|
1222
|
-
return ensured.session.id;
|
|
1223
|
-
}
|
|
1224
1195
|
export async function agentReady(aid) {
|
|
1225
|
-
const
|
|
1196
|
+
const managedSessionId = process.env.EVOLCORE_SESSION_ID;
|
|
1197
|
+
const taskSelfAid = managedSessionId ? readTaskRuntimeContextFromEnv()?.selfAid : undefined;
|
|
1226
1198
|
if (taskSelfAid && taskSelfAid.replace(/^@/, '') !== aid.replace(/^@/, '')) {
|
|
1227
1199
|
return {
|
|
1228
1200
|
ok: false,
|
|
@@ -1231,77 +1203,18 @@ export async function agentReady(aid) {
|
|
|
1231
1203
|
};
|
|
1232
1204
|
}
|
|
1233
1205
|
const p = resolvePaths();
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (!delegationToken)
|
|
1206
|
+
try {
|
|
1207
|
+
const delegationToken = managedSessionId ? process.env[AGENT_DELEGATION_TOKEN_ENV] : undefined;
|
|
1208
|
+
if (managedSessionId && !delegationToken) {
|
|
1238
1209
|
return { ok: false, code: 'DELEGATION_REQUIRED', error: `${AGENT_DELEGATION_TOKEN_ENV} is not set for the active task` };
|
|
1210
|
+
}
|
|
1239
1211
|
const result = await ipcQuery(p.socket, {
|
|
1240
1212
|
type: 'evolagent.bootstrapComplete',
|
|
1241
1213
|
aid,
|
|
1242
|
-
sessionId: managedSessionId,
|
|
1243
|
-
delegationToken,
|
|
1214
|
+
...(managedSessionId ? { sessionId: managedSessionId, delegationToken } : {}),
|
|
1244
1215
|
}, 30_000);
|
|
1245
1216
|
if (!result)
|
|
1246
|
-
return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' };
|
|
1247
|
-
return result.ok
|
|
1248
|
-
? { ok: true, aid, reloaded: result.reloaded === true }
|
|
1249
|
-
: { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
|
|
1250
|
-
}
|
|
1251
|
-
const config = loadAgent(aid);
|
|
1252
|
-
if (!config)
|
|
1253
|
-
return { ok: false, error: `Agent "${aid}" not found` };
|
|
1254
|
-
try {
|
|
1255
|
-
let controlToken;
|
|
1256
|
-
try {
|
|
1257
|
-
controlToken = fs.readFileSync(p.controlToken, 'utf8').trim() || undefined;
|
|
1258
|
-
}
|
|
1259
|
-
catch { }
|
|
1260
|
-
const result = await ipcQuery(p.socket, { type: 'evolagent.bootstrapComplete', aid, controlToken }, 30_000);
|
|
1261
|
-
if (!result) {
|
|
1262
|
-
const offlineConfig = loadAgent(aid) || config;
|
|
1263
|
-
const lifecycle = normalizeAgentLifecycle(offlineConfig).lifecycle;
|
|
1264
|
-
if (lifecycle !== 'bootstrapping') {
|
|
1265
|
-
if (lifecycle === 'active') {
|
|
1266
|
-
if (hasPendingPostBootstrapWelcomeOutbox(aid)) {
|
|
1267
|
-
try {
|
|
1268
|
-
const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
|
|
1269
|
-
if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
|
|
1270
|
-
throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
catch (e) {
|
|
1274
|
-
return {
|
|
1275
|
-
ok: false,
|
|
1276
|
-
code: 'MAIN_SESSION_CREATE_FAILED',
|
|
1277
|
-
error: e instanceof Error ? e.message : 'Failed to recover the post-bootstrap main session',
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
return { ok: true, aid, reloaded: false };
|
|
1282
|
-
}
|
|
1283
|
-
return { ok: false, code: 'INVALID_LIFECYCLE', error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready` };
|
|
1284
|
-
}
|
|
1285
|
-
const owner = offlineConfig.owners?.[0];
|
|
1286
|
-
if (!owner)
|
|
1287
|
-
return { ok: false, code: 'OWNER_REQUIRED', error: `Agent "${aid}" has no owner for the completion welcome` };
|
|
1288
|
-
try {
|
|
1289
|
-
preparePostBootstrapWelcomeOutbox(aid, owner);
|
|
1290
|
-
const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
|
|
1291
|
-
if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
|
|
1292
|
-
throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
|
|
1293
|
-
}
|
|
1294
|
-
saveAgent(withLifecycleForWrite(offlineConfig, 'active'));
|
|
1295
|
-
}
|
|
1296
|
-
catch (e) {
|
|
1297
|
-
return {
|
|
1298
|
-
ok: false,
|
|
1299
|
-
code: 'READY_PERSIST_FAILED',
|
|
1300
|
-
error: e instanceof Error ? e.message : 'Failed to persist bootstrap completion',
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
return { ok: true, aid, reloaded: false };
|
|
1304
|
-
}
|
|
1217
|
+
return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon; agent ready requires daemon IPC' };
|
|
1305
1218
|
if (!result.ok) {
|
|
1306
1219
|
return { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
|
|
1307
1220
|
}
|
package/dist/cli/aun-commands.js
CHANGED
|
@@ -1698,7 +1698,7 @@ Options:
|
|
|
1698
1698
|
else if (r.handoff_id)
|
|
1699
1699
|
console.log(`✓ 已排队 handoff ${r.handoff_id}(尚未投递)`);
|
|
1700
1700
|
else
|
|
1701
|
-
console.log(`✓ 已发送 message_id=${r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
|
|
1701
|
+
console.log(`✓ 已发送 message_id=${r.message_id ?? r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
|
|
1702
1702
|
});
|
|
1703
1703
|
return;
|
|
1704
1704
|
}
|
package/dist/cli/cli-argv.js
CHANGED
|
@@ -3,6 +3,16 @@ export function normalizeCliArgv(argv) {
|
|
|
3
3
|
return argv.slice(1);
|
|
4
4
|
return argv;
|
|
5
5
|
}
|
|
6
|
+
export function splitConfigBatchGetArgv(inputArgv) {
|
|
7
|
+
const argv = normalizeCliArgv(inputArgv);
|
|
8
|
+
if (argv[0] !== 'config' || argv[1] !== 'get')
|
|
9
|
+
return null;
|
|
10
|
+
const fields = [];
|
|
11
|
+
let i = 2;
|
|
12
|
+
for (; i < argv.length && !argv[i].startsWith('-'); i++)
|
|
13
|
+
fields.push(argv[i]);
|
|
14
|
+
return { fields, rest: argv.slice(i) };
|
|
15
|
+
}
|
|
6
16
|
export function validateCliArgv(value) {
|
|
7
17
|
if (!Array.isArray(value) || value.length === 0)
|
|
8
18
|
return { ok: false, reason: 'args.argv must be a non-empty string array' };
|
package/dist/cli/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand, } from '..
|
|
|
5
5
|
import { ipcQuery } from '../ipc.js';
|
|
6
6
|
import { resolvePaths } from '../paths.js';
|
|
7
7
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
8
|
-
import { splitConfigBatchGetArgv } from '
|
|
8
|
+
import { splitConfigBatchGetArgv } from './cli-argv.js';
|
|
9
9
|
function emit(formatJson, payload, text) {
|
|
10
10
|
if (formatJson)
|
|
11
11
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -47,6 +47,8 @@ function renderConfigExecution(result, formatJson) {
|
|
|
47
47
|
switch (result.subcommand) {
|
|
48
48
|
case 'get':
|
|
49
49
|
return emit(formatJson, getResultPayload(result), () => getResultLine(result));
|
|
50
|
+
case 'batch-get':
|
|
51
|
+
return renderBatchGet(result.results, formatJson);
|
|
50
52
|
case 'set':
|
|
51
53
|
return emit(formatJson, {
|
|
52
54
|
ok: true,
|
|
@@ -186,23 +188,13 @@ function runDirectBatchGet(fields, rest, formatJson) {
|
|
|
186
188
|
}
|
|
187
189
|
async function runManagedBatchGet(fields, rest, formatJson) {
|
|
188
190
|
const { sessionId, delegationToken } = managedSession(formatJson);
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
else if (!response.ok || !response.result || !response.result.ok) {
|
|
196
|
-
results.push({ ok: false, field, code: response.code || 'CONFIG_AUTH_FAILED', error: response.error || 'config operation failed' });
|
|
197
|
-
}
|
|
198
|
-
else if (response.result.subcommand === 'get') {
|
|
199
|
-
results.push(response.result);
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
results.push({ ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` });
|
|
203
|
-
}
|
|
191
|
+
const response = await managedConfigQuery(['get', ...fields, ...rest], sessionId, delegationToken);
|
|
192
|
+
if (!response)
|
|
193
|
+
fail(formatJson, 'DAEMON_UNAVAILABLE', 'Unable to connect to the evolcore daemon');
|
|
194
|
+
if (!response.ok || !response.result) {
|
|
195
|
+
fail(formatJson, response.code || 'CONFIG_AUTH_FAILED', response.error || 'config operation failed', response.data);
|
|
204
196
|
}
|
|
205
|
-
|
|
197
|
+
renderConfigExecution(response.result, formatJson);
|
|
206
198
|
}
|
|
207
199
|
const HELP = `Usage: ec config <command> [options]
|
|
208
200
|
|
package/dist/cli/ctl-command.js
CHANGED
|
@@ -389,9 +389,6 @@ export async function cmdStart(opts = {}) {
|
|
|
389
389
|
cleanEnv();
|
|
390
390
|
// 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
|
|
391
391
|
ensureServiceProxyConfigBeforeStart(p);
|
|
392
|
-
const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
|
|
393
|
-
? await startEcwebIfEnabled(p)
|
|
394
|
-
: false;
|
|
395
392
|
// 删除旧的 ready signal
|
|
396
393
|
try {
|
|
397
394
|
fs.unlinkSync(p.readySignal);
|
|
@@ -421,23 +418,11 @@ export async function cmdStart(opts = {}) {
|
|
|
421
418
|
console.log(` EVOLCORE_HOME: ${resolveRoot()}`);
|
|
422
419
|
console.log(` Logs: ${p.logs}/`);
|
|
423
420
|
console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
})
|
|
431
|
-
.catch((err) => {
|
|
432
|
-
console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
|
-
if (shouldPrintNoSelfAgentHints) {
|
|
434
|
-
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
else if (shouldPrintNoSelfAgentHints) {
|
|
439
|
-
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
|
|
440
|
-
}
|
|
421
|
+
void waitForExistingEcweb(p).then((ecweb) => {
|
|
422
|
+
if (shouldPrintNoSelfAgentHints) {
|
|
423
|
+
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: ecweb !== null, skipped });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
441
426
|
}, 500);
|
|
442
427
|
let forwardingShutdown = false;
|
|
443
428
|
const forwardSignal = (signal) => {
|
|
@@ -536,8 +521,9 @@ export async function cmdStart(opts = {}) {
|
|
|
536
521
|
printCodeStats(getPackageRoot(), p.logs);
|
|
537
522
|
}
|
|
538
523
|
console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
539
|
-
// ECWeb
|
|
540
|
-
|
|
524
|
+
// ECWeb lifecycle belongs to the daemon; the CLI only reports whether
|
|
525
|
+
// the supervised instance became healthy.
|
|
526
|
+
const ecwebStarted = await waitForExistingEcweb(p) !== null;
|
|
541
527
|
if (shouldPrintNoSelfAgentHints) {
|
|
542
528
|
printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
|
|
543
529
|
}
|
|
@@ -2422,6 +2408,24 @@ async function probeEcwebReady(port) {
|
|
|
2422
2408
|
return false;
|
|
2423
2409
|
}
|
|
2424
2410
|
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Wait briefly for an already-launched ECWeb to become healthy. `ec start`,
|
|
2413
|
+
* restart-monitor, and the daemon can overlap by a few milliseconds; treating
|
|
2414
|
+
* a healthy instance as an adoption target avoids killing/replacing it.
|
|
2415
|
+
*/
|
|
2416
|
+
async function waitForExistingEcweb(p, timeoutMs = 3_000) {
|
|
2417
|
+
const deadline = Date.now() + timeoutMs;
|
|
2418
|
+
do {
|
|
2419
|
+
for (const entry of findAliveEcwebs(p)) {
|
|
2420
|
+
if (await probeEcwebReady(entry.port))
|
|
2421
|
+
return entry;
|
|
2422
|
+
}
|
|
2423
|
+
if (Date.now() >= deadline)
|
|
2424
|
+
break;
|
|
2425
|
+
await sleep(250);
|
|
2426
|
+
} while (true);
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2425
2429
|
/**
|
|
2426
2430
|
* 输出 ECWeb 状态。判定口径:
|
|
2427
2431
|
* 1. 进程是否存活(instance/ 下的 pid 文件 + isProcessRunning)
|
|
@@ -2633,10 +2637,21 @@ function ensureServiceProxyConfig(cfg, port) {
|
|
|
2633
2637
|
* Kept public for restart-monitor, which launches the daemon directly and
|
|
2634
2638
|
* therefore cannot rely on cmdStart's post-ready ECWeb startup path.
|
|
2635
2639
|
*/
|
|
2636
|
-
export async function startEcwebIfEnabled(p) {
|
|
2640
|
+
export async function startEcwebIfEnabled(p, options = {}) {
|
|
2637
2641
|
const cfg = loadDaemonConfig();
|
|
2638
2642
|
if (!cfg.ecweb?.enabled)
|
|
2639
2643
|
return false;
|
|
2644
|
+
// The daemon is the lifecycle owner. CLI callers normally only need to
|
|
2645
|
+
// ensure ECWeb is available, so adopt a healthy process instead of killing
|
|
2646
|
+
// it. `ec watch web` passes forceRestart explicitly for an intentional
|
|
2647
|
+
// manual replacement.
|
|
2648
|
+
if (!options.forceRestart) {
|
|
2649
|
+
const existing = await waitForExistingEcweb(p);
|
|
2650
|
+
if (existing) {
|
|
2651
|
+
console.log(`✓ ECWeb 已在运行 (PID: ${existing.pid}) http://localhost:${existing.port}`);
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2640
2655
|
const installedPkg = resolveGlobalPkg(WEB_PACKAGE_NAME);
|
|
2641
2656
|
if (!installedPkg) {
|
|
2642
2657
|
console.log(`⚠ EC Web 未安装,跳过启动(运行 npm i -g ${WEB_PACKAGE_NAME}@latest 后可用)`);
|
|
@@ -2795,7 +2810,7 @@ async function cmdWatchWeb() {
|
|
|
2795
2810
|
saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true, port } });
|
|
2796
2811
|
// 自动配置 serviceProxy(如果控制 AID 已配置)
|
|
2797
2812
|
ensureServiceProxyConfigBeforeStart(p);
|
|
2798
|
-
const ok = await startEcwebIfEnabled(p);
|
|
2813
|
+
const ok = await startEcwebIfEnabled(p, { forceRestart: true });
|
|
2799
2814
|
if (!ok)
|
|
2800
2815
|
process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
|
|
2801
2816
|
// 启动成功的访问信息已由 startEcwebIfEnabled 打印,无需 printEcwebAccess 重复输出
|
package/dist/cli/fs-command.js
CHANGED
|
@@ -6,6 +6,7 @@ import { agentmdGet, getAidStore, isValidAid, loadClient, SLOT } from '../aun/ai
|
|
|
6
6
|
import { ipcQuery } from '../ipc.js';
|
|
7
7
|
import { resolvePaths } from '../paths.js';
|
|
8
8
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
9
|
+
import { downloadGroupFsBytes } from '../aun/group-fs-download.js';
|
|
9
10
|
class FsCliError extends Error {
|
|
10
11
|
code;
|
|
11
12
|
suggestion;
|
|
@@ -33,8 +34,8 @@ Commands:
|
|
|
33
34
|
mkdir [-p] <AID>:<path> 创建目录
|
|
34
35
|
ln -s <target> <AID>:<path> 创建软链(personal storage)
|
|
35
36
|
chmod [mode] <AID>:<path> 切换公开/私有(personal storage)
|
|
36
|
-
setfacl <AID>:<path> -m|-x ... 设置/移除 ACL
|
|
37
|
-
getfacl <AID>:<path> 查看 ACL
|
|
37
|
+
setfacl <AID>:<path> -m|-x ... 设置/移除 ACL(群路径使用 role:member|role:admin)
|
|
38
|
+
getfacl <AID>:<path> 查看 ACL
|
|
38
39
|
token issue|revoke|ls <path> 管理访问 token(personal storage)
|
|
39
40
|
find <AID>:<path> [filters] 查找节点
|
|
40
41
|
df <AID>: 查看容量/配额
|
|
@@ -257,6 +258,27 @@ async function runManagedFs(args, formatJson) {
|
|
|
257
258
|
for (const item of result.items ?? [])
|
|
258
259
|
console.log(JSON.stringify(item));
|
|
259
260
|
return;
|
|
261
|
+
case 'mkdir':
|
|
262
|
+
console.log(`✓ 已创建目录: ${result.path}`);
|
|
263
|
+
return;
|
|
264
|
+
case 'upload':
|
|
265
|
+
console.log(`✓ 已上传: ${result.localPath} -> ${result.path}`);
|
|
266
|
+
return;
|
|
267
|
+
case 'copy':
|
|
268
|
+
console.log(`✓ 已复制: ${result.src} -> ${result.path}`);
|
|
269
|
+
return;
|
|
270
|
+
case 'move':
|
|
271
|
+
console.log(`✓ 已移动: ${result.src} -> ${result.path}`);
|
|
272
|
+
return;
|
|
273
|
+
case 'remove':
|
|
274
|
+
console.log(`✓ 已删除: ${result.path}`);
|
|
275
|
+
return;
|
|
276
|
+
case 'getfacl':
|
|
277
|
+
console.log(JSON.stringify(result.result ?? {}, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
case 'setfacl':
|
|
280
|
+
console.log(`✓ 已${result.removed ? '移除' : '设置'}群 ACL: ${result.path}`);
|
|
281
|
+
return;
|
|
260
282
|
}
|
|
261
283
|
}
|
|
262
284
|
function parseCommonOptions(args) {
|
|
@@ -385,8 +407,9 @@ async function fsCat(args, opts) {
|
|
|
385
407
|
if (route.backend === 'group') {
|
|
386
408
|
statInfo = await client.group.fs.stat(remoteRef(target));
|
|
387
409
|
const size = numberFrom(statInfo, ['size', 'size_bytes', 'bytes']);
|
|
388
|
-
const downloaded = await client
|
|
389
|
-
bytes =
|
|
410
|
+
const downloaded = await downloadGroupFsBytes(client, remoteRef(target));
|
|
411
|
+
bytes = downloaded.bytes;
|
|
412
|
+
statInfo = { ...statInfo, ...downloaded.ticket };
|
|
390
413
|
statInfo = { ...statInfo, size: bytes.byteLength };
|
|
391
414
|
if (size !== undefined && size > opts.maxCatBytes) {
|
|
392
415
|
binaryHeadOnly = true;
|
|
@@ -821,10 +844,16 @@ async function fsSetfacl(args, opts) {
|
|
|
821
844
|
}
|
|
822
845
|
await withClient(opts, async ({ client, store }) => {
|
|
823
846
|
const route = await resolveRoute(target.aid, store);
|
|
824
|
-
if (route.backend === 'group') {
|
|
825
|
-
throw new FsCliError('UNSUPPORTED', 'group fs 当前 SDK facade 没有 setfacl 接口', '请在 personal storage 中使用 setfacl。');
|
|
826
|
-
}
|
|
827
847
|
if (modify) {
|
|
848
|
+
if (route.backend === 'group') {
|
|
849
|
+
const [role, rolePerms] = parseGroupAclSpec(modify, true);
|
|
850
|
+
if (expiresAt !== undefined || maxUses !== undefined) {
|
|
851
|
+
throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 不支持 --expires 或 --max-uses');
|
|
852
|
+
}
|
|
853
|
+
const result = await client.group.fs.setAcl(remoteRef(target), { granteeAid: role, perms: rolePerms });
|
|
854
|
+
outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已设置群 ACL: ${remoteRef(target)}`));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
828
857
|
const [grantee, perms] = parseAclSpec(modify, true);
|
|
829
858
|
const result = await client.storage.setAcl(target.path, {
|
|
830
859
|
owner: target.aid,
|
|
@@ -836,6 +865,12 @@ async function fsSetfacl(args, opts) {
|
|
|
836
865
|
outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已设置 ACL: ${remoteRef(target)}`));
|
|
837
866
|
return;
|
|
838
867
|
}
|
|
868
|
+
if (route.backend === 'group') {
|
|
869
|
+
const [role] = parseGroupAclSpec(remove || '', false);
|
|
870
|
+
const result = await client.group.fs.removeAcl(remoteRef(target), { granteeAid: role });
|
|
871
|
+
outputSuccess(opts, { command: 'setfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(`✓ 已移除群 ACL: ${remoteRef(target)}`));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
839
874
|
const [grantee] = parseAclSpec(remove || '', false);
|
|
840
875
|
const result = await client.storage.removeAcl(target.path, {
|
|
841
876
|
owner: target.aid,
|
|
@@ -848,10 +883,9 @@ async function fsGetfacl(args, opts) {
|
|
|
848
883
|
const target = parseRemoteRequired(firstPositional(args), { command: 'getfacl' });
|
|
849
884
|
await withClient(opts, async ({ client, store }) => {
|
|
850
885
|
const route = await resolveRoute(target.aid, store);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const result = await client.storage.listAcl(target.path, { owner: target.aid });
|
|
886
|
+
const result = route.backend === 'group'
|
|
887
|
+
? await client.group.fs.getAcl(remoteRef(target))
|
|
888
|
+
: await client.storage.listAcl(target.path, { owner: target.aid });
|
|
855
889
|
outputSuccess(opts, { command: 'getfacl', backend: route.backend, route, path: remoteRef(target), result }, () => console.log(JSON.stringify(result, null, 2)));
|
|
856
890
|
});
|
|
857
891
|
}
|
|
@@ -1288,6 +1322,26 @@ function parseAclSpec(value, requirePerms) {
|
|
|
1288
1322
|
}
|
|
1289
1323
|
return [parts[1]];
|
|
1290
1324
|
}
|
|
1325
|
+
function parseGroupAclSpec(value, requirePerms) {
|
|
1326
|
+
const parts = value.split(':');
|
|
1327
|
+
if (parts[0] !== 'role' || !parts[1] || (parts.length !== (requirePerms ? 3 : 2))) {
|
|
1328
|
+
throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 条目格式应为 role:member:<perms>、role:admin:<perms> 或 role:<role>');
|
|
1329
|
+
}
|
|
1330
|
+
const role = `role:${parts[1]}`;
|
|
1331
|
+
if (!['role:member', 'role:admin'].includes(role)) {
|
|
1332
|
+
throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 仅支持 role:member 或 role:admin');
|
|
1333
|
+
}
|
|
1334
|
+
if (!requirePerms)
|
|
1335
|
+
return [role];
|
|
1336
|
+
const perms = parts[2];
|
|
1337
|
+
if (!perms || !/^(?:r|rw|rwx)$/.test(perms)) {
|
|
1338
|
+
throw new FsCliError('INVALID_ARGUMENT', '群空间 ACL 权限必须是 r、rw 或 rwx');
|
|
1339
|
+
}
|
|
1340
|
+
if (role === 'role:member' && perms !== 'rw') {
|
|
1341
|
+
throw new FsCliError('INVALID_ARGUMENT', 'role:member 只能授予 rw 群空间 ACL');
|
|
1342
|
+
}
|
|
1343
|
+
return [role, perms];
|
|
1344
|
+
}
|
|
1291
1345
|
function assertLocalFile(localPath) {
|
|
1292
1346
|
let st;
|
|
1293
1347
|
try {
|
|
@@ -1420,14 +1474,6 @@ function contentTypeFrom(value) {
|
|
|
1420
1474
|
const raw = value?.contentType ?? value?.content_type ?? value?.mime ?? value?.mime_type;
|
|
1421
1475
|
return raw ? String(raw).toLowerCase() : undefined;
|
|
1422
1476
|
}
|
|
1423
|
-
function bytesFromDownload(value) {
|
|
1424
|
-
const data = value?.data;
|
|
1425
|
-
if (data instanceof Uint8Array)
|
|
1426
|
-
return data;
|
|
1427
|
-
if (data instanceof ArrayBuffer)
|
|
1428
|
-
return new Uint8Array(data);
|
|
1429
|
-
throw new FsCliError('BACKEND_ERROR', '下载接口未返回字节内容', undefined, value);
|
|
1430
|
-
}
|
|
1431
1477
|
function looksBinary(bytes, contentType) {
|
|
1432
1478
|
if (contentType) {
|
|
1433
1479
|
if (contentType.startsWith('text/'))
|
package/dist/cli/init.js
CHANGED
|
@@ -7,7 +7,7 @@ import { commandExists } from '../utils/cross-platform.js';
|
|
|
7
7
|
import { scanInstances } from '../utils/instance-registry.js';
|
|
8
8
|
import { saveDefaultsSafe, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
|
|
9
9
|
import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
|
|
10
|
-
import { getCodexAppServerAvailability
|
|
10
|
+
import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
|
|
11
11
|
import { resolveEcagentConfig } from '../agents/baseagent.js';
|
|
12
12
|
import { defaultProjectsRoot } from '../utils/project-path.js';
|
|
13
13
|
import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
|
|
@@ -28,15 +28,44 @@ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
|
|
|
28
28
|
}
|
|
29
29
|
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
30
30
|
const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
31
|
-
function isBaseagentAvailable(baseagent) {
|
|
31
|
+
function isBaseagentAvailable(baseagent, codexAvailability) {
|
|
32
32
|
if (baseagent === 'codex')
|
|
33
|
-
return
|
|
33
|
+
return (codexAvailability ?? getCodexAppServerAvailability()).available;
|
|
34
34
|
if (baseagent === 'ecagent')
|
|
35
35
|
return true;
|
|
36
36
|
return commandExists(baseagent);
|
|
37
37
|
}
|
|
38
|
-
function detectAvailable() {
|
|
39
|
-
return BASEAGENT_CANDIDATES.filter(isBaseagentAvailable);
|
|
38
|
+
function detectAvailable(codexAvailability = getCodexAppServerAvailability()) {
|
|
39
|
+
return BASEAGENT_CANDIDATES.filter(baseagent => isBaseagentAvailable(baseagent, codexAvailability));
|
|
40
|
+
}
|
|
41
|
+
/** Offer installation only when the probe positively identified an old CLI. */
|
|
42
|
+
export async function maybeUpgradeCodexForInit(availability, dependencies) {
|
|
43
|
+
if (!availability.requiresUpgrade) {
|
|
44
|
+
return { availability, offered: false, accepted: false };
|
|
45
|
+
}
|
|
46
|
+
const accepted = await dependencies.confirm();
|
|
47
|
+
if (!accepted)
|
|
48
|
+
return { availability, offered: true, accepted: false };
|
|
49
|
+
try {
|
|
50
|
+
const install = dependencies.install ?? (async () => {
|
|
51
|
+
const { npmInstallGlobal } = await import('../utils/npm-ops.js');
|
|
52
|
+
await npmInstallGlobal('@openai/codex@latest');
|
|
53
|
+
});
|
|
54
|
+
await install();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
availability,
|
|
59
|
+
offered: true,
|
|
60
|
+
accepted: true,
|
|
61
|
+
error: installErrorMessage(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
availability: (dependencies.probe ?? getCodexAppServerAvailability)(),
|
|
66
|
+
offered: true,
|
|
67
|
+
accepted: true,
|
|
68
|
+
};
|
|
40
69
|
}
|
|
41
70
|
function pickDefault(available) {
|
|
42
71
|
return AUTO_BASEAGENT_CANDIDATES.find(candidate => available.includes(candidate));
|
|
@@ -192,7 +221,45 @@ export async function cmdInit(options) {
|
|
|
192
221
|
return false;
|
|
193
222
|
}
|
|
194
223
|
// ── 2. 探测 baseagent ──
|
|
195
|
-
|
|
224
|
+
let codexAvailability = getCodexAppServerAvailability();
|
|
225
|
+
if (!options?.nonInteractive && codexAvailability.version) {
|
|
226
|
+
console.log(`检测到 Codex CLI: ${codexAvailability.version}`);
|
|
227
|
+
}
|
|
228
|
+
if (!options?.nonInteractive && codexAvailability.requiresUpgrade) {
|
|
229
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
230
|
+
let result;
|
|
231
|
+
try {
|
|
232
|
+
result = await maybeUpgradeCodexForInit(codexAvailability, {
|
|
233
|
+
confirm: async () => {
|
|
234
|
+
console.log(`⚠ ${codexAvailability.reason}`);
|
|
235
|
+
const answer = (await ask(rl, '是否立即升级 Codex CLI?[Y/n] ')).trim().toLowerCase();
|
|
236
|
+
return answer === '' || answer === 'y' || answer === 'yes';
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
try {
|
|
242
|
+
rl.close();
|
|
243
|
+
}
|
|
244
|
+
catch { /* ignore */ }
|
|
245
|
+
}
|
|
246
|
+
if (!result.accepted) {
|
|
247
|
+
console.log(' 已跳过 Codex CLI 升级');
|
|
248
|
+
}
|
|
249
|
+
else if (result.error) {
|
|
250
|
+
console.log(` ⚠ Codex CLI 升级失败: ${result.error}`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
codexAvailability = result.availability;
|
|
254
|
+
if (codexAvailability.available) {
|
|
255
|
+
console.log(` ✓ Codex CLI 已升级至 ${codexAvailability.version}`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log(` ⚠ Codex CLI 升级后仍不可用: ${codexAvailability.reason ?? '未知原因'}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const available = detectAvailable(codexAvailability);
|
|
196
263
|
if (available.length === 0 || (available.length === 1 && available[0] === 'ecagent' && ecagentUnavailableReason())) {
|
|
197
264
|
console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
|
|
198
265
|
console.log(' - claude CLI');
|
|
@@ -202,8 +269,10 @@ export async function cmdInit(options) {
|
|
|
202
269
|
return false;
|
|
203
270
|
}
|
|
204
271
|
console.log('检测到可用的 baseagent:');
|
|
205
|
-
for (const b of available)
|
|
206
|
-
|
|
272
|
+
for (const b of available) {
|
|
273
|
+
const version = b === 'codex' ? codexAvailability.version : undefined;
|
|
274
|
+
console.log(` ● ${b}${version ? ` (CLI ${version})` : ''}`);
|
|
275
|
+
}
|
|
207
276
|
console.log('');
|
|
208
277
|
const defaultsPath = p.defaultsConfig;
|
|
209
278
|
const defaultsExisted = fs.existsSync(defaultsPath);
|