evolcore 0.0.22 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +24 -14
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/install-codex-managed-hooks.mjs +373 -95
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +30 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
- 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 +154 -0
- package/dist/agents/codex-runner.js +407 -139
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -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 +51 -4
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +31 -1
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +142 -45
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +12 -2
- package/dist/cli/trigger-command.js +9 -1
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +10 -7
- package/dist/core/auth/authorization-audit.js +4 -0
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/command/command-handler.js +41 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +141 -33
- package/dist/core/command/menu-protocol.js +119 -0
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +42 -22
- package/dist/core/message/message-queue.js +20 -15
- package/dist/core/message/response-engine.js +74 -73
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -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 +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- 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 +244 -25
- package/dist/core/runtime-lock.js +13 -8
- package/dist/index.js +40 -29
- package/dist/ipc.js +123 -81
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +77 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/npm-ops.js +47 -15
- 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/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +8 -1
- package/package.json +11 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { getArgValue, wantsHelp } from './help.js';
|
|
3
|
+
import { executeMenuClientRequest, MenuClientError } from '../aun/menu-client.js';
|
|
4
|
+
function parseDurationMs(value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/i);
|
|
8
|
+
if (!match)
|
|
9
|
+
throw new MenuClientError('INVALID_ARGUMENT', `Invalid timeout: ${value}`);
|
|
10
|
+
const amount = Number(match[1]);
|
|
11
|
+
const unit = match[2].toLowerCase();
|
|
12
|
+
return amount * (unit === 'ms' ? 1 : unit === 's' ? 1_000 : unit === 'm' ? 60_000 : 3_600_000);
|
|
13
|
+
}
|
|
14
|
+
function readRequest(args) {
|
|
15
|
+
const file = getArgValue(args, '--request-file');
|
|
16
|
+
const inline = getArgValue(args, '--request-json');
|
|
17
|
+
if (!!file === !!inline)
|
|
18
|
+
throw new MenuClientError('INVALID_ARGUMENT', '--request-file and --request-json are mutually exclusive');
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = file ? fs.readFileSync(file, 'utf8') : inline;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new MenuClientError('INVALID_ARGUMENT', `Unable to read request: ${error?.message ?? String(error)}`);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
29
|
+
throw new Error('request must be a JSON object');
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new MenuClientError('INVALID_ARGUMENT', `Invalid request JSON: ${error?.message ?? String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function validateOptions(args) {
|
|
37
|
+
const valueOptions = new Set(['--request-file', '--request-json', '--timeout', '--remote-token', '--ecweb-token', '--app', '--aun-path', '--format']);
|
|
38
|
+
const booleanOptions = new Set(['--encrypt', '--no-encrypt']);
|
|
39
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
40
|
+
const option = args[index];
|
|
41
|
+
if (booleanOptions.has(option))
|
|
42
|
+
continue;
|
|
43
|
+
if (!valueOptions.has(option))
|
|
44
|
+
throw new MenuClientError('INVALID_ARGUMENT', `Unknown menu-client option: ${option}`);
|
|
45
|
+
if (index + 1 >= args.length || args[index + 1].startsWith('--')) {
|
|
46
|
+
throw new MenuClientError('INVALID_ARGUMENT', `${option} requires a value`);
|
|
47
|
+
}
|
|
48
|
+
index += 1;
|
|
49
|
+
}
|
|
50
|
+
if (args.includes('--encrypt') && args.includes('--no-encrypt')) {
|
|
51
|
+
throw new MenuClientError('INVALID_ARGUMENT', '--encrypt and --no-encrypt are mutually exclusive');
|
|
52
|
+
}
|
|
53
|
+
const format = getArgValue(args, '--format');
|
|
54
|
+
if (format !== undefined && format !== 'json')
|
|
55
|
+
throw new MenuClientError('INVALID_ARGUMENT', '--format currently supports only json');
|
|
56
|
+
}
|
|
57
|
+
export async function cmdMenuClient(args) {
|
|
58
|
+
if (wantsHelp(args)) {
|
|
59
|
+
console.log(`用法: ec menu-client request <from-aid> <daemon-aid> [选项]
|
|
60
|
+
|
|
61
|
+
--request-file <path> 从 JSON 文件读取一个 menu.* 请求
|
|
62
|
+
--request-json <json> 直接传入一个 menu.* JSON 请求
|
|
63
|
+
--timeout <时长> 默认 15s,最大 120s
|
|
64
|
+
--encrypt 使用 E2EE(默认)
|
|
65
|
+
--no-encrypt 已拒绝:ec-menu-client 必须使用 E2EE
|
|
66
|
+
--remote-token <token> 使用 daemon 远程访问 token 临时授权
|
|
67
|
+
--ecweb-token <token> --remote-token 的兼容别名
|
|
68
|
+
--app <slot> 覆盖默认 evolcore-menu-client 消费槽位
|
|
69
|
+
--aun-path <path> 覆盖本地 AUN keystore 根目录
|
|
70
|
+
--format json 输出标准 menu.response JSON`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (args[0] !== 'request')
|
|
74
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'menu-client currently supports only the request subcommand');
|
|
75
|
+
const fromAid = args[1]?.trim();
|
|
76
|
+
const targetAid = args[2]?.trim();
|
|
77
|
+
if (!fromAid || !targetAid)
|
|
78
|
+
throw new MenuClientError('INVALID_ARGUMENT', 'request requires <from-aid> and <daemon-aid>');
|
|
79
|
+
const options = args.slice(3);
|
|
80
|
+
validateOptions(options);
|
|
81
|
+
const request = readRequest(options);
|
|
82
|
+
const timeoutMs = parseDurationMs(getArgValue(args, '--timeout'));
|
|
83
|
+
const encrypt = args.includes('--no-encrypt') ? false : true;
|
|
84
|
+
const response = await executeMenuClientRequest({
|
|
85
|
+
fromAid,
|
|
86
|
+
targetAid,
|
|
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
|
+
});
|
|
94
|
+
console.log(JSON.stringify(response, null, 2));
|
|
95
|
+
}
|
package/dist/cli/model.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
|
|
14
14
|
import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, writeScope, clearScope, } from '../core/model/config-scope.js';
|
|
15
15
|
import { resolveEffectiveFieldWithSource } from '../config/config-manager.js';
|
|
16
|
-
import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
|
|
16
|
+
import { getCatalog, getModelInfo, catalogEfforts, resolveCatalogModel, assertCatalogSelectable } from '../core/model/model-catalog.js';
|
|
17
17
|
import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole, } from '../core/model/model-permission.js';
|
|
18
18
|
import { resolvePaths } from '../paths.js';
|
|
19
19
|
import { ipcQuery } from '../ipc.js';
|
|
@@ -23,7 +23,7 @@ import { getFieldPermission } from '../config/roles.js';
|
|
|
23
23
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
24
24
|
import { formatModelCheck, runModelCheck } from '../core/model/model-diagnostics.js';
|
|
25
25
|
import { validateRuntimeStringFieldOverride } from '../core/role/runtime-policy.js';
|
|
26
|
-
const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
26
|
+
const ALL_EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
27
27
|
const SCOPE_LABEL = {
|
|
28
28
|
global: '全局', agent: 'agent级', role: '角色级', relation: '关系级',
|
|
29
29
|
};
|
|
@@ -298,7 +298,7 @@ async function cmdList(args, formatJson) {
|
|
|
298
298
|
const cat = await getCatalog(sel.self, ba);
|
|
299
299
|
const resolved = resolveModelConfig(effectiveSel, ba);
|
|
300
300
|
const allowedModelIds = role
|
|
301
|
-
? filterModelsForRole(role, ba, cat.models.map(m => m.id),
|
|
301
|
+
? filterModelsForRole(role, ba, cat.models.map(m => m.id), model => resolveCatalogModel(cat, model), sel.self)
|
|
302
302
|
: null;
|
|
303
303
|
const models = allowedModelIds
|
|
304
304
|
? allowedModelIds.map(id => cat.models.find(m => m.id === id) ?? { id })
|
|
@@ -308,15 +308,21 @@ async function cmdList(args, formatJson) {
|
|
|
308
308
|
effective: { model: resolved.model ?? null, source: resolved.source ?? null },
|
|
309
309
|
role: role ?? null,
|
|
310
310
|
catalogSource: cat.source,
|
|
311
|
+
freshness: cat.freshness,
|
|
312
|
+
revision: cat.revision,
|
|
313
|
+
fetchedAt: cat.fetchedAt,
|
|
314
|
+
diagnostics: cat.diagnostics,
|
|
311
315
|
models,
|
|
312
316
|
}, () => {
|
|
313
317
|
const lines = [];
|
|
314
318
|
lines.push(`当前生效: ${resolved.model ?? '(未设置,回落 SDK 默认)'}` +
|
|
315
319
|
(resolved.source ? ` (来源: ${SCOPE_LABEL[resolved.source]})` : ''));
|
|
316
320
|
lines.push('');
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
if (resolved.model && !cat.models.some(m => m.id === resolved.model))
|
|
322
|
+
lines.push('当前配置未列入目录');
|
|
323
|
+
lines.push(...cat.diagnostics.map(d => d.message));
|
|
324
|
+
const srcTag = cat.freshness === 'fallback' ? ` [${cat.source}/unverified]`
|
|
325
|
+
: cat.freshness === 'stale' ? ' [stale]' : '';
|
|
320
326
|
lines.push(`可用模型 (${models.length})${srcTag}:`);
|
|
321
327
|
for (const e of models) {
|
|
322
328
|
const live = resolved.model === e.id ? '✓' : ' ';
|
|
@@ -430,6 +436,7 @@ async function cmdUse(args, formatJson) {
|
|
|
430
436
|
const ba = activeBaseagent(sel.self);
|
|
431
437
|
// 校验模型在 catalog 中
|
|
432
438
|
const cat = await getCatalog(sel.self, ba);
|
|
439
|
+
assertCatalogSelectable(cat);
|
|
433
440
|
if (!cat.models.some(m => m.id === modelId)) {
|
|
434
441
|
fail(formatJson, 'UNKNOWN_MODEL', `模型不在目录中: ${modelId}(model list 查看可用模型)`);
|
|
435
442
|
}
|
|
@@ -439,14 +446,15 @@ async function cmdUse(args, formatJson) {
|
|
|
439
446
|
baseagent: ba,
|
|
440
447
|
requestedModel: modelId,
|
|
441
448
|
models: cat.models.map(m => m.id),
|
|
449
|
+
resolveModelId: model => resolveCatalogModel(cat, model),
|
|
442
450
|
selfAid: sel.self,
|
|
443
451
|
});
|
|
444
452
|
if (!decision.ok)
|
|
445
453
|
fail(formatJson, decision.code || 'MODEL_NOT_ALLOWED', decision.message || `model not allowed: ${modelId}`);
|
|
446
454
|
}
|
|
447
455
|
const effort = getArgValue(args, '--effort');
|
|
448
|
-
if (effort !== undefined && !
|
|
449
|
-
fail(formatJson, 'INVALID_EFFORT',
|
|
456
|
+
if (effort !== undefined && effort !== 'auto' && !catalogEfforts(cat, modelId, ba).includes(effort)) {
|
|
457
|
+
fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${effort}`);
|
|
450
458
|
}
|
|
451
459
|
const effortVal = effort === 'auto' ? null : (effort ?? undefined);
|
|
452
460
|
if (isManagedTargetedWriteEnv(args)) {
|
|
@@ -492,6 +500,13 @@ async function cmdEffort(args, formatJson) {
|
|
|
492
500
|
const scope = determineWriteScope(sel, formatJson);
|
|
493
501
|
const ba = activeBaseagent(sel.self);
|
|
494
502
|
const val = level === 'auto' ? null : level;
|
|
503
|
+
if (val) {
|
|
504
|
+
const cat = await getCatalog(sel.self, ba);
|
|
505
|
+
assertCatalogSelectable(cat);
|
|
506
|
+
const model = resolveModelConfig(selectorWithRole(sel, explicitOrDerivedRole(args, sel)), ba).model;
|
|
507
|
+
if (!model || !catalogEfforts(cat, model, ba).includes(val))
|
|
508
|
+
fail(formatJson, 'INVALID_EFFORT', `当前模型目录未声明支持推理强度: ${val}`);
|
|
509
|
+
}
|
|
495
510
|
if (isManagedTargetedWriteEnv(args)) {
|
|
496
511
|
const effortKey = 'effort';
|
|
497
512
|
preflightManagedFieldOverride(sel, explicitOrDerivedRole(args, sel), `baseagents.${ba}.${effortKey}`, val, formatJson);
|
package/dist/cli/task-context.js
CHANGED
|
@@ -253,8 +253,18 @@ export function getManagedTaskTempDir(runtimeEnv) {
|
|
|
253
253
|
* silently escape the session-owned temporary workspace.
|
|
254
254
|
*/
|
|
255
255
|
export function ensureSessionRuntimeDir(sessionId) {
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
if (!sessionId)
|
|
257
|
+
return undefined;
|
|
258
|
+
let base = process.env.TMPDIR?.trim();
|
|
259
|
+
try {
|
|
260
|
+
if (!base || !path.isAbsolute(base) || !isPrivateDirectory(path.resolve(base))) {
|
|
261
|
+
base = ensureProcessManagedTempDir();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
if (!base || !path.isAbsolute(base))
|
|
258
268
|
return undefined;
|
|
259
269
|
const root = path.resolve(base);
|
|
260
270
|
const digest = crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 24);
|
|
@@ -121,12 +121,20 @@ Create 参数模式支持:
|
|
|
121
121
|
通用输出参数:
|
|
122
122
|
--json | --format json
|
|
123
123
|
|
|
124
|
-
Update
|
|
124
|
+
Update 参数:
|
|
125
125
|
--once | --delay | --at | --cron | --every | --event [--tz]
|
|
126
126
|
--prompt <文本> | --prompt-file <路径> --script-file <路径> --name <名称>
|
|
127
|
+
--description <文本> (空字符串删除)
|
|
127
128
|
--model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
|
|
128
129
|
--max-runs <次数> --max-duration <时长>
|
|
129
130
|
--concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
|
|
131
|
+
--feedback <origin|target|silent>
|
|
132
|
+
--target-channel <channelKey> --target-channel-id <ID> (必须同时指定)
|
|
133
|
+
--target-chat-type <private|group> --target-session <main|thread> [--target-thread-id <threadId>]
|
|
134
|
+
--trigger-thread <by-run|by-trigger> --baseagent <name> (仅 trigger_session 之间)
|
|
135
|
+
--on-error <fail|retry> --noop-sentinel <文本>
|
|
136
|
+
--retry-attempts <次数> --retry-backoff <毫秒>
|
|
137
|
+
--script-runtime <node|python|bash> --script-args <JSON> --script-timeout <时长>
|
|
130
138
|
|
|
131
139
|
省略 --agent 时使用当前 agent AID;无会话上下文或跨身份操作时需要显式指定。
|
|
132
140
|
任务会话中的跨身份操作仅允许目标 Agent 上具备 owner/admin 权限的调用者执行 list/show/history;创建、修改、启停、删除和运行仍只允许当前 Agent。`);
|
|
@@ -342,8 +342,20 @@ function processIsAlive(pid) {
|
|
|
342
342
|
function assertDaemonStopped(home) {
|
|
343
343
|
const candidates = [];
|
|
344
344
|
const pidFile = path.join(home, 'daemon.pid');
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
try {
|
|
346
|
+
// daemon.pid was retired in favor of the instance registry. It may still
|
|
347
|
+
// exist as a stale legacy file, but a Windows Codex sandbox can also leave
|
|
348
|
+
// a directory at this path when protecting a not-yet-created file. Never
|
|
349
|
+
// feed a directory (or symlink) to readFileSync: that turns harmless
|
|
350
|
+
// legacy residue into an EISDIR startup failure.
|
|
351
|
+
const stat = fs.lstatSync(pidFile);
|
|
352
|
+
if (stat.isFile()) {
|
|
353
|
+
candidates.push({ pid: Number(fs.readFileSync(pidFile, 'utf8').trim()), source: pidFile });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (!isRecord(error) || error.code !== 'ENOENT')
|
|
358
|
+
throw error;
|
|
347
359
|
}
|
|
348
360
|
const instanceDir = path.join(home, 'data', 'instance');
|
|
349
361
|
if (fs.existsSync(instanceDir)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { authorizeOperation, buildAuthSubject } from './auth-gateway.js';
|
|
3
3
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
4
5
|
export const AGENT_DELEGATION_TOKEN_ENV = 'EVOLCORE_DELEGATION_TOKEN';
|
|
5
6
|
export const AGENT_DELEGATION_COMMAND_TTL_MS = 5 * 60_000;
|
|
6
7
|
export class AgentDelegationRegistry {
|
|
@@ -43,17 +44,19 @@ export class AgentDelegationRegistry {
|
|
|
43
44
|
: undefined;
|
|
44
45
|
if (approved)
|
|
45
46
|
return { ok: true, grant: approved };
|
|
47
|
+
const diagnosticCause = grant
|
|
48
|
+
? grant.sessionId !== sessionId
|
|
49
|
+
? 'session_mismatch'
|
|
50
|
+
: 'task_replaced'
|
|
51
|
+
: commandHash
|
|
52
|
+
? 'approved_command_unavailable'
|
|
53
|
+
: 'unknown_token';
|
|
54
|
+
logger.warn(`[AgentDelegation] validate failed: cause=${diagnosticCause} session=${sessionId} grantSession=${grant?.sessionId ?? 'none'} taskId=${grant?.taskId ?? 'none'} hasCommandHash=${!!commandHash} tokenKnown=${!!grant} activeForSession=${this.activeHashBySession.get(sessionId) ?? 'none'} grantTokenHash=${grant?.tokenHash ?? 'none'}`);
|
|
46
55
|
return {
|
|
47
56
|
ok: false,
|
|
48
57
|
code: 'INVALID_DELEGATION',
|
|
49
58
|
reason: 'The task delegation token is invalid, revoked, or belongs to another session',
|
|
50
|
-
diagnosticCause
|
|
51
|
-
? grant.sessionId !== sessionId
|
|
52
|
-
? 'session_mismatch'
|
|
53
|
-
: 'task_replaced'
|
|
54
|
-
: commandHash
|
|
55
|
-
? 'approved_command_unavailable'
|
|
56
|
-
: 'unknown_token',
|
|
59
|
+
diagnosticCause,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
armApprovedCommand(input) {
|
|
@@ -185,6 +185,9 @@ export function auditCodexApprovalDecision(input) {
|
|
|
185
185
|
taskId: input.taskId,
|
|
186
186
|
argsSummary: {
|
|
187
187
|
method: input.method,
|
|
188
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
189
|
+
...(input.threadId ? { threadId: input.threadId } : {}),
|
|
190
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
188
191
|
...(input.parseFailure ? { parseFailure: input.parseFailure } : {}),
|
|
189
192
|
},
|
|
190
193
|
});
|
|
@@ -218,6 +221,7 @@ export function auditPermissionDecision(input) {
|
|
|
218
221
|
dangerous: false,
|
|
219
222
|
role: input.role ?? 'unknown',
|
|
220
223
|
taskId: input.taskId,
|
|
224
|
+
argsSummary: input.summary ? { summary: input.summary } : undefined,
|
|
221
225
|
decision: 'deny',
|
|
222
226
|
executed: false,
|
|
223
227
|
executionState: 'blocked',
|
|
@@ -38,6 +38,10 @@ const GROUP_MANAGEMENT_OPERATIONS = [
|
|
|
38
38
|
['ec.group.join', 'write-agent', ['agent'], 'Request to join a group'],
|
|
39
39
|
['ec.group.leave', 'write-agent', ['agent'], 'Leave a group'],
|
|
40
40
|
['ec.group.invite', 'write-agent', ['agent'], 'Invite group members'],
|
|
41
|
+
['ec.group.invites', 'read', ['agent'], 'List pending targeted group invitations'],
|
|
42
|
+
['ec.group.accept-invite', 'write-agent', ['agent'], 'Accept a targeted group invitation'],
|
|
43
|
+
['ec.group.reject-invite', 'write-agent', ['agent'], 'Reject a targeted group invitation'],
|
|
44
|
+
['ec.group.revoke-invite', 'write-agent', ['agent'], 'Revoke a targeted group invitation'],
|
|
41
45
|
['ec.group.kick', 'write-agent', ['agent'], 'Remove a group member'],
|
|
42
46
|
['ec.group.role', 'write-agent', ['agent'], 'Change a group member role'],
|
|
43
47
|
['ec.group.owner', 'write-agent', ['agent'], 'Transfer group ownership'],
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolvePaths } from '../../paths.js';
|
|
5
|
+
export const REMOTE_ACCESS_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
6
|
+
export const REMOTE_ACCESS_PAIRING_TTL_MS = 5 * 60 * 1000;
|
|
7
|
+
export function remoteAccessTokenStorePath() {
|
|
8
|
+
return path.join(resolvePaths().instanceDir, 'control-access-tokens.json');
|
|
9
|
+
}
|
|
10
|
+
function loadStore() {
|
|
11
|
+
try {
|
|
12
|
+
const value = JSON.parse(fs.readFileSync(remoteAccessTokenStorePath(), 'utf8'));
|
|
13
|
+
if (Array.isArray(value.tokens))
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
return { tokens: [] };
|
|
18
|
+
}
|
|
19
|
+
function saveStore(store) {
|
|
20
|
+
const file = remoteAccessTokenStorePath();
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(temporary, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
|
|
25
|
+
fs.renameSync(temporary, file);
|
|
26
|
+
try {
|
|
27
|
+
fs.chmodSync(file, 0o600);
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
if (fs.existsSync(temporary)) {
|
|
33
|
+
try {
|
|
34
|
+
fs.unlinkSync(temporary);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function pruneExpired(store, now) {
|
|
41
|
+
const before = store.tokens.length;
|
|
42
|
+
store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
|
|
43
|
+
&& typeof record.lastActive === 'number'
|
|
44
|
+
&& now - record.lastActive < REMOTE_ACCESS_TOKEN_TTL_MS);
|
|
45
|
+
return store.tokens.length !== before;
|
|
46
|
+
}
|
|
47
|
+
function validLabel(value) {
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return 'remote';
|
|
50
|
+
const normalized = value.trim().replace(/[\r\n]/g, ' ').slice(0, 256);
|
|
51
|
+
return normalized || 'remote';
|
|
52
|
+
}
|
|
53
|
+
function validOwner(value) {
|
|
54
|
+
if (typeof value !== 'string')
|
|
55
|
+
return undefined;
|
|
56
|
+
const normalized = value.trim();
|
|
57
|
+
if (!normalized || normalized.length > 512 || /[\r\n]/.test(normalized))
|
|
58
|
+
return undefined;
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
/** Validate and slide the expiry window of a daemon-owned remote token. */
|
|
62
|
+
export function validateAndRenewRemoteAccessToken(token, now = Date.now()) {
|
|
63
|
+
if (typeof token !== 'string' || !token || token.length > 512)
|
|
64
|
+
return { valid: false };
|
|
65
|
+
try {
|
|
66
|
+
const store = loadStore();
|
|
67
|
+
const changed = pruneExpired(store, now);
|
|
68
|
+
const record = store.tokens.find(candidate => candidate.token === token);
|
|
69
|
+
if (!record) {
|
|
70
|
+
if (changed)
|
|
71
|
+
saveStore(store);
|
|
72
|
+
return { valid: false };
|
|
73
|
+
}
|
|
74
|
+
record.lastActive = now;
|
|
75
|
+
saveStore(store);
|
|
76
|
+
const authorizedBy = validOwner(record.authorizedBy);
|
|
77
|
+
return { valid: true, ...(authorizedBy ? { authorizedBy } : {}) };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { valid: false };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export class RemoteAccessService {
|
|
84
|
+
pairingCode = '';
|
|
85
|
+
pairingExpiresAt = 0;
|
|
86
|
+
pairingAuthorizedBy;
|
|
87
|
+
getPairingCode(authorizedBy, now = Date.now()) {
|
|
88
|
+
if (!this.pairingCode || now >= this.pairingExpiresAt) {
|
|
89
|
+
this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
|
|
90
|
+
this.pairingExpiresAt = now + REMOTE_ACCESS_PAIRING_TTL_MS;
|
|
91
|
+
this.pairingAuthorizedBy = undefined;
|
|
92
|
+
}
|
|
93
|
+
const owner = validOwner(authorizedBy);
|
|
94
|
+
if (!this.pairingAuthorizedBy && owner)
|
|
95
|
+
this.pairingAuthorizedBy = owner;
|
|
96
|
+
return { code: this.pairingCode, expiresAt: this.pairingExpiresAt };
|
|
97
|
+
}
|
|
98
|
+
redeemPairingCode(code, label, authorizedBy, now = Date.now()) {
|
|
99
|
+
if (typeof code !== 'string' || !/^\d{6}$/.test(code)) {
|
|
100
|
+
return { ok: false, code: 'INVALID_ARGUMENT', error: 'pairing code must be six digits' };
|
|
101
|
+
}
|
|
102
|
+
if (!this.pairingCode || now >= this.pairingExpiresAt) {
|
|
103
|
+
return { ok: false, code: 'PAIRING_EXPIRED', error: 'pairing code has expired' };
|
|
104
|
+
}
|
|
105
|
+
if (code !== this.pairingCode) {
|
|
106
|
+
return { ok: false, code: 'PAIRING_INVALID', error: 'pairing code is invalid' };
|
|
107
|
+
}
|
|
108
|
+
const createdAt = now;
|
|
109
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
110
|
+
const store = loadStore();
|
|
111
|
+
pruneExpired(store, now);
|
|
112
|
+
const owner = validOwner(authorizedBy) ?? this.pairingAuthorizedBy;
|
|
113
|
+
store.tokens.push({
|
|
114
|
+
token,
|
|
115
|
+
createdAt,
|
|
116
|
+
lastActive: createdAt,
|
|
117
|
+
label: validLabel(label),
|
|
118
|
+
...(owner ? { authorizedBy: owner } : {}),
|
|
119
|
+
});
|
|
120
|
+
saveStore(store);
|
|
121
|
+
return { ok: true, token, expiresAt: createdAt + REMOTE_ACCESS_TOKEN_TTL_MS };
|
|
122
|
+
}
|
|
123
|
+
issueLocalToken(label, now = Date.now()) {
|
|
124
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
125
|
+
const store = loadStore();
|
|
126
|
+
pruneExpired(store, now);
|
|
127
|
+
store.tokens.push({ token, createdAt: now, lastActive: now, label: validLabel(label) });
|
|
128
|
+
saveStore(store);
|
|
129
|
+
return { ok: true, token, expiresAt: now + REMOTE_ACCESS_TOKEN_TTL_MS };
|
|
130
|
+
}
|
|
131
|
+
validateToken(token, now = Date.now()) {
|
|
132
|
+
return validateAndRenewRemoteAccessToken(token, now);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export const remoteAccessService = new RemoteAccessService();
|
|
@@ -114,7 +114,12 @@ export class CommandHandler {
|
|
|
114
114
|
triggerSchedulerResolver;
|
|
115
115
|
triggerSchedulerFallback;
|
|
116
116
|
triggerSchedulerFallbackAid;
|
|
117
|
+
/** Runtime-only owner for the daemon trigger scheduler. It is intentionally
|
|
118
|
+
* kept outside EvolAgentRegistry because the daemon channel is not an Agent. */
|
|
119
|
+
daemonTriggerOwnerAid;
|
|
120
|
+
daemonTriggerChannel = 'daemon';
|
|
117
121
|
daemonStatusProvider;
|
|
122
|
+
daemonMonitorProvider;
|
|
118
123
|
agentDelegationRegistry;
|
|
119
124
|
handoffRuntime;
|
|
120
125
|
contactRequestSubmitter;
|
|
@@ -219,6 +224,32 @@ export class CommandHandler {
|
|
|
219
224
|
setTriggerSchedulerResolver(resolver) {
|
|
220
225
|
this.triggerSchedulerResolver = resolver;
|
|
221
226
|
}
|
|
227
|
+
/** Bind the virtual daemon channel to its trigger scheduler owner AID. */
|
|
228
|
+
setDaemonTriggerOwner(aid, channel = 'daemon') {
|
|
229
|
+
const normalizedAid = String(aid || '').trim();
|
|
230
|
+
if (!normalizedAid)
|
|
231
|
+
throw new Error('daemon trigger owner AID is required');
|
|
232
|
+
this.daemonTriggerOwnerAid = normalizedAid;
|
|
233
|
+
this.daemonTriggerChannel = String(channel || 'daemon').trim() || 'daemon';
|
|
234
|
+
}
|
|
235
|
+
/** Used by control-menu routing; daemon owner is not in the Agent registry. */
|
|
236
|
+
getDaemonTriggerOwnerAid(channel = 'daemon') {
|
|
237
|
+
if (channel !== this.daemonTriggerChannel)
|
|
238
|
+
return undefined;
|
|
239
|
+
if (this.daemonTriggerOwnerAid)
|
|
240
|
+
return this.daemonTriggerOwnerAid;
|
|
241
|
+
try {
|
|
242
|
+
const controlAid = String(loadDaemonConfig().aid || '__daemon__').trim() || '__daemon__';
|
|
243
|
+
const existing = this.agentRegistry?.get?.(controlAid);
|
|
244
|
+
// Match index.ts: only a runnable (pre-start `stopped`) Agent causes
|
|
245
|
+
// the daemon scheduler owner to receive the collision suffix.
|
|
246
|
+
const conflicts = !!existing && (existing.status === undefined || existing.status === 'stopped');
|
|
247
|
+
return conflicts ? `${controlAid}#daemon` : controlAid;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
222
253
|
/** Compatibility entrypoint for older tests/callers that inject a single scheduler. */
|
|
223
254
|
setTriggerScheduler(scheduler, manager) {
|
|
224
255
|
this.triggerSchedulerFallback = scheduler;
|
|
@@ -233,6 +264,9 @@ export class CommandHandler {
|
|
|
233
264
|
setDaemonStatusProvider(provider) {
|
|
234
265
|
this.daemonStatusProvider = provider;
|
|
235
266
|
}
|
|
267
|
+
setDaemonMonitorProvider(provider) {
|
|
268
|
+
this.daemonMonitorProvider = provider;
|
|
269
|
+
}
|
|
236
270
|
/** 返回管理当前通道的 EvolAgent,无则返回 null */
|
|
237
271
|
getOwningAgent(channel) {
|
|
238
272
|
if (!this.agentRegistry)
|
|
@@ -240,7 +274,9 @@ export class CommandHandler {
|
|
|
240
274
|
return this.agentRegistry.resolveByChannel(channel);
|
|
241
275
|
}
|
|
242
276
|
getTriggerSchedulerForChannel(channel) {
|
|
243
|
-
const agentAid = this.getOwningAgent(channel)?.aid
|
|
277
|
+
const agentAid = this.getOwningAgent(channel)?.aid
|
|
278
|
+
?? this.getDaemonTriggerOwnerAid(channel)
|
|
279
|
+
?? tryParseChannelKey(channel)?.selfAID;
|
|
244
280
|
return agentAid ? this.triggerSchedulerResolver?.(agentAid) : this.triggerSchedulerFallback;
|
|
245
281
|
}
|
|
246
282
|
getTriggerSchedulerForAgent(agentAid) {
|
|
@@ -975,9 +1011,9 @@ export class CommandHandler {
|
|
|
975
1011
|
async execMenuForEcweb(payload, auth) {
|
|
976
1012
|
return await menuExecMenuForEcweb.call(this, payload, auth);
|
|
977
1013
|
}
|
|
978
|
-
/**
|
|
979
|
-
async execMenuForControl(payload, peerId) {
|
|
980
|
-
return await menuExecMenuForControl.call(this, payload, peerId);
|
|
1014
|
+
/** Control AID Menu entry: daemon-owner or validated remote-access token. */
|
|
1015
|
+
async execMenuForControl(payload, peerId, auth) {
|
|
1016
|
+
return await menuExecMenuForControl.call(this, payload, peerId, auth);
|
|
981
1017
|
}
|
|
982
1018
|
/**
|
|
983
1019
|
* CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
|
|
@@ -1344,7 +1380,7 @@ export class CommandHandler {
|
|
|
1344
1380
|
/trigger list [--all] — 查看所有触发器
|
|
1345
1381
|
/trigger create <参数> — 创建触发器(set 为兼容别名)
|
|
1346
1382
|
/trigger update <名称|ID> <参数> — 修改触发器
|
|
1347
|
-
常用参数:--
|
|
1383
|
+
常用参数:--delay/--at/--cron/--every、--prompt、--description、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)、--feedback、--target-channel/--target-channel-id、--trigger-thread、--baseagent、--on-error、--retry-attempts/--retry-backoff、--script-runtime/--script-args/--script-timeout
|
|
1348
1384
|
/trigger enable <名称|ID> — 启用触发器
|
|
1349
1385
|
/trigger disable <名称|ID> — 暂停触发器
|
|
1350
1386
|
/trigger show <名称|ID> — 查看触发器详情
|