evolcore 0.0.38 → 0.0.40

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 (125) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +31 -20
  3. package/bin/ec-safe-output.js +14 -2
  4. package/dist/agents/baseagent-availability.js +1 -0
  5. package/dist/agents/claude-runner.js +32 -46
  6. package/dist/agents/codex-runner.js +54 -30
  7. package/dist/agents/ecagent-runner.js +18 -7
  8. package/dist/agents/gemini-runner.js +1 -2
  9. package/dist/agents/runner-types.js +28 -6
  10. package/dist/aun/aid/agentmd.js +21 -0
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +23 -98
  13. package/dist/aun/aid/ownership-operation.js +35 -12
  14. package/dist/aun/group/invite-service.js +6 -0
  15. package/dist/aun/msg/p2p.js +8 -12
  16. package/dist/channels/aun.js +66 -48
  17. package/dist/channels/dingtalk.js +8 -2
  18. package/dist/channels/feishu.js +6 -1
  19. package/dist/channels/wecom.js +7 -3
  20. package/dist/cli/agent-create.js +30 -6
  21. package/dist/cli/agent.js +30 -37
  22. package/dist/cli/aid-bind-command.js +16 -2
  23. package/dist/cli/aun-commands.js +31 -23
  24. package/dist/cli/config.js +15 -18
  25. package/dist/cli/contact.js +9 -12
  26. package/dist/cli/ctl-command.js +3 -3
  27. package/dist/cli/daemon-commands.js +242 -17
  28. package/dist/cli/diag.js +7 -3
  29. package/dist/cli/fs-command.js +5 -4
  30. package/dist/cli/handoff-command.js +11 -11
  31. package/dist/cli/help.js +18 -0
  32. package/dist/cli/index.js +25 -13
  33. package/dist/cli/init-channel.js +2 -1
  34. package/dist/cli/memory-command.js +47 -27
  35. package/dist/cli/model.js +5 -4
  36. package/dist/cli/queue-command.js +3 -3
  37. package/dist/cli/response.js +5 -4
  38. package/dist/cli/restart-monitor.js +35 -13
  39. package/dist/cli/skill-command.js +13 -13
  40. package/dist/cli/task-command.js +3 -3
  41. package/dist/cli/task-context.js +1 -0
  42. package/dist/cli/thread-command.js +5 -3
  43. package/dist/cli/trigger-command.js +3 -3
  44. package/dist/cli/version.js +6 -2
  45. package/dist/config/config-field-policy.js +1 -0
  46. package/dist/contracts/ecweb-service-protocol.js +44 -0
  47. package/dist/core/agent-runtime.js +6 -0
  48. package/dist/core/auth/agent-delegation.js +7 -4
  49. package/dist/core/auth/auth-gateway.js +3 -1
  50. package/dist/core/auth/authorization-audit.js +5 -1
  51. package/dist/core/auth/debug-context.js +5 -1
  52. package/dist/core/auth/operation-catalog.js +17 -1
  53. package/dist/core/bootstrap-service.js +20 -12
  54. package/dist/core/command/cli-intent-parser.js +5 -2
  55. package/dist/core/command/command-handler.js +20 -5
  56. package/dist/core/command/menu-catalog.js +48 -12
  57. package/dist/core/command/menu-handler.js +74 -6
  58. package/dist/core/command/menu-protocol.js +20 -1
  59. package/dist/core/command/slash-gate.js +17 -3
  60. package/dist/core/command/slash-handler.js +90 -7
  61. package/dist/core/event-catalog.js +2 -0
  62. package/dist/core/interaction-router.js +2 -1
  63. package/dist/core/ipc-registration.js +26 -7
  64. package/dist/core/memory/batch-meta.js +2 -0
  65. package/dist/core/memory/consolidation-context.js +42 -51
  66. package/dist/core/memory/consolidation-ops-records.js +39 -7
  67. package/dist/core/memory/consolidation-trigger.js +71 -64
  68. package/dist/core/memory/consolidation.js +226 -20
  69. package/dist/core/memory/memory-writer.js +160 -34
  70. package/dist/core/memory/relation-idle-monitor.js +287 -0
  71. package/dist/core/memory/relation-sweep.js +206 -0
  72. package/dist/core/message/aun-ipc-sender.js +5 -0
  73. package/dist/core/message/message-bridge.js +64 -55
  74. package/dist/core/message/message-log.js +17 -0
  75. package/dist/core/message/message-queue.js +6 -0
  76. package/dist/core/message/outbound.js +70 -0
  77. package/dist/core/message/peer-mode.js +7 -2
  78. package/dist/core/message/response-engine.js +341 -73
  79. package/dist/core/ownership/owner-resolver.js +465 -140
  80. package/dist/core/permission/ec-command-parser.js +19 -3
  81. package/dist/core/permission/protected-paths.js +51 -0
  82. package/dist/core/permission/tool-policy.js +7 -7
  83. package/dist/core/role/runtime-policy.js +64 -0
  84. package/dist/core/session/session-manager.js +25 -5
  85. package/dist/core/system-session/memory.js +3 -18
  86. package/dist/ecweb/basic-settings-service.js +709 -0
  87. package/dist/ecweb/providers-service.js +172 -0
  88. package/dist/index.js +92 -4
  89. package/dist/ipc.js +78 -11
  90. package/dist/stats/db.js +33 -0
  91. package/dist/stats/index.js +2 -2
  92. package/dist/stats/query.js +246 -0
  93. package/dist/stats/writer.js +41 -1
  94. package/dist/trigger/application-service.js +12 -0
  95. package/dist/trigger/authorization.js +2 -2
  96. package/dist/trigger/history.js +36 -0
  97. package/dist/trigger/scheduler.js +3 -0
  98. package/dist/utils/ecweb-supervisor.js +31 -10
  99. package/dist/utils/error-utils.js +8 -4
  100. package/dist/utils/evolcore-version.js +3 -2
  101. package/dist/utils/host-info.js +52 -0
  102. package/dist/utils/welcome.js +18 -24
  103. package/ecagent/dist/harness/agent-harness.js +4 -1
  104. package/kits/docs/evolcore/INDEX.md +1 -1
  105. package/kits/docs/evolcore/agent.md +1 -1
  106. package/kits/docs/evolcore/aid.md +58 -8
  107. package/kits/docs/evolcore/config.md +9 -7
  108. package/kits/docs/evolcore/memory-architecture.md +3 -4
  109. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  110. package/kits/docs/evolcore/memory-tags.md +1 -1
  111. package/kits/docs/evolcore/memory.md +1 -1
  112. package/kits/docs/evolcore/menu-client.md +1 -1
  113. package/kits/docs/evolcore/menu-protocol.md +6 -0
  114. package/kits/docs/evolcore/msg.md +1 -1
  115. package/kits/eck_manifest.json +3 -3
  116. package/kits/rules/04-relation.md +3 -2
  117. package/kits/schemas/agent-config.schema.13.json +5 -1
  118. package/kits/templates/memory-consolidation/generalize.md +32 -4
  119. package/kits/templates/memory-consolidation/notes.md +2 -2
  120. package/kits/templates/memory-consolidation/promote.md +10 -2
  121. package/kits/templates/system-fragments/baseagent.md +5 -0
  122. package/kits/templates/system-fragments/memory.md +12 -3
  123. package/kits/templates/system-fragments/session.md +1 -1
  124. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  125. package/package.json +3 -3
@@ -22,7 +22,7 @@ import path from 'path';
22
22
  import { spawn } from 'child_process';
23
23
  import { getPackageRoot, agentDir, resolvePaths } from '../paths.js';
24
24
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
25
- import { ipcQuery } from '../ipc.js';
25
+ import { daemonUnavailableMessage, queryDaemon } from './help.js';
26
26
  import { readDelegationCredential } from '../core/auth/agent-delegation.js';
27
27
  import { authorizeLocalCommand, TASK_RUNTIME_CONTEXT_ENV } from './task-context.js';
28
28
  import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
@@ -87,17 +87,17 @@ async function cmdSkillRefresh(args) {
87
87
  process.exitCode = 1;
88
88
  return;
89
89
  }
90
- let response;
91
- try {
92
- response = await ipcQuery(resolvePaths().socket, {
93
- type: 'skill.refresh',
94
- aid,
95
- argv,
96
- ...(sessionId ? { sessionId, delegationToken } : {}),
97
- }, 30_000, { throwOnError: true });
98
- }
99
- catch (e) {
100
- console.error(`❌ 刷新失败 (DAEMON_UNAVAILABLE): ${e instanceof Error ? e.message : String(e)}`);
90
+ const { response, transportError } = await queryDaemon(resolvePaths().socket, {
91
+ type: 'skill.refresh',
92
+ aid,
93
+ argv,
94
+ ...(sessionId ? { sessionId, delegationToken } : {}),
95
+ }, 30_000);
96
+ if (transportError) {
97
+ if (asJson)
98
+ console.log(JSON.stringify({ ok: false, code: 'DAEMON_UNAVAILABLE', error: transportError.message }, null, 2));
99
+ else
100
+ console.error(`❌ 刷新失败 (DAEMON_UNAVAILABLE): ${transportError.message}`);
101
101
  process.exitCode = 1;
102
102
  return;
103
103
  }
@@ -109,7 +109,7 @@ async function cmdSkillRefresh(args) {
109
109
  }
110
110
  if (!response?.ok) {
111
111
  const code = response?.code ?? (response ? 'FAILED' : 'DAEMON_UNAVAILABLE');
112
- console.error(`❌ 刷新失败 (${code}): ${response?.error ?? '无法连接 evolcore daemon'}`);
112
+ console.error(`❌ 刷新失败 (${code}): ${response?.error ?? daemonUnavailableMessage(transportError, '无法连接 evolcore daemon')}`);
113
113
  if (response?.data?.nextStep)
114
114
  console.error(` ${response.data.nextStep}`);
115
115
  process.exitCode = 1;
@@ -1,5 +1,5 @@
1
1
  import { resolvePaths } from '../paths.js';
2
- import { ipcQuery } from '../ipc.js';
2
+ import { daemonUnavailableMessage, queryDaemon } from './help.js';
3
3
  import { readDelegationCredential } from '../core/auth/agent-delegation.js';
4
4
  import { wantsHelp } from './help.js';
5
5
  const TASK_HELP = `用法: ec task resume <sessionId> [--format json]
@@ -39,14 +39,14 @@ export async function cmdTask(args) {
39
39
  printError('DELEGATION_REQUIRED', '托管任务缺少 delegation token');
40
40
  return;
41
41
  }
42
- const result = await ipcQuery(resolvePaths().socket, {
42
+ const { response: result, transportError } = await queryDaemon(resolvePaths().socket, {
43
43
  type: 'task.resume',
44
44
  argv: ['task', ...args],
45
45
  targetSessionId,
46
46
  ...(sessionId ? { sessionId, delegationToken } : {}),
47
47
  }, 10_000);
48
48
  if (!result) {
49
- printError('IPC_UNAVAILABLE', '无法连接 EvolCore 服务;请查询任务状态后再决定是否重试');
49
+ printError('IPC_UNAVAILABLE', daemonUnavailableMessage(transportError, '无法连接 EvolCore 服务;请查询任务状态后再决定是否重试'));
50
50
  return;
51
51
  }
52
52
  if (json)
@@ -47,6 +47,7 @@ function normalizeTaskRuntimeContext(value) {
47
47
  executionSource: value.executionSource === 'fullaccess-command'
48
48
  || value.executionSource === 'trigger'
49
49
  || value.executionSource === 'agent-policy'
50
+ || value.executionSource === 'session-policy'
50
51
  ? value.executionSource : undefined,
51
52
  daemonRuntimeEpoch: optionalString(value.daemonRuntimeEpoch),
52
53
  threadId: optionalString(value.threadId),
@@ -1,5 +1,5 @@
1
1
  import { resolvePaths } from '../paths.js';
2
- import { ipcQuery } from '../ipc.js';
2
+ import { daemonUnavailableMessage, queryDaemon } from './help.js';
3
3
  import { readDelegationCredential } from '../core/auth/agent-delegation.js';
4
4
  import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
5
5
  import { isMainSessionThreadId, resolveThreadRef } from '../core/thread/topic-id.js';
@@ -91,7 +91,7 @@ async function sendThreadOp(req, commandArgv) {
91
91
  const cred = readDelegationCredential();
92
92
  const { socket } = resolvePaths();
93
93
  const invokingSessionId = process.env.EVOLCORE_SESSION_ID;
94
- const result = await ipcQuery(socket, {
94
+ const { response: result, transportError } = await queryDaemon(socket, {
95
95
  ...req,
96
96
  type: 'thread.op',
97
97
  argv: commandArgv,
@@ -99,7 +99,9 @@ async function sendThreadOp(req, commandArgv) {
99
99
  sessionId: invokingSessionId,
100
100
  }, 10000);
101
101
  if (!result)
102
- return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'daemon 暂时不可用' };
102
+ return {
103
+ ok: false, code: 'DAEMON_UNAVAILABLE', error: daemonUnavailableMessage(transportError, 'daemon 暂时不可用'),
104
+ };
103
105
  return result;
104
106
  }
105
107
  function printResult(data, jsonFormat) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { resolvePaths } from '../paths.js';
4
- import { ipcQuery } from '../ipc.js';
4
+ import { daemonUnavailableMessage, queryDaemon } from './help.js';
5
5
  import { isHelpFlag, wantsHelp } from './help.js';
6
6
  import { readDelegationCredential } from '../core/auth/agent-delegation.js';
7
7
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
@@ -642,9 +642,9 @@ async function request(cmd, timeoutMs = 10_000) {
642
642
  }
643
643
  catch { }
644
644
  }
645
- const res = await ipcQuery(resolvePaths().socket, actorSessionId ? { ...cmd, actorSessionId, delegationToken } : delegationToken ? { ...cmd, delegationToken } : { ...cmd, controlToken }, timeoutMs);
645
+ const { response: res, transportError } = await queryDaemon(resolvePaths().socket, actorSessionId ? { ...cmd, actorSessionId, delegationToken } : delegationToken ? { ...cmd, delegationToken } : { ...cmd, controlToken }, timeoutMs);
646
646
  if (!res)
647
- throw new Error('错误: 无法连接 evolcore 服务');
647
+ throw new Error(`错误: ${daemonUnavailableMessage(transportError, '无法连接 evolcore 服务')}`);
648
648
  if (res.ok === false || res.error)
649
649
  throw new Error(res.error || 'trigger command failed');
650
650
  return res;
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { MAIN_PACKAGE_NAME, PRODUCT_DISPLAY_NAME, WEB_PACKAGE_NAME } from '../product.js';
8
8
  import { getArgValue, wantsHelp } from './help.js';
9
+ import { resolveGlobalPkg } from '../utils/npm-ops.js';
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const PACKAGE_ROOT = path.resolve(__dirname, '../..');
11
12
  const RESET = '\x1b[0m';
@@ -54,8 +55,11 @@ export function handleVersion(args) {
54
55
  const mainPkg = readPkgJson(PACKAGE_ROOT);
55
56
  const mainEntry = path.join(PACKAGE_ROOT, 'dist', 'cli', 'index.js');
56
57
  const mainTs = getFileModTime(mainEntry);
57
- // ecweb 包
58
- const ecwebDir = path.join(PACKAGE_ROOT, 'ecweb');
58
+ // EC Web may be in the repository (dev mode) or nested under the globally
59
+ // installed EvolCore package. Use the shared resolver so both layouts report
60
+ // the actual installed package instead of `?`.
61
+ const ecwebInstalled = resolveGlobalPkg(WEB_PACKAGE_NAME);
62
+ const ecwebDir = ecwebInstalled ? path.dirname(ecwebInstalled.path) : path.join(PACKAGE_ROOT, 'ecweb');
59
63
  const ecwebPkg = readPkgJson(ecwebDir);
60
64
  const ecwebEntry = path.join(ecwebDir, 'dist', 'index.js');
61
65
  const ecwebTs = getFileModTime(ecwebEntry);
@@ -227,6 +227,7 @@ export function resolveConfigFieldRule(fieldPath, scope) {
227
227
  }
228
228
  // 数值参数:整理节奏与容量上限,均 owner-only
229
229
  if (field === 'memory.idleHours'
230
+ || field === 'memory.maxUnconsolidatedHours'
230
231
  || field === 'memory.checkIntervalHours'
231
232
  || field === 'memory.backlogHours'
232
233
  || field === 'memory.indexMaxBytes'
@@ -0,0 +1,44 @@
1
+ export const ECWEB_SERVICE_PROTOCOL = Object.freeze({
2
+ name: 'evolcore.ecweb',
3
+ major: 1,
4
+ minor: 1,
5
+ capabilities: Object.freeze([
6
+ 'daemon.status',
7
+ 'agent.directory',
8
+ 'aun.directory',
9
+ 'menu.execute',
10
+ 'provider.management',
11
+ 'basic-settings',
12
+ 'model.catalog',
13
+ 'baseagent.snapshot',
14
+ 'monitor.snapshot',
15
+ 'monitor.subscription',
16
+ 'remote-auth',
17
+ 'trigger.history',
18
+ ]),
19
+ });
20
+ export function validateEcwebClientProtocolRequirements(value) {
21
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
22
+ return 'ECWeb client protocol requirements are missing or malformed';
23
+ }
24
+ const requirements = value;
25
+ if (requirements.name !== ECWEB_SERVICE_PROTOCOL.name) {
26
+ return `Unsupported ECWeb protocol name: ${String(requirements.name ?? 'missing')}`;
27
+ }
28
+ if (requirements.major !== ECWEB_SERVICE_PROTOCOL.major) {
29
+ return `Unsupported ECWeb protocol major version: ${String(requirements.major ?? 'missing')}`;
30
+ }
31
+ if (!Number.isInteger(requirements.minMinor) || requirements.minMinor < 0
32
+ || requirements.minMinor > ECWEB_SERVICE_PROTOCOL.minor) {
33
+ return `ECWeb requires protocol minor ${String(requirements.minMinor ?? 'missing')}, host supports ${ECWEB_SERVICE_PROTOCOL.minor}`;
34
+ }
35
+ if (!Array.isArray(requirements.requiredCapabilities)
36
+ || requirements.requiredCapabilities.some(capability => typeof capability !== 'string' || !capability.trim())) {
37
+ return 'ECWeb required capabilities are malformed';
38
+ }
39
+ const supported = new Set(ECWEB_SERVICE_PROTOCOL.capabilities);
40
+ const missing = requirements.requiredCapabilities.filter(capability => !supported.has(capability));
41
+ if (missing.length > 0)
42
+ return `ECWeb requires unsupported capabilities: ${missing.join(', ')}`;
43
+ return null;
44
+ }
@@ -226,6 +226,11 @@ export function createAgentInitializationRuntime({ agentRegistry, agentMap, chan
226
226
  bindOwner: async (aid, ownerAid) => {
227
227
  await ownerResolver.bind(aid, ownerAid);
228
228
  },
229
+ ownerBindActive: (aid, ownerAid) => {
230
+ const task = ownerResolver.snapshot(aid).task;
231
+ return task?.operation === 'bind' && task.owner_aid === ownerAid
232
+ && (task.status === 'pending' || task.status === 'completed');
233
+ },
229
234
  checkOwner: async (aid, expectedOwnerAid) => {
230
235
  const view = await ownerResolver.refresh(aid, { maxAgeMs: OWNERSHIP_AUTHORIZATION_MAX_AGE_MS });
231
236
  if (view.consensusStatus === 'consensus')
@@ -257,6 +262,7 @@ export function createAgentInitializationRuntime({ agentRegistry, agentMap, chan
257
262
  const agent = agentRegistry.get(aid);
258
263
  return !!agent && agent.config.enabled !== false && agentMap.has(`${aid}::${agent.baseagent}`) && isConnected(aid);
259
264
  },
265
+ authorize: actor => actor === 'local-control' || ownerResolver.daemonOwners().includes(actor),
260
266
  });
261
267
  }
262
268
  export function recoverIncompleteAgentInitializations(service, agentsDir = resolvePaths().agentsDir) {
@@ -440,7 +440,7 @@ export async function authorizeDelegatedAunMsgSend(registry, input) {
440
440
  catch {
441
441
  return { ok: false, code: 'INVALID_DELEGATION', reason: 'Delegation relation key is invalid' };
442
442
  }
443
- await refreshOwnershipForAuthorization(grant.selfAid);
443
+ await refreshOwnershipForAuthorization(grant.selfAid, { allowTransient: true });
444
444
  const builtSubject = buildAuthSubject({
445
445
  selfAid: grant.selfAid,
446
446
  actorId: grant.actorId,
@@ -503,10 +503,13 @@ function isValidFullAccessExecutionIdentity(identity) {
503
503
  || identity.processRole !== 'fullaccess-run'
504
504
  || identity.dataScope !== 'daemon')
505
505
  return false;
506
- if (identity.source === 'fullaccess-command' || identity.source === 'agent-policy') {
506
+ if (identity.source === 'fullaccess-command' || identity.source === 'agent-policy' || identity.source === 'session-policy') {
507
507
  return typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
508
- && (identity.source !== 'agent-policy'
509
- || (typeof identity.agentAid === 'string' && identity.agentAid.length > 0));
508
+ && (identity.source === 'fullaccess-command'
509
+ || (typeof identity.agentAid === 'string'
510
+ && identity.agentAid.length > 0
511
+ && (identity.source !== 'session-policy'
512
+ || (typeof identity.sessionId === 'string' && identity.sessionId.length > 0))));
510
513
  }
511
514
  return identity.source === 'trigger'
512
515
  && typeof identity.authorizedBy === 'string' && identity.authorizedBy.length > 0
@@ -107,7 +107,8 @@ export async function authorizeFreshOperation(params) {
107
107
  const configCommand = params.resolvedConfigCommand;
108
108
  const changesOwner = configCommand?.kind === 'field' && configCommand.subcommand !== 'get'
109
109
  && ['owner', 'owners'].includes(configCommand.field.split('.')[0]);
110
- const force = changesOwner || params.intent.operation === 'ec.aid.bind' || params.intent.operation === 'config.restore';
110
+ const force = changesOwner || ['ec.aid.bind', 'ec.aid.bind.status', 'ec.aid.unbind', 'ec.aid.transfer']
111
+ .includes(params.intent.operation) || params.intent.operation === 'config.restore';
111
112
  const operation = getOperationMeta(params.intent.operation);
112
113
  const sensitive = force || params.intent.dangerous || operation?.dangerous || operation?.category !== 'read'
113
114
  || configCommand?.dangerous || subject.processRole === 'fullaccess-run';
@@ -116,6 +117,7 @@ export async function authorizeFreshOperation(params) {
116
117
  ...(sensitive ? {} : { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS }),
117
118
  includeDaemon: subject.isDaemonOwner || subject.processRole === 'fullaccess-run'
118
119
  || params.intent.scope === 'process' || params.intent.scope === 'control' || force,
120
+ allowTransient: !force,
119
121
  });
120
122
  const current = buildAuthSubject({
121
123
  selfAid: subject.selfAid, agentName: subject.agentName, actorId: subject.actorId,
@@ -25,11 +25,15 @@ export function auditCommandAuthorization(event) {
25
25
  }
26
26
  export function auditFullAccessEvent(input) {
27
27
  const configurationEvent = input.event === 'fullaccess.trigger.configured'
28
- || input.event === 'fullaccess.trigger.deescalated';
28
+ || input.event === 'fullaccess.trigger.deescalated'
29
+ || input.event === 'fullaccess.session-policy.configured'
30
+ || input.event === 'fullaccess.session-policy.deescalated';
29
31
  const executionEvent = input.event === 'fullaccess.execution.started'
30
32
  || input.event === 'fullaccess.execution.ended'
31
33
  || input.event === 'fullaccess.agent-policy.execution.started'
32
34
  || input.event === 'fullaccess.agent-policy.execution.ended'
35
+ || input.event === 'fullaccess.session-policy.execution.started'
36
+ || input.event === 'fullaccess.session-policy.execution.ended'
33
37
  || input.event === 'fullaccess.trigger.execution.started'
34
38
  || input.event === 'fullaccess.trigger.execution.ended';
35
39
  // The scheduler triggers a run, but the executing subject is the task-bound
@@ -127,7 +127,11 @@ export function buildDebugTaskContext(input) {
127
127
  chatType,
128
128
  selfAid: session.selfAID,
129
129
  peerId,
130
- peerKey: channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined,
130
+ // 系统会话的目标关系在 metadata.peerKey(与 response-engine 的任务上下文同源),
131
+ // 不能从后台伪对端 peerId 推。
132
+ peerKey: meta.systemSession && typeof meta.peerKey === 'string' && meta.peerKey
133
+ ? meta.peerKey
134
+ : (channel && peerKeyId ? formatPeerKey(channel, peerKeyId) : undefined),
131
135
  peerName: meta.peerName,
132
136
  peerType: meta.peerType,
133
137
  threadId,
@@ -323,6 +323,22 @@ const OPERATIONS = [
323
323
  description: '更新当前主会话或话题的 idle 触发设置',
324
324
  sources: ['slash', 'menu', 'ecweb', 'control'],
325
325
  },
326
+ {
327
+ id: 'session.fullaccess.current',
328
+ category: 'read',
329
+ dangerous: true,
330
+ defaultScopes: ['relation'],
331
+ description: '查看当前 EC 会话的 fullaccess 状态',
332
+ sources: ['slash', 'menu'],
333
+ },
334
+ {
335
+ id: 'session.fullaccess.update',
336
+ category: 'dangerous',
337
+ dangerous: true,
338
+ defaultScopes: ['relation'],
339
+ description: '更新当前 EC 会话的 fullaccess 状态',
340
+ sources: ['slash', 'menu'],
341
+ },
326
342
  {
327
343
  id: 'session.topic.current',
328
344
  category: 'read',
@@ -1123,7 +1139,7 @@ const OPERATIONS = [
1123
1139
  },
1124
1140
  {
1125
1141
  id: 'ec.aid.bind', category: 'write-agent', dangerous: true, defaultScopes: ['agent'],
1126
- description: 'Request a protocol Owner binding using the subject SDK client', sources: ['agent-tool', 'menu.cli'],
1142
+ description: 'Request or decide a protocol Owner binding using the subject SDK client', sources: ['agent-tool', 'menu.cli'],
1127
1143
  },
1128
1144
  {
1129
1145
  id: 'ec.aid.bind.status', category: 'read', dangerous: false, defaultScopes: ['agent'],
@@ -3,8 +3,8 @@ import { ownerResolver, OWNERSHIP_AUTHORIZATION_MAX_AGE_MS } from './ownership/o
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { randomBytes } from 'crypto';
6
- import { kitsTemplatesDir, agentMdPath, resolvePaths } from '../paths.js';
7
- import { buildInitialAgentMd, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
6
+ import { kitsTemplatesDir, resolvePaths } from '../paths.js';
7
+ import { buildInitialAgentMd, resolveAgentDisplayName, verifyLocalAgentMd } from '../aun/aid/agentmd.js';
8
8
  import { logger } from '../utils/logger.js';
9
9
  import { readAgentWelcomeTemplate } from '../utils/welcome.js';
10
10
  import { loadAgent, saveAgent } from '../config-store.js';
@@ -308,7 +308,13 @@ export class BootstrapService {
308
308
  return false;
309
309
  }
310
310
  if (starting) {
311
- await this.publishAgentMdIfSupported(ctx.adapter, aid, agentName);
311
+ // AUN creation/binding has already published agent.md, and the fresh
312
+ // Owner consensus above requires it. A normal upload here re-signs the
313
+ // approved original while the Owner may still be confirming its hash
314
+ // and ETag. Sending a welcome must not mutate that binding evidence.
315
+ if (channelType !== 'aun') {
316
+ await this.publishAgentMdIfSupported(ctx.adapter, aid, agentName);
317
+ }
312
318
  this.setLifecycle(agent, aid, 'bootstrapping');
313
319
  lifecycleStarted = true;
314
320
  }
@@ -546,18 +552,20 @@ export class BootstrapService {
546
552
  async publishAgentMdIfSupported(adapter, aid, fallbackName) {
547
553
  if (typeof adapter.uploadAgentMd !== 'function')
548
554
  return;
549
- const existing = fs.existsSync(agentMdPath(aid)) ? fs.readFileSync(agentMdPath(aid), 'utf-8') : '';
550
- const content = existing.trim() || buildInitialAgentMd({
555
+ const verification = await verifyLocalAgentMd(aid);
556
+ if (verification.status === 'verified') {
557
+ logger.info(`[Bootstrap] Preserving verified local agent.md for ${aid}; upload skipped`);
558
+ return;
559
+ }
560
+ if (verification.status !== 'missing') {
561
+ throw new Error(`Local agent.md cannot be safely republished for ${aid}: ${verification.reason ?? verification.status}`);
562
+ }
563
+ const content = buildInitialAgentMd({
551
564
  aid,
552
565
  name: fallbackName,
553
566
  type: 'agent',
554
567
  });
555
- try {
556
- await adapter.uploadAgentMd(content);
557
- logger.info(`[Bootstrap] Published agent.md for ${aid}`);
558
- }
559
- catch (e) {
560
- logger.warn(`[Bootstrap] Failed to publish agent.md for ${aid}: ${e instanceof Error ? e.message : String(e)}`);
561
- }
568
+ await adapter.uploadAgentMd(content);
569
+ logger.info(`[Bootstrap] Published initial agent.md for ${aid}`);
562
570
  }
563
571
  }
@@ -243,11 +243,14 @@ function parseAidCommand(argv, source, defaultSelfAid) {
243
243
  try {
244
244
  const command = parseOwnershipCommand(argv.slice(1));
245
245
  const subjectAid = command.subjectAid ?? defaultSelfAid;
246
- return recognized(command.action === 'bind' ? 'ec.aid.bind' : 'ec.aid.bind.status', 'agent', source, {
246
+ const isMutation = command.action !== 'status';
247
+ return recognized(isMutation ? 'ec.aid.bind' : 'ec.aid.bind.status', 'agent', source, {
247
248
  ...(subjectAid ? { self: subjectAid } : {}),
248
249
  ...(command.subjectAid ? { subjectAid: command.subjectAid } : {}),
249
250
  ...(command.action === 'bind' ? { ownerAid: command.ownerAid } : {}),
250
- }, argv, command.action === 'bind');
251
+ ...(command.action === 'accept' || command.action === 'reject'
252
+ ? { decision: command.action, operationId: command.operationId } : {}),
253
+ }, argv, isMutation);
251
254
  }
252
255
  catch (error) {
253
256
  return raw(source, argv, error instanceof Error ? error.message : 'Invalid aid bind command');
@@ -1380,6 +1380,7 @@ export class CommandHandler {
1380
1380
  ? {
1381
1381
  kind: 'system.error',
1382
1382
  text: result.text,
1383
+ ...(result.raw !== undefined ? { raw: result.raw } : {}),
1383
1384
  subtype: `session.${content.trim().toLowerCase().startsWith('/new') ? 'new' : 'renew'}.error`,
1384
1385
  recoverable: true,
1385
1386
  }
@@ -1394,7 +1395,12 @@ export class CommandHandler {
1394
1395
  catch (error) {
1395
1396
  if (error instanceof BaseagentRunnerUnavailableError) {
1396
1397
  logger.error(`[CommandHandler] baseagent mismatch blocked: channel=${channel} requested=${error.baseagent} owner=${error.evolagentName} available=${error.availableBaseagents.join(',') || '<none>'}`);
1397
- return { kind: 'command.error', text: this.formatBaseagentUnavailable(error), reason: error.code };
1398
+ return {
1399
+ kind: 'command.error',
1400
+ text: this.formatBaseagentUnavailable(error),
1401
+ raw: error.message,
1402
+ reason: error.code,
1403
+ };
1398
1404
  }
1399
1405
  throw error;
1400
1406
  }
@@ -2196,8 +2202,9 @@ export class CommandHandler {
2196
2202
  const command = parseOwnershipCommand(argv);
2197
2203
  const targetAid = command.subjectAid ?? grant.selfAid;
2198
2204
  ownerResolver.assertLocalSubject(targetAid);
2199
- if (command.action === 'bind' && ['readonly', 'auto'].includes(grant.permissionMode ?? '')) {
2200
- return { ok: false, code: 'NOT_ALLOWED', error: 'Binding requires a request or bypass task' };
2205
+ const isMutation = command.action !== 'status';
2206
+ if (isMutation && !['request', 'bypass'].includes(grant.permissionMode ?? '')) {
2207
+ return { ok: false, code: 'NOT_ALLOWED', error: 'Binding changes and decisions require a request or bypass task' };
2201
2208
  }
2202
2209
  await refreshOwnershipForAuthorization(targetAid);
2203
2210
  let conversationId;
@@ -2215,12 +2222,20 @@ export class CommandHandler {
2215
2222
  if (targetAid !== grant.selfAid && !subject.isDaemonOwner) {
2216
2223
  return { ok: false, code: 'SCOPE_MISMATCH', error: 'A different local subject requires the current daemon Owner' };
2217
2224
  }
2218
- if (!subject.isDaemonOwner && (command.action === 'bind' ? subject.role !== 'owner' : !isManagementRole(subject.role))) {
2225
+ if (!subject.isDaemonOwner && (isMutation ? subject.role !== 'owner' : !isManagementRole(subject.role))) {
2219
2226
  return { ok: false, code: 'OWNER_REQUIRED', error: 'Ownership binding requires the subject Owner or daemon Owner; first daemon binding uses the local CLI' };
2220
2227
  }
2221
2228
  const decision = await authorizeFreshOperation({
2222
2229
  source: 'agent-tool', subject,
2223
- intent: { operation: command.action === 'bind' ? 'ec.aid.bind' : 'ec.aid.bind.status', scope: 'agent', source: 'agent-tool', args: { self: targetAid, ...(command.action === 'bind' ? { ownerAid: command.ownerAid } : {}) } },
2230
+ intent: {
2231
+ operation: isMutation ? 'ec.aid.bind' : 'ec.aid.bind.status', scope: 'agent', source: 'agent-tool',
2232
+ args: {
2233
+ self: targetAid,
2234
+ ...(command.action === 'bind' ? { ownerAid: command.ownerAid } : {}),
2235
+ ...(command.action === 'accept' || command.action === 'reject'
2236
+ ? { decision: command.action, operationId: command.operationId } : {}),
2237
+ },
2238
+ },
2224
2239
  auditMetadata: { taskId: grant.taskId, messageId: grant.messageId },
2225
2240
  });
2226
2241
  if (!decision.allow)
@@ -6,6 +6,7 @@ import { authorizationDenialData, formatAuthorizationDenial } from '../auth/auth
6
6
  import { ConfigTarget, read as readConfig } from '../../config/config-manager.js';
7
7
  import { readRolesConfig } from '../../config/roles.js';
8
8
  import { normalizeMenuError } from './menu-protocol.js';
9
+ import { isAgentFullAccessOwner } from '../role/runtime-policy.js';
9
10
  import { isValidAid } from '../../aun/aid/validation.js';
10
11
  import { DEFAULT_SESSION_LOOP_SECONDS, MAX_SESSION_LOOP_SECONDS, MIN_SESSION_LOOP_SECONDS } from '../session/session-idle-monitor.js';
11
12
  const DEFAULT_DIRECTORY = { getGroup: groupInfo, getMembers: groupMembers };
@@ -79,6 +80,19 @@ const ENTRIES = [
79
80
  valueSchema: { type: 'boolean' },
80
81
  control: 'toggle-with-number',
81
82
  },
83
+ {
84
+ id: 'session.fullaccess',
85
+ type: 'setting',
86
+ contexts: ['private_relation'],
87
+ name: 'session',
88
+ label: '当前 EC 会话 fullaccess',
89
+ section: 'relation_behavior',
90
+ order: 6,
91
+ query: { cmd: '/session', args: { view: 'fullaccess' }, operation: 'session.fullaccess.current', scope: 'relation' },
92
+ update: { cmd: '/session', args: { resource: 'fullaccess' }, operation: 'session.fullaccess.update', scope: 'relation' },
93
+ valueSchema: { type: 'boolean' },
94
+ control: 'toggle',
95
+ },
82
96
  relation('model.relation', 'model', '模型', 10, '/model', 'model.current', { options: { cmd: '/model', operation: 'model.list', scope: 'relation', mode: 'dynamic' }, update: { cmd: '/model', operation: 'model.use', scope: 'relation' }, control: 'select' }),
83
97
  relation('effort.relation', 'effort', '推理强度', 20, '/effort', 'model.effort.current', { options: { cmd: '/effort', operation: 'model.effort.current', scope: 'relation', mode: 'dynamic', dependsOn: ['model.relation'] }, update: { cmd: '/effort', operation: 'model.effort', scope: 'relation' }, control: 'select' }),
84
98
  relation('chatmode.relation', 'chatmode', '对话模式', 30, '/chatmode', 'chatmode.current', { options: { cmd: '/chatmode', operation: 'chatmode.current', scope: 'relation', mode: 'static', items: [{ value: 'interactive', label: '交互模式' }, { value: 'proactive', label: '主动模式' }] }, update: { cmd: '/chatmode', operation: 'chatmode.update', scope: 'relation' }, control: 'select' }),
@@ -290,8 +304,8 @@ export function bindMenuExecutionContextArgs(args, context) {
290
304
  if (!args && context.kind !== 'group_relation')
291
305
  return args;
292
306
  const { context: _context, ...bound } = args ?? {};
293
- const isSessionLoop = bound.resource === 'loop' || bound.view === 'loop';
294
- if (isSessionLoop)
307
+ const isSessionScoped = ['loop', 'fullaccess'].includes(String(bound.resource ?? bound.view ?? ''));
308
+ if (isSessionScoped)
295
309
  return bound;
296
310
  if (context.kind === 'private_relation' || context.kind === 'group_relation') {
297
311
  if (bound.scope !== 'role')
@@ -368,6 +382,17 @@ async function operationView(binding, context, options = {}) {
368
382
  reason: { code: 'NOT_ALLOWED', message: 'Pairing code issuance requires encrypted DaemonOwner authentication' },
369
383
  };
370
384
  }
385
+ if (binding.operation.startsWith('session.fullaccess.')) {
386
+ const allowed = context.kind === 'private_relation'
387
+ && context.subject.chatType === 'private'
388
+ && context.subject.isDaemonOwner === true
389
+ && isAgentFullAccessOwner(context.subject.selfAid ?? '', context.subject.principalId);
390
+ return {
391
+ supported: true,
392
+ allowed,
393
+ ...(allowed ? {} : { reason: { code: 'FORBIDDEN', message: 'session fullaccess requires the Agent Owner and daemon Owner identity' } }),
394
+ };
395
+ }
371
396
  if (binding.operation === 'group.rulespolicy.update' && context.subject.role !== 'owner') {
372
397
  return { supported: true, allowed: false, reason: { code: 'PERMISSION_DENIED', message: 'rulesPolicy can only be changed by an Agent owner' } };
373
398
  }
@@ -377,7 +402,7 @@ async function operationView(binding, context, options = {}) {
377
402
  function wireBinding(binding, context) {
378
403
  if (!binding || binding.supported === false)
379
404
  return undefined;
380
- if (binding.operation.startsWith('session.loop.'))
405
+ if (binding.operation.startsWith('session.loop.') || binding.operation.startsWith('session.fullaccess.'))
381
406
  return {
382
407
  args: context.kind === 'group_relation'
383
408
  ? { ...(binding.args ?? {}), context: { kind: context.kind, ...(context.groupId ? { groupId: context.groupId } : {}) } }
@@ -406,10 +431,10 @@ export async function getMenuCatalog(input) {
406
431
  operations[key] = view;
407
432
  }
408
433
  entries.push({ id: entry.id, type: entry.type, name: entry.name, label: entry.label, section: entry.section, order: entry.order, target: { scope: context.kind === 'agent' ? 'agent' : context.kind === 'process' ? 'process' : 'relation', ...(context.self ? { self: context.self } : {}), ...(context.peerKey ? { peerKey: context.peerKey } : {}) }, valueSchema: entry.valueSchema ?? { type: 'object' }, operations, wire: { name: entry.name, operations: { ...(wireBinding(entry.query, context) ? { query: wireBinding(entry.query, context) } : {}), ...(wireBinding(entry.options, context) ? { options: wireBinding(entry.options, context) } : {}), ...(wireBinding(entry.update, context) ? { update: wireBinding(entry.update, context) } : {}), ...(wireBinding(entry.action, context) ? { action: wireBinding(entry.action, context) } : {}) } }, ui: { control: entry.control ?? 'action' } });
409
- if (entry.id === 'session.loop') {
434
+ if (entry.id === 'session.loop' || entry.id === 'session.fullaccess') {
410
435
  const loopEntry = entries[entries.length - 1];
411
- let target = { level: 'relation-main' };
412
- if (input.resolveLoopTarget) {
436
+ let target = { level: 'relation-main', scope: 'agent' };
437
+ if ((entry.id === 'session.loop' || entry.id === 'session.fullaccess') && input.resolveLoopTarget) {
413
438
  try {
414
439
  target = await input.resolveLoopTarget(context);
415
440
  }
@@ -420,12 +445,23 @@ export async function getMenuCatalog(input) {
420
445
  }
421
446
  }
422
447
  }
423
- loopEntry.target = target;
424
- loopEntry.label = target.level === 'topic-session' ? '当前话题闲置触发' : '关系主会话闲置触发';
425
- loopEntry.ui = {
426
- control: entry.control,
427
- idleSeconds: { type: 'integer', minimum: MIN_SESSION_LOOP_SECONDS, maximum: MAX_SESSION_LOOP_SECONDS, default: DEFAULT_SESSION_LOOP_SECONDS, unit: 'seconds' },
428
- };
448
+ if (entry.id === 'session.loop') {
449
+ loopEntry.target = target;
450
+ loopEntry.label = target.level === 'topic-session' ? '当前话题闲置触发' : '关系主会话闲置触发';
451
+ loopEntry.ui = {
452
+ control: entry.control,
453
+ idleSeconds: { type: 'integer', minimum: MIN_SESSION_LOOP_SECONDS, maximum: MAX_SESSION_LOOP_SECONDS, default: DEFAULT_SESSION_LOOP_SECONDS, unit: 'seconds' },
454
+ };
455
+ }
456
+ else {
457
+ loopEntry.target = {
458
+ level: target.level,
459
+ sessionId: target.sessionId,
460
+ scope: target.level === 'topic-session' ? 'session' : 'agent',
461
+ };
462
+ loopEntry.label = '当前 EC 会话 fullaccess';
463
+ loopEntry.ui = { control: entry.control };
464
+ }
429
465
  }
430
466
  }
431
467
  const policyRevision = hash({ role: context.subject.role, self: context.subject.selfAid, peerKey: context.subject.peerKey, groupMembershipRole: context.groupMembershipRole, baselineRole: context.baselineRole, paircodeAllowed, roles: readRolesConfig(context.subject.selfAid) });