evolcore 0.0.10 → 0.0.11
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 +22 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +123 -42
- package/dist/agents/codex-app-server-client.js +33 -9
- package/dist/agents/codex-runner.js +58 -8
- package/dist/agents/ecagent-runner.js +17 -2
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +131 -128
- package/dist/cli/agent-command.js +16 -9
- package/dist/cli/agent.js +82 -19
- package/dist/cli/daemon-commands.js +21 -2
- package/dist/cli/index.js +76 -61
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/gateway-config.js +26 -10
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/operation-authorizer.js +32 -147
- package/dist/core/auth/operation-catalog.js +80 -0
- package/dist/core/bootstrap-messages.js +50 -0
- package/dist/core/bootstrap-service.js +85 -10
- package/dist/core/channel-loader.js +23 -6
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/menu-handler.js +67 -76
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +8 -3
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/message-bridge.js +1 -1
- package/dist/core/message/message-log.js +22 -0
- package/dist/core/message/message-queue.js +19 -4
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +231 -93
- package/dist/core/protected-paths.js +10 -7
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/index.js +262 -84
- package/dist/ipc.js +29 -11
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/rules/01-overview.md +9 -0
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +17 -0
- package/kits/templates/roles/visitor.json +8 -0
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +2 -1
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
package/dist/cli/init.js
CHANGED
|
@@ -12,9 +12,10 @@ 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';
|
|
14
14
|
import { autostartInstalled, autostartPlatformLabel, configureAutostart } from '../utils/autostart.js';
|
|
15
|
+
import { askInit, isInitCancelledError, printInitCancelHint } from './init-cancel.js';
|
|
15
16
|
// ==================== Helpers ====================
|
|
16
17
|
function ask(rl, question) {
|
|
17
|
-
return
|
|
18
|
+
return askInit(rl, question);
|
|
18
19
|
}
|
|
19
20
|
/** 展开用户在交互式输入中使用的 home 目录简写(shell 不会替 readline 自动做这件事)。 */
|
|
20
21
|
export function expandLeadingTildePath(value, homeDir = os.homedir()) {
|
|
@@ -254,6 +255,7 @@ export async function cmdInit(options) {
|
|
|
254
255
|
}
|
|
255
256
|
else {
|
|
256
257
|
// ── 4. 交互式分支(rl 生命周期封装在内部函数,tail 不引用 rl)──
|
|
258
|
+
printInitCancelHint();
|
|
257
259
|
await runInteractive();
|
|
258
260
|
}
|
|
259
261
|
// ── 共享 tail(单一出口):提示创建 agent + 生成控制 AID ──
|
|
@@ -373,6 +375,7 @@ export async function initTail(options = {}) {
|
|
|
373
375
|
const daemonConfig = loadDaemonConfig();
|
|
374
376
|
let controlAidReady = !!daemonConfig.aid;
|
|
375
377
|
let autoStartReady = true;
|
|
378
|
+
let ownerBindingInterrupted = false;
|
|
376
379
|
if (daemonConfig.aid) {
|
|
377
380
|
console.log(`✓ 控制 AID 已存在: ${daemonConfig.aid}`);
|
|
378
381
|
}
|
|
@@ -394,8 +397,10 @@ export async function initTail(options = {}) {
|
|
|
394
397
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
395
398
|
try {
|
|
396
399
|
await handleOwnersPrompt(rl);
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
if (!ownerBindingInterrupted) {
|
|
401
|
+
await handleEcwebPrompt(rl);
|
|
402
|
+
await handleAutostartPrompt(rl);
|
|
403
|
+
}
|
|
399
404
|
}
|
|
400
405
|
finally {
|
|
401
406
|
try {
|
|
@@ -407,6 +412,8 @@ export async function initTail(options = {}) {
|
|
|
407
412
|
else if (options.autoStart !== undefined) {
|
|
408
413
|
autoStartReady = applyAutostart(options.autoStart);
|
|
409
414
|
}
|
|
415
|
+
if (ownerBindingInterrupted)
|
|
416
|
+
return false;
|
|
410
417
|
async function handleAutostartPrompt(rl) {
|
|
411
418
|
if (options.autoStart !== undefined) {
|
|
412
419
|
autoStartReady = applyAutostart(options.autoStart);
|
|
@@ -446,17 +453,22 @@ export async function initTail(options = {}) {
|
|
|
446
453
|
rl.pause();
|
|
447
454
|
try {
|
|
448
455
|
const { runDaemonOwnerQrBindFlow } = await import('./init-channel.js');
|
|
449
|
-
const result = await runDaemonOwnerQrBindFlow(
|
|
450
|
-
if (result
|
|
456
|
+
const result = await runDaemonOwnerQrBindFlow();
|
|
457
|
+
if (result.action === 'bound') {
|
|
451
458
|
console.log(` ✓ 已配置管理者: ${result.boundAid}`);
|
|
452
459
|
}
|
|
453
|
-
else {
|
|
460
|
+
else if (result.action === 'manual') {
|
|
454
461
|
// QR 流程失败,恢复 rl 并提示手动输入
|
|
455
462
|
rl.resume();
|
|
456
463
|
await promptOwnersManually(rl);
|
|
457
464
|
}
|
|
465
|
+
else {
|
|
466
|
+
ownerBindingInterrupted = true;
|
|
467
|
+
}
|
|
458
468
|
}
|
|
459
469
|
catch (e) {
|
|
470
|
+
if (isInitCancelledError(e))
|
|
471
|
+
throw e;
|
|
460
472
|
console.log(` ⚠ 扫码绑定不可用: ${e?.message || e}`);
|
|
461
473
|
rl.resume();
|
|
462
474
|
await promptOwnersManually(rl);
|
|
@@ -561,7 +573,7 @@ export async function selectInstance(rl, channelType, instances, options = {}) {
|
|
|
561
573
|
const validOptions = letters.slice(0, instances.length + 1).split('');
|
|
562
574
|
let choice = '';
|
|
563
575
|
while (!validOptions.includes(choice)) {
|
|
564
|
-
choice = (await
|
|
576
|
+
choice = (await ask(rl, '请选择: ')).trim().toLowerCase();
|
|
565
577
|
if (!validOptions.includes(choice)) {
|
|
566
578
|
console.log(`无效选择,请输入 ${validOptions.join('/')}`);
|
|
567
579
|
}
|
|
@@ -570,7 +582,7 @@ export async function selectInstance(rl, channelType, instances, options = {}) {
|
|
|
570
582
|
if (choiceIndex === instances.length) {
|
|
571
583
|
let name = '';
|
|
572
584
|
while (!name) {
|
|
573
|
-
name = (await
|
|
585
|
+
name = (await ask(rl, '请输入新配置名称: ')).trim();
|
|
574
586
|
if (!name)
|
|
575
587
|
console.log(' 名称不能为空');
|
|
576
588
|
if (instances.some(i => i.name === name)) {
|
|
@@ -583,7 +595,7 @@ export async function selectInstance(rl, channelType, instances, options = {}) {
|
|
|
583
595
|
const target = instances[choiceIndex];
|
|
584
596
|
console.log(`\n已选择:${target.name}`);
|
|
585
597
|
if (options.confirmOverwrite !== false) {
|
|
586
|
-
const confirm = (await
|
|
598
|
+
const confirm = (await ask(rl, `⚠️ 即将覆盖该配置,确认?(y/N) `)).trim().toLowerCase();
|
|
587
599
|
if (confirm !== 'y' && confirm !== 'yes') {
|
|
588
600
|
console.log('已取消');
|
|
589
601
|
return null;
|
|
@@ -35,6 +35,7 @@ export function getManagementCommandPermissions(role) {
|
|
|
35
35
|
'role.revoke': { allow: true, scopes: ['agent'] },
|
|
36
36
|
'role.policy.read': { allow: true, scopes: ['agent'] },
|
|
37
37
|
'role.relation.read': { allow: true, scopes: ['agent'] },
|
|
38
|
+
'observable.current': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
|
|
38
39
|
'config.get': { allow: true, scopes: ['agent', 'relation'] },
|
|
39
40
|
'config.set': { allow: true, scopes: ['agent', 'relation'] },
|
|
40
41
|
'config.unset': { allow: true, scopes: ['agent', 'relation'] },
|
|
@@ -16,9 +16,11 @@ import path from 'path';
|
|
|
16
16
|
import { resolvePaths } from '../paths.js';
|
|
17
17
|
import { saveDefaultsSafe, saveAgent } from '../config-store.js';
|
|
18
18
|
import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../agents/baseagent.js';
|
|
19
|
+
import { buildModelRequestHeaders } from '../agents/request-identity.js';
|
|
19
20
|
import { resolvePriceRow } from '../stats/billing.js';
|
|
20
21
|
import { ipcQuery } from '../ipc.js';
|
|
21
22
|
import { logger } from '../utils/logger.js';
|
|
23
|
+
import { ConfigTarget, read as readConfig } from './config-manager.js';
|
|
22
24
|
/** 已知 baseagent 类型(网关可管理范围)。 */
|
|
23
25
|
const GATEWAY_TYPES = ['claude', 'codex', 'gemini', 'ecagent'];
|
|
24
26
|
const DISPLAY_NAMES = {
|
|
@@ -522,8 +524,10 @@ export async function gatewayDelete(args) {
|
|
|
522
524
|
}
|
|
523
525
|
/** 解析某 scope/type 的真实 baseUrl + apiKey(在 daemon 内展开 $ENV,不出站)。 */
|
|
524
526
|
function resolveGatewayCreds(scope, type) {
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
+
const expandedConfig = scope === 'defaults'
|
|
528
|
+
? readConfig(ConfigTarget.Defaults, undefined, { expand: true, cache: true })
|
|
529
|
+
: readConfig(ConfigTarget.Agent, { self: scope }, { expand: true, cache: true });
|
|
530
|
+
const block = expandedConfig?.baseagents?.[type];
|
|
527
531
|
if (!block)
|
|
528
532
|
return { error: `${scope}/${type} 未配置`, code: 'NOT_FOUND' };
|
|
529
533
|
const expanded = expandEnv(block);
|
|
@@ -531,15 +535,27 @@ function resolveGatewayCreds(scope, type) {
|
|
|
531
535
|
const synth = { agents: { [type]: expanded } };
|
|
532
536
|
if (type === 'codex') {
|
|
533
537
|
const r = resolveOpenaiConfig(synth, expanded);
|
|
534
|
-
return {
|
|
538
|
+
return {
|
|
539
|
+
baseUrl: r.baseUrl,
|
|
540
|
+
apiKey: r.apiKey,
|
|
541
|
+
headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
|
|
542
|
+
};
|
|
535
543
|
}
|
|
536
544
|
else if (type === 'claude') {
|
|
537
545
|
const r = resolveAnthropicConfig(synth, expanded);
|
|
538
|
-
return {
|
|
546
|
+
return {
|
|
547
|
+
baseUrl: r.baseUrl,
|
|
548
|
+
apiKey: r.apiKey,
|
|
549
|
+
headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
|
|
550
|
+
};
|
|
539
551
|
}
|
|
540
552
|
else if (type === 'ecagent') {
|
|
541
553
|
const r = resolveEcagentConfig(synth, expanded);
|
|
542
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
baseUrl: r.baseUrl,
|
|
556
|
+
apiKey: r.apiKey,
|
|
557
|
+
headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: scope === 'defaults' ? undefined : scope, configuredHeaders: r.headers }),
|
|
558
|
+
};
|
|
543
559
|
}
|
|
544
560
|
return { error: 'gemini 暂不支持 HTTP 探测(CLI 后端)', code: 'NOT_SUPPORTED' };
|
|
545
561
|
}
|
|
@@ -556,7 +572,7 @@ export async function gatewayTest(args) {
|
|
|
556
572
|
const creds = resolveGatewayCreds(scope, type);
|
|
557
573
|
if ('error' in creds)
|
|
558
574
|
return creds;
|
|
559
|
-
const { baseUrl, apiKey } = creds;
|
|
575
|
+
const { baseUrl, apiKey, headers } = creds;
|
|
560
576
|
if (!baseUrl)
|
|
561
577
|
return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
|
|
562
578
|
const start = Date.now();
|
|
@@ -566,7 +582,7 @@ export async function gatewayTest(args) {
|
|
|
566
582
|
const base = baseUrl.replace(/\/+$/, '');
|
|
567
583
|
const modelsUrl = base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`;
|
|
568
584
|
const resp = await fetch(modelsUrl, {
|
|
569
|
-
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
585
|
+
headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
|
|
570
586
|
signal: controller.signal,
|
|
571
587
|
});
|
|
572
588
|
clearTimeout(timer);
|
|
@@ -614,7 +630,7 @@ export async function gatewayModels(args) {
|
|
|
614
630
|
const creds = resolveGatewayCreds(scope, type);
|
|
615
631
|
if ('error' in creds)
|
|
616
632
|
return creds;
|
|
617
|
-
const { baseUrl, apiKey } = creds;
|
|
633
|
+
const { baseUrl, apiKey, headers } = creds;
|
|
618
634
|
if (!baseUrl)
|
|
619
635
|
return { error: '未配置 baseUrl(或为官方占位地址)', code: 'INVALID_ARGS' };
|
|
620
636
|
let arr = [];
|
|
@@ -623,7 +639,7 @@ export async function gatewayModels(args) {
|
|
|
623
639
|
const controller = new AbortController();
|
|
624
640
|
const timer = setTimeout(() => controller.abort(), 8000);
|
|
625
641
|
const resp = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
|
|
626
|
-
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
642
|
+
headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
|
|
627
643
|
signal: controller.signal,
|
|
628
644
|
});
|
|
629
645
|
clearTimeout(timer);
|
|
@@ -906,7 +922,7 @@ function expandEnv(obj) {
|
|
|
906
922
|
if (typeof obj === 'string') {
|
|
907
923
|
if (obj.startsWith(ENV_PREFIX))
|
|
908
924
|
return process.env[obj.slice(ENV_PREFIX.length)] ?? '';
|
|
909
|
-
return obj;
|
|
925
|
+
return obj.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
|
|
910
926
|
}
|
|
911
927
|
if (Array.isArray(obj))
|
|
912
928
|
return obj.map(expandEnv);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class AgentReloadBusyError extends Error {
|
|
2
|
+
aid;
|
|
3
|
+
busyCount;
|
|
4
|
+
code = 'AGENT_BUSY';
|
|
5
|
+
constructor(aid, busyCount) {
|
|
6
|
+
super(`Agent "${aid}" has ${busyCount} task(s) in progress`);
|
|
7
|
+
this.aid = aid;
|
|
8
|
+
this.busyCount = busyCount;
|
|
9
|
+
this.name = 'AgentReloadBusyError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class AgentReloadCoordinator {
|
|
13
|
+
deps;
|
|
14
|
+
inFlight = new Map();
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.deps = deps;
|
|
17
|
+
}
|
|
18
|
+
busyCount(aid) {
|
|
19
|
+
return Math.max(0, this.deps.getBusyCount(aid));
|
|
20
|
+
}
|
|
21
|
+
runExclusive(aid, operation, options = {}) {
|
|
22
|
+
const previous = this.inFlight.get(aid) ?? Promise.resolve();
|
|
23
|
+
const pending = previous
|
|
24
|
+
.catch(() => undefined)
|
|
25
|
+
.then(async () => {
|
|
26
|
+
const releaseWorkGate = await this.deps.acquireWorkGate?.(aid);
|
|
27
|
+
try {
|
|
28
|
+
const busyCount = this.busyCount(aid);
|
|
29
|
+
if (!options.force && busyCount > 0)
|
|
30
|
+
throw new AgentReloadBusyError(aid, busyCount);
|
|
31
|
+
if (options.force && busyCount > 0)
|
|
32
|
+
await this.deps.forceReload?.(aid);
|
|
33
|
+
return await operation();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await releaseWorkGate?.();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
this.inFlight.set(aid, pending);
|
|
40
|
+
return pending.finally(() => {
|
|
41
|
+
if (this.inFlight.get(aid) === pending)
|
|
42
|
+
this.inFlight.delete(aid);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
reload(aid, options = {}) {
|
|
46
|
+
return this.runExclusive(aid, async () => {
|
|
47
|
+
if (!this.deps.registry.get(aid))
|
|
48
|
+
throw new Error(`Agent "${aid}" not found`);
|
|
49
|
+
await this.deps.beforeReload?.();
|
|
50
|
+
await this.deps.registry.reload(aid, this.deps.hooks);
|
|
51
|
+
}, options);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -5,68 +5,6 @@ import { isResolvedConfigMutation, } from '../../config/resolved-config-op.js';
|
|
|
5
5
|
import { normalizePeer } from '../model/config-scope.js';
|
|
6
6
|
import { getOperationMeta } from './operation-catalog.js';
|
|
7
7
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
8
|
-
export const USER_PLANE_CAPABILITY_CEILING = {
|
|
9
|
-
allowOperations: new Set([
|
|
10
|
-
'model.list',
|
|
11
|
-
'model.current',
|
|
12
|
-
'model.use',
|
|
13
|
-
'model.effort',
|
|
14
|
-
'model.reset',
|
|
15
|
-
'permission.current',
|
|
16
|
-
'permission.answer',
|
|
17
|
-
'chatmode.current',
|
|
18
|
-
'chatmode.update',
|
|
19
|
-
'mentionmode.current',
|
|
20
|
-
'mentionmode.update',
|
|
21
|
-
'group.mentionmode.current',
|
|
22
|
-
'group.rulespolicy.current',
|
|
23
|
-
'session.list',
|
|
24
|
-
'session.create',
|
|
25
|
-
'session.rename',
|
|
26
|
-
'trigger.list',
|
|
27
|
-
'trigger.show',
|
|
28
|
-
'trigger.history',
|
|
29
|
-
'trigger.eventCatalog',
|
|
30
|
-
'trigger.create',
|
|
31
|
-
'trigger.update',
|
|
32
|
-
'trigger.setEnabled',
|
|
33
|
-
'trigger.cancel',
|
|
34
|
-
'trigger.delete',
|
|
35
|
-
'trigger.run',
|
|
36
|
-
'stats.summary',
|
|
37
|
-
'stats.session',
|
|
38
|
-
'stats.context',
|
|
39
|
-
'file.list',
|
|
40
|
-
'file.fetch',
|
|
41
|
-
'ec.msg.send',
|
|
42
|
-
'ec.msg.file',
|
|
43
|
-
'ec.group.send',
|
|
44
|
-
'ec.group.file',
|
|
45
|
-
'config.get',
|
|
46
|
-
'config.set',
|
|
47
|
-
'config.unset',
|
|
48
|
-
]),
|
|
49
|
-
denyOperations: new Set([
|
|
50
|
-
'role.assign',
|
|
51
|
-
'role.revoke',
|
|
52
|
-
'config.write',
|
|
53
|
-
'gateway.write',
|
|
54
|
-
'cli.exec.raw',
|
|
55
|
-
'shell.exec',
|
|
56
|
-
'rce.exec',
|
|
57
|
-
]),
|
|
58
|
-
denyNamespaces: new Set([
|
|
59
|
-
'role',
|
|
60
|
-
'connect',
|
|
61
|
-
'contact',
|
|
62
|
-
'agent',
|
|
63
|
-
'system',
|
|
64
|
-
'storage',
|
|
65
|
-
'aid',
|
|
66
|
-
]),
|
|
67
|
-
allowCategories: new Set(['read', 'write-own']),
|
|
68
|
-
denyCategories: new Set(['write-agent', 'process', 'dangerous']),
|
|
69
|
-
};
|
|
70
8
|
export function authorizeCommand(ctx) {
|
|
71
9
|
return authorizeCommandInternal(ctx);
|
|
72
10
|
}
|
|
@@ -84,68 +22,33 @@ export function authorizeResolvedConfigCommand(command, context) {
|
|
|
84
22
|
* enforced here.
|
|
85
23
|
*/
|
|
86
24
|
export function evaluateRoleOperationCapability(params) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
const roleDef = getRoleDefinition(params.role, params.selfAid);
|
|
96
|
-
if (!roleDef || roleDef.allowAccess === false) {
|
|
97
|
-
return {
|
|
98
|
-
allow: false,
|
|
99
|
-
operation: params.operation,
|
|
100
|
-
reason: `Role ${params.role} cannot access commands`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
const matched = matchCommandPermission(params.operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
|
|
104
|
-
if (!matched) {
|
|
105
|
-
return {
|
|
106
|
-
allow: false,
|
|
107
|
-
operation: params.operation,
|
|
108
|
-
reason: `Role ${params.role} has no permission for ${params.operation}`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const { permission, matchedRule } = matched;
|
|
112
|
-
if (!permission.allow) {
|
|
113
|
-
return {
|
|
114
|
-
allow: false,
|
|
25
|
+
// Capability discovery has no concrete relation target. Use a synthetic
|
|
26
|
+
// self relation so target/current-relation constraints are evaluated as
|
|
27
|
+
// role capabilities without weakening the real authorization path.
|
|
28
|
+
const capabilitySelf = params.selfAid ?? 'capability.agentid.pub';
|
|
29
|
+
const capabilityPeer = formatPeerKey('aun', capabilitySelf);
|
|
30
|
+
const decision = authorizeCommand({
|
|
31
|
+
intent: {
|
|
115
32
|
operation: params.operation,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (constraints?.requireAgentOwner && params.role !== 'owner') {
|
|
137
|
-
return { allow: false, operation: params.operation, reason: 'Operation requires Agent owner', matchedRule };
|
|
138
|
-
}
|
|
139
|
-
if (constraints?.requireAgentAdmin && params.role !== 'owner' && params.role !== 'admin') {
|
|
140
|
-
return { allow: false, operation: params.operation, reason: 'Operation requires Agent admin', matchedRule };
|
|
141
|
-
}
|
|
142
|
-
if (constraints?.requireControlChannel && !params.fromControlChannel) {
|
|
143
|
-
return { allow: false, operation: params.operation, reason: 'Operation requires the control channel', matchedRule };
|
|
144
|
-
}
|
|
145
|
-
if (opMeta.dangerous && !isExplicitDangerousGrant(matchedRule, permission)) {
|
|
146
|
-
return { allow: false, operation: params.operation, reason: 'Dangerous operation is not explicitly granted', matchedRule };
|
|
147
|
-
}
|
|
148
|
-
return { allow: true, operation: params.operation, matchedRule };
|
|
33
|
+
scope: params.scope,
|
|
34
|
+
source: 'menu',
|
|
35
|
+
args: {
|
|
36
|
+
self: capabilitySelf,
|
|
37
|
+
peer: capabilityPeer,
|
|
38
|
+
peerKey: capabilityPeer,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
selfAid: capabilitySelf,
|
|
42
|
+
peerKey: capabilityPeer,
|
|
43
|
+
role: params.role,
|
|
44
|
+
chatType: params.chatType,
|
|
45
|
+
isDaemonOwner: params.fromControlChannel && params.role === 'owner',
|
|
46
|
+
fromControlChannel: params.fromControlChannel,
|
|
47
|
+
source: 'menu',
|
|
48
|
+
});
|
|
49
|
+
return decision.allow
|
|
50
|
+
? { allow: true, operation: params.operation, matchedRule: decision.matchedRule }
|
|
51
|
+
: { allow: false, operation: params.operation, reason: decision.reason, matchedRule: decision.matchedRule };
|
|
149
52
|
}
|
|
150
53
|
function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
151
54
|
const { intent, role } = ctx;
|
|
@@ -208,11 +111,6 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
208
111
|
if (roleDef.allowAccess === false) {
|
|
209
112
|
return denyDecision(ctx, 'ROLE_ACCESS_DENIED', `Role ${role} is not allowed to access commands`, operation, intent.scope, opMeta.dangerous);
|
|
210
113
|
}
|
|
211
|
-
if (!isManagementRole(role)
|
|
212
|
-
&& !resolvedConfigCommand
|
|
213
|
-
&& !isUserPlaneOperationAllowed(operation, opMeta.category, opMeta.dangerous)) {
|
|
214
|
-
return denyDecision(ctx, 'NOT_ALLOWED', `Operation ${operation} is outside the user permission plane`, operation, intent.scope, opMeta.dangerous);
|
|
215
|
-
}
|
|
216
114
|
const matchResult = matchCommandPermission(operation, opMeta.category, roleDef.commandPermissions || {}, opMeta.dangerous);
|
|
217
115
|
if (!matchResult) {
|
|
218
116
|
return denyDecision(ctx, 'NO_PERMISSION', `Role ${role} has no permission for ${operation}`, operation, intent.scope, opMeta.dangerous);
|
|
@@ -261,7 +159,7 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
|
|
|
261
159
|
if (permission.constraints) {
|
|
262
160
|
const constraintCheck = checkConstraints(ctx, permission.constraints, resolvedConfigCommand);
|
|
263
161
|
if (!constraintCheck.ok) {
|
|
264
|
-
return denyDecision(ctx, 'ARGUMENT_MISMATCH', constraintCheck.reason || 'Command arguments do not satisfy permission constraints', operation, intent.scope, opMeta.dangerous, matchedRule);
|
|
162
|
+
return denyDecision(ctx, constraintCheck.code ?? 'ARGUMENT_MISMATCH', constraintCheck.reason || 'Command arguments do not satisfy permission constraints', operation, intent.scope, opMeta.dangerous, matchedRule);
|
|
265
163
|
}
|
|
266
164
|
}
|
|
267
165
|
return {
|
|
@@ -305,20 +203,6 @@ function getRuleRank(rule, permission, operation, namespace, category, isDangero
|
|
|
305
203
|
function isExplicitDangerousGrant(rule, permission) {
|
|
306
204
|
return permission.allow === true && permission.dangerous === true && (rule === 'dangerous:*' || !rule.includes('*') && !rule.startsWith('category:'));
|
|
307
205
|
}
|
|
308
|
-
function isUserPlaneOperationAllowed(operation, category, dangerous) {
|
|
309
|
-
const namespace = operation.split('.')[0];
|
|
310
|
-
if (dangerous)
|
|
311
|
-
return false;
|
|
312
|
-
if (USER_PLANE_CAPABILITY_CEILING.denyCategories.has(category))
|
|
313
|
-
return false;
|
|
314
|
-
if (!USER_PLANE_CAPABILITY_CEILING.allowCategories.has(category))
|
|
315
|
-
return false;
|
|
316
|
-
if (USER_PLANE_CAPABILITY_CEILING.denyOperations.has(operation))
|
|
317
|
-
return false;
|
|
318
|
-
if (USER_PLANE_CAPABILITY_CEILING.denyNamespaces.has(namespace))
|
|
319
|
-
return false;
|
|
320
|
-
return USER_PLANE_CAPABILITY_CEILING.allowOperations.has(operation);
|
|
321
|
-
}
|
|
322
206
|
function checkConstraints(ctx, constraints, resolvedConfigCommand) {
|
|
323
207
|
if (constraints.ownPeerOnly) {
|
|
324
208
|
const peerCheck = checkOwnPeer(ctx);
|
|
@@ -326,16 +210,17 @@ function checkConstraints(ctx, constraints, resolvedConfigCommand) {
|
|
|
326
210
|
return peerCheck;
|
|
327
211
|
}
|
|
328
212
|
if (constraints.ownAgentOnly || constraints.targetCurrentAgentOnly) {
|
|
329
|
-
const
|
|
330
|
-
|
|
213
|
+
const controlOwnerOverride = ctx.fromControlChannel && ctx.isDaemonOwner;
|
|
214
|
+
const targetAid = stringArg(ctx.intent.args.self) ?? stringArg(ctx.intent.args.aid);
|
|
215
|
+
if (!controlOwnerOverride && (!ctx.selfAid || !targetAid || targetAid !== ctx.selfAid)) {
|
|
331
216
|
return { ok: false, reason: 'Only the current agent can be targeted' };
|
|
332
217
|
}
|
|
333
218
|
}
|
|
334
219
|
if (constraints.requireAgentOwner && ctx.role !== 'owner') {
|
|
335
|
-
return { ok: false, reason: 'This command requires agent owner permission' };
|
|
220
|
+
return { ok: false, code: 'NO_PERMISSION', reason: 'This command requires agent owner permission' };
|
|
336
221
|
}
|
|
337
222
|
if (constraints.requireAgentAdmin && ctx.role !== 'owner' && ctx.role !== 'admin') {
|
|
338
|
-
return { ok: false, reason: 'This command requires agent admin permission' };
|
|
223
|
+
return { ok: false, code: 'NO_PERMISSION', reason: 'This command requires agent admin permission' };
|
|
339
224
|
}
|
|
340
225
|
if (constraints.privateOnly && ctx.chatType !== 'private') {
|
|
341
226
|
return { ok: false, reason: 'This command is only allowed in private chats' };
|
|
@@ -44,6 +44,14 @@ const OPERATIONS = [
|
|
|
44
44
|
description: '查询当前使用的模型',
|
|
45
45
|
sources: ['slash', 'menu', 'menu.cli'],
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: 'model.effort.current',
|
|
49
|
+
category: 'read',
|
|
50
|
+
dangerous: false,
|
|
51
|
+
defaultScopes: ['relation', 'role', 'agent'],
|
|
52
|
+
description: '查询当前推理努力度',
|
|
53
|
+
sources: ['slash', 'menu', 'menu.cli'],
|
|
54
|
+
},
|
|
47
55
|
{
|
|
48
56
|
id: 'model.info',
|
|
49
57
|
category: 'read',
|
|
@@ -181,6 +189,30 @@ const OPERATIONS = [
|
|
|
181
189
|
description: '列出会话',
|
|
182
190
|
sources: ['slash', 'menu'],
|
|
183
191
|
},
|
|
192
|
+
{
|
|
193
|
+
id: 'session.current',
|
|
194
|
+
category: 'read',
|
|
195
|
+
dangerous: false,
|
|
196
|
+
defaultScopes: ['relation'],
|
|
197
|
+
description: '查看当前会话状态',
|
|
198
|
+
sources: ['slash', 'menu'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'session.topic.current',
|
|
202
|
+
category: 'read',
|
|
203
|
+
dangerous: false,
|
|
204
|
+
defaultScopes: ['relation'],
|
|
205
|
+
description: '查看当前关系下的话题会话状态',
|
|
206
|
+
sources: ['menu'],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'session.topic.list',
|
|
210
|
+
category: 'read',
|
|
211
|
+
dangerous: false,
|
|
212
|
+
defaultScopes: ['relation'],
|
|
213
|
+
description: '列出当前关系下的话题会话',
|
|
214
|
+
sources: ['menu'],
|
|
215
|
+
},
|
|
184
216
|
{
|
|
185
217
|
id: 'session.create',
|
|
186
218
|
category: 'write-own',
|
|
@@ -206,6 +238,14 @@ const OPERATIONS = [
|
|
|
206
238
|
sources: ['slash', 'menu'],
|
|
207
239
|
},
|
|
208
240
|
// ── File Operations ──
|
|
241
|
+
{
|
|
242
|
+
id: 'project.current',
|
|
243
|
+
category: 'read',
|
|
244
|
+
dangerous: false,
|
|
245
|
+
defaultScopes: ['relation', 'agent'],
|
|
246
|
+
description: '查看当前项目摘要',
|
|
247
|
+
sources: ['menu'],
|
|
248
|
+
},
|
|
209
249
|
{
|
|
210
250
|
id: 'file.list',
|
|
211
251
|
category: 'read',
|
|
@@ -425,6 +465,22 @@ const OPERATIONS = [
|
|
|
425
465
|
description: '查看 agent 详情(敏感信息)',
|
|
426
466
|
sources: ['menu', 'menu.cli', 'control'],
|
|
427
467
|
},
|
|
468
|
+
{
|
|
469
|
+
id: 'agent.baseagent.current',
|
|
470
|
+
category: 'read',
|
|
471
|
+
dangerous: false,
|
|
472
|
+
defaultScopes: ['relation', 'agent', 'control'],
|
|
473
|
+
description: '查看当前 Agent 使用的 BaseAgent',
|
|
474
|
+
sources: ['menu', 'control'],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
id: 'agent.baseagent.list',
|
|
478
|
+
category: 'read',
|
|
479
|
+
dangerous: false,
|
|
480
|
+
defaultScopes: ['agent', 'control'],
|
|
481
|
+
description: '列出当前 Agent 可用的 BaseAgent',
|
|
482
|
+
sources: ['menu', 'control'],
|
|
483
|
+
},
|
|
428
484
|
{
|
|
429
485
|
id: 'agent.getConfig',
|
|
430
486
|
category: 'read',
|
|
@@ -466,6 +522,30 @@ const OPERATIONS = [
|
|
|
466
522
|
description: '查看系统状态(敏感信息)',
|
|
467
523
|
sources: ['slash', 'menu', 'menu.cli'],
|
|
468
524
|
},
|
|
525
|
+
{
|
|
526
|
+
id: 'activity.current',
|
|
527
|
+
category: 'read',
|
|
528
|
+
dangerous: false,
|
|
529
|
+
defaultScopes: ['relation', 'agent'],
|
|
530
|
+
description: '查看当前中间活动显示策略',
|
|
531
|
+
sources: ['menu'],
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: 'observable.current',
|
|
535
|
+
category: 'read',
|
|
536
|
+
dangerous: false,
|
|
537
|
+
defaultScopes: ['agent'],
|
|
538
|
+
description: '查看当前 Agent 观察者模式',
|
|
539
|
+
sources: ['menu', 'control'],
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: 'capability.read',
|
|
543
|
+
category: 'read',
|
|
544
|
+
dangerous: false,
|
|
545
|
+
defaultScopes: ['agent'],
|
|
546
|
+
description: '查看当前 Agent 的能力策略摘要',
|
|
547
|
+
sources: ['menu', 'control'],
|
|
548
|
+
},
|
|
469
549
|
{
|
|
470
550
|
id: 'system.restart',
|
|
471
551
|
category: 'process',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { agentMdPath, resolvePaths } from '../paths.js';
|
|
3
|
+
import { parseAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
4
|
+
import { generateWelcomeMessage } from '../utils/welcome.js';
|
|
5
|
+
import * as outbox from '../aun/outbox.js';
|
|
6
|
+
import { chatDirPath } from './session/session-fs-store.js';
|
|
7
|
+
import { hasMessageLogOperation } from './message/message-log.js';
|
|
8
|
+
export const BOOTSTRAP_MESSAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
9
|
+
export function bootstrapInitialMessageOperationId(aid) {
|
|
10
|
+
return `bootstrap-initial:v1:${aid}`;
|
|
11
|
+
}
|
|
12
|
+
export function postBootstrapWelcomeOperationId(aid) {
|
|
13
|
+
return `bootstrap-complete:v1:${aid}`;
|
|
14
|
+
}
|
|
15
|
+
export function renderPostBootstrapWelcome(aid, owner, ownerName) {
|
|
16
|
+
const mdPath = agentMdPath(aid);
|
|
17
|
+
const agentMd = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
|
|
18
|
+
const agentName = parseAgentDisplayName(agentMd)
|
|
19
|
+
|| resolveAgentDisplayName(aid)
|
|
20
|
+
|| aid.split('.')[0];
|
|
21
|
+
const fallbackOwnerName = owner.replace(/^@/, '').split('.')[0];
|
|
22
|
+
return generateWelcomeMessage({
|
|
23
|
+
channelType: 'aun',
|
|
24
|
+
agentName,
|
|
25
|
+
ownerName: ownerName || fallbackOwnerName,
|
|
26
|
+
includeBindingNote: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function preparePostBootstrapWelcomeOutbox(aid, owner, ownerName) {
|
|
30
|
+
const operationId = postBootstrapWelcomeOperationId(aid);
|
|
31
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
32
|
+
if (hasMessageLogOperation(chatDir, operationId))
|
|
33
|
+
return;
|
|
34
|
+
outbox.enqueue(aid, {
|
|
35
|
+
channelId: owner,
|
|
36
|
+
dedupeKey: operationId,
|
|
37
|
+
critical: true,
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: renderPostBootstrapWelcome(aid, owner, ownerName),
|
|
40
|
+
ttl: BOOTSTRAP_MESSAGE_TTL_MS,
|
|
41
|
+
context: {
|
|
42
|
+
metadata: {
|
|
43
|
+
source: 'daemon',
|
|
44
|
+
chatmode: 'interactive',
|
|
45
|
+
persistRequired: true,
|
|
46
|
+
operationId,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|