evolcore 0.0.9 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/contact-book-store.js +1 -1
  20. package/dist/config/gateway-config.js +26 -10
  21. package/dist/core/agent-reload-coordinator.js +53 -0
  22. package/dist/core/auth/operation-authorizer.js +32 -147
  23. package/dist/core/auth/operation-catalog.js +80 -0
  24. package/dist/core/bootstrap-messages.js +50 -0
  25. package/dist/core/bootstrap-service.js +85 -10
  26. package/dist/core/channel-loader.js +23 -6
  27. package/dist/core/command/agent-control.js +14 -11
  28. package/dist/core/command/menu-handler.js +67 -76
  29. package/dist/core/command/slash-handler.js +4 -4
  30. package/dist/core/data-migration.js +79 -27
  31. package/dist/core/evolagent-registry.js +125 -35
  32. package/dist/core/evolagent.js +8 -3
  33. package/dist/core/inference/text-inference.js +38 -4
  34. package/dist/core/message/message-bridge.js +1 -1
  35. package/dist/core/message/message-log.js +22 -0
  36. package/dist/core/message/message-queue.js +19 -4
  37. package/dist/core/model/model-catalog.js +143 -24
  38. package/dist/core/model/model-diagnostics.js +28 -10
  39. package/dist/core/permission/index.js +1 -0
  40. package/dist/core/permission/readonly-shell-query.js +532 -0
  41. package/dist/core/permission/shell-environment.js +46 -0
  42. package/dist/core/permission/tool-policy.js +231 -93
  43. package/dist/core/protected-paths.js +10 -7
  44. package/dist/core/runner-reload-transaction.js +57 -0
  45. package/dist/index.js +262 -84
  46. package/dist/ipc.js +29 -11
  47. package/dist/utils/aid-bind.js +3 -8
  48. package/dist/utils/log-writer.js +6 -10
  49. package/dist/utils/logger.js +5 -5
  50. package/kits/docs/evolcore/msg.md +13 -0
  51. package/kits/rules/01-overview.md +9 -0
  52. package/kits/schemas/agent-config.schema.3.json +1 -1
  53. package/kits/schemas/agent-config.schema.4.json +1 -1
  54. package/kits/schemas/relation-config.schema.2.json +1 -1
  55. package/kits/schemas/role-config.schema.1.json +1 -1
  56. package/kits/templates/roles/admin.json +5 -0
  57. package/kits/templates/roles/member.json +17 -0
  58. package/kits/templates/roles/visitor.json +8 -0
  59. package/kits/templates/system-fragments/bootstrap.md +12 -6
  60. package/kits/templates/system-fragments/channel.md +6 -0
  61. package/kits/templates/system-fragments/session.md +2 -0
  62. package/package.json +2 -1
  63. package/skills/eclink/SKILL.md +15 -3
  64. 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 new Promise(resolve => rl.question(question, resolve));
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
- await handleEcwebPrompt(rl);
398
- await handleAutostartPrompt(rl);
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('append');
450
- if (result?.boundAid) {
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 new Promise(r => rl.question('请选择: ', r))).trim().toLowerCase();
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 new Promise(r => rl.question('请输入新配置名称: ', r))).trim();
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 new Promise(r => rl.question(`⚠️ 即将覆盖该配置,确认?(y/N) `, r))).trim().toLowerCase();
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'] },
@@ -566,7 +566,7 @@ function journalFile(selfAid) {
566
566
  return path.join(agentDir(selfAid), JOURNAL_NAME);
567
567
  }
568
568
  function appendAudit(selfAid, request, previousContactRevision, contactRevisionValue) {
569
- const file = path.join(resolvePaths().dataDir, 'contact-book-audit.jsonl');
569
+ const file = path.join(agentDir(selfAid), 'data', 'contact-audit.jsonl');
570
570
  fs.mkdirSync(path.dirname(file), { recursive: true });
571
571
  fs.appendFileSync(file, `${JSON.stringify({
572
572
  timestamp: new Date().toISOString(),
@@ -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 raw = scope === 'defaults' ? readDefaultsRaw() : readAgentRaw(scope);
526
- const block = raw?.baseagents?.[type];
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 { baseUrl: r.baseUrl, apiKey: r.apiKey };
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 { baseUrl: r.baseUrl, apiKey: r.apiKey };
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 { baseUrl: r.baseUrl, apiKey: r.apiKey };
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
- const opMeta = getOperationMeta(params.operation);
88
- if (!opMeta) {
89
- return {
90
- allow: false,
91
- operation: params.operation,
92
- reason: `Unknown operation: ${params.operation}`,
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
- reason: permission.reason || `Role ${params.role} explicitly denies ${params.operation}`,
117
- matchedRule,
118
- };
119
- }
120
- const scopes = permission.scopes || opMeta.defaultScopes;
121
- if (!scopes.includes(params.scope)) {
122
- return {
123
- allow: false,
124
- operation: params.operation,
125
- reason: `Scope ${params.scope} is not allowed for ${params.operation}`,
126
- matchedRule,
127
- };
128
- }
129
- const constraints = permission.constraints;
130
- if (constraints?.privateOnly && params.chatType !== 'private') {
131
- return { allow: false, operation: params.operation, reason: 'Operation is private-chat only', matchedRule };
132
- }
133
- if (constraints?.groupOnly && params.chatType !== 'group') {
134
- return { allow: false, operation: params.operation, reason: 'Operation is group-chat only', matchedRule };
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 argSelf = stringArg(ctx.intent.args.self);
330
- if (argSelf && argSelf !== ctx.selfAid) {
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
+ }