evolcore 0.0.39 → 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 (102) hide show
  1. package/CHANGELOG.md +49 -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 +51 -27
  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/client.js +1 -1
  11. package/dist/aun/aid/control-aid.js +23 -98
  12. package/dist/aun/aid/ownership-operation.js +1 -3
  13. package/dist/aun/group/invite-service.js +6 -0
  14. package/dist/aun/msg/p2p.js +8 -12
  15. package/dist/channels/aun.js +66 -48
  16. package/dist/channels/dingtalk.js +8 -2
  17. package/dist/channels/feishu.js +6 -1
  18. package/dist/channels/wecom.js +7 -3
  19. package/dist/cli/agent-create.js +23 -4
  20. package/dist/cli/aid-bind-command.js +4 -1
  21. package/dist/cli/daemon-commands.js +242 -17
  22. package/dist/cli/index.js +25 -13
  23. package/dist/cli/memory-command.js +33 -14
  24. package/dist/cli/restart-monitor.js +35 -13
  25. package/dist/cli/task-context.js +1 -0
  26. package/dist/config/config-field-policy.js +1 -0
  27. package/dist/contracts/ecweb-service-protocol.js +44 -0
  28. package/dist/core/agent-runtime.js +6 -0
  29. package/dist/core/auth/agent-delegation.js +7 -4
  30. package/dist/core/auth/auth-gateway.js +3 -1
  31. package/dist/core/auth/authorization-audit.js +5 -1
  32. package/dist/core/auth/debug-context.js +5 -1
  33. package/dist/core/auth/operation-catalog.js +16 -0
  34. package/dist/core/command/command-handler.js +7 -1
  35. package/dist/core/command/menu-catalog.js +48 -12
  36. package/dist/core/command/menu-handler.js +72 -4
  37. package/dist/core/command/menu-protocol.js +20 -1
  38. package/dist/core/command/slash-gate.js +17 -3
  39. package/dist/core/command/slash-handler.js +90 -7
  40. package/dist/core/event-catalog.js +2 -0
  41. package/dist/core/interaction-router.js +2 -1
  42. package/dist/core/ipc-registration.js +26 -7
  43. package/dist/core/memory/batch-meta.js +2 -0
  44. package/dist/core/memory/consolidation-context.js +42 -51
  45. package/dist/core/memory/consolidation-ops-records.js +39 -7
  46. package/dist/core/memory/consolidation-trigger.js +71 -64
  47. package/dist/core/memory/consolidation.js +226 -20
  48. package/dist/core/memory/memory-writer.js +160 -34
  49. package/dist/core/memory/relation-idle-monitor.js +287 -0
  50. package/dist/core/memory/relation-sweep.js +206 -0
  51. package/dist/core/message/aun-ipc-sender.js +5 -0
  52. package/dist/core/message/message-bridge.js +64 -55
  53. package/dist/core/message/message-log.js +17 -0
  54. package/dist/core/message/message-queue.js +6 -0
  55. package/dist/core/message/outbound.js +70 -0
  56. package/dist/core/message/peer-mode.js +7 -2
  57. package/dist/core/message/response-engine.js +341 -73
  58. package/dist/core/ownership/owner-resolver.js +384 -178
  59. package/dist/core/permission/ec-command-parser.js +19 -3
  60. package/dist/core/permission/tool-policy.js +7 -7
  61. package/dist/core/role/runtime-policy.js +64 -0
  62. package/dist/core/session/session-manager.js +25 -5
  63. package/dist/core/system-session/memory.js +3 -18
  64. package/dist/ecweb/basic-settings-service.js +709 -0
  65. package/dist/ecweb/providers-service.js +172 -0
  66. package/dist/index.js +92 -4
  67. package/dist/ipc.js +78 -11
  68. package/dist/stats/db.js +33 -0
  69. package/dist/stats/index.js +2 -2
  70. package/dist/stats/query.js +246 -0
  71. package/dist/stats/writer.js +41 -1
  72. package/dist/trigger/application-service.js +12 -0
  73. package/dist/trigger/authorization.js +2 -2
  74. package/dist/trigger/history.js +36 -0
  75. package/dist/trigger/scheduler.js +3 -0
  76. package/dist/utils/ecweb-supervisor.js +31 -10
  77. package/dist/utils/error-utils.js +8 -4
  78. package/dist/utils/evolcore-version.js +3 -2
  79. package/dist/utils/host-info.js +52 -0
  80. package/ecagent/dist/harness/agent-harness.js +4 -1
  81. package/kits/docs/evolcore/INDEX.md +1 -1
  82. package/kits/docs/evolcore/agent.md +1 -1
  83. package/kits/docs/evolcore/aid.md +29 -12
  84. package/kits/docs/evolcore/config.md +9 -7
  85. package/kits/docs/evolcore/memory-architecture.md +3 -4
  86. package/kits/docs/evolcore/memory-consolidation.md +28 -7
  87. package/kits/docs/evolcore/memory-tags.md +1 -1
  88. package/kits/docs/evolcore/memory.md +1 -1
  89. package/kits/docs/evolcore/menu-client.md +1 -1
  90. package/kits/docs/evolcore/menu-protocol.md +6 -0
  91. package/kits/docs/evolcore/msg.md +1 -1
  92. package/kits/eck_manifest.json +3 -3
  93. package/kits/rules/04-relation.md +3 -2
  94. package/kits/schemas/agent-config.schema.13.json +5 -1
  95. package/kits/templates/memory-consolidation/generalize.md +32 -4
  96. package/kits/templates/memory-consolidation/notes.md +2 -2
  97. package/kits/templates/memory-consolidation/promote.md +10 -2
  98. package/kits/templates/system-fragments/baseagent.md +5 -0
  99. package/kits/templates/system-fragments/memory.md +12 -3
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/kits/{rules/07-memory-and-thread.md → templates/system-fragments/thread.md} +17 -80
  102. package/package.json +3 -3
@@ -130,6 +130,18 @@ export async function cmdRestartMonitor() {
130
130
  const hasCorrelatedRequest = requestId !== 'unknown';
131
131
  const upgradeJobId = hasCorrelatedRequest ? readRestartRequest(pendingFile, requestId)?.upgradeJobId : undefined;
132
132
  const upgradeJob = typeof upgradeJobId === 'string' ? readUpgradeJob(upgradeJobId) : null;
133
+ let configuredAutoUpgrade = true;
134
+ if (!upgradeJob && !process.argv.includes('--apply-package-upgrades') && !process.argv.includes('--skip-package-upgrades')) {
135
+ try {
136
+ configuredAutoUpgrade = loadDaemonConfig().upgrade?.autoOnStart !== false;
137
+ }
138
+ catch {
139
+ configuredAutoUpgrade = false;
140
+ }
141
+ }
142
+ const applyPackageUpgrades = !!upgradeJob
143
+ || process.argv.includes('--apply-package-upgrades')
144
+ || (!process.argv.includes('--skip-package-upgrades') && configuredAutoUpgrade);
133
145
  if (upgradeJobId && (!upgradeJob || upgradeJob.restartRequestId !== requestId || upgradeJob.released || upgradeJob.deadline <= Date.now())) {
134
146
  if (upgradeJob)
135
147
  updateUpgradeJob(upgradeJob.jobId, { status: 'timeout', success: false, released: true, code: 'UPGRADE_TIMEOUT', reason: 'Update expired before restart preflight' }, '更新已超时,未执行重启');
@@ -371,16 +383,24 @@ export async function cmdRestartMonitor() {
371
383
  const stoppedCodex = stopCodexAppServerOrphans();
372
384
  if (stoppedCodex)
373
385
  log(`Cleaned ${stoppedCodex} EvolCore codex app-server orphan(s)`);
374
- // Apply only the preflight cache. There is no registry VERSION query after
375
- // shutdown (npm still downloads the exact cached package when needed).
376
- log('Applying cached version information...');
377
- // 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
378
- // Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
379
- const upgradeBackup = await backupDistForUpgrade(p, log);
380
- const [upgrade] = await applyCachedPackageUpgrades(undefined, {
381
- log,
382
- ...(upgradeJob ? { target: upgradeJob.target, distTag: upgradeJob.distTag, npmConfig: upgradeJob.npmConfig } : {}),
383
- });
386
+ let upgradeBackup = null;
387
+ let upgrade;
388
+ if (applyPackageUpgrades) {
389
+ // Apply only the preflight cache. There is no registry VERSION query after
390
+ // shutdown (npm still downloads the exact cached package when needed).
391
+ log('Applying cached version information...');
392
+ // 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
393
+ // Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
394
+ upgradeBackup = await backupDistForUpgrade(p, log);
395
+ [upgrade] = await applyCachedPackageUpgrades(undefined, {
396
+ log,
397
+ ...(upgradeJob ? { target: upgradeJob.target, distTag: upgradeJob.distTag, npmConfig: upgradeJob.npmConfig } : {}),
398
+ });
399
+ }
400
+ else {
401
+ log('Skipping cached package upgrade because upgrade.autoOnStart=false');
402
+ upgrade = { status: 'skipped', error: 'Automatic package upgrades disabled by autoOnStart' };
403
+ }
384
404
  let upgradeRolledBack;
385
405
  switch (upgrade.status) {
386
406
  case 'upgraded':
@@ -405,9 +425,11 @@ export async function cmdRestartMonitor() {
405
425
  log(`Already up to date (${upgrade.from})`);
406
426
  break;
407
427
  case 'skipped':
408
- log(upgrade.error
409
- ? 'Skipped upgrade (network unavailable)'
410
- : 'Skipped upgrade check (dev mode)');
428
+ log(upgrade.error === 'Automatic package upgrades disabled by autoOnStart'
429
+ ? 'Skipped package upgrade (autoOnStart=false)'
430
+ : upgrade.error
431
+ ? 'Skipped upgrade (network unavailable)'
432
+ : 'Skipped upgrade check (dev mode)');
411
433
  break;
412
434
  case 'failed':
413
435
  log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
@@ -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),
@@ -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',
@@ -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
  }
@@ -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) });
@@ -42,7 +42,7 @@ import { isUnsupportedBaseagentConfigField } from '../../config/config-field-pol
42
42
  import { splitConfigBatchGetArgv } from '../../cli/cli-argv.js';
43
43
  import { chatmodeFieldForPeer, isPrivateChatmodeRoleLocked, resolveChatModeForField } from '../message/peer-mode.js';
44
44
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
45
- import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
45
+ import { isAgentFullAccessOwner, resolveRuntimePermissionMode, resolveRuntimeStringField, sessionFullAccessStatus, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
46
46
  import { isManagementRole } from '../../config/builtin-roles.js';
47
47
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
48
48
  import { logger } from '../../utils/logger.js';
@@ -2149,6 +2149,21 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2149
2149
  return menuResultFailure(error);
2150
2150
  }
2151
2151
  }
2152
+ if (cmdBase === '/session' && args?.view === 'fullaccess') {
2153
+ if (Object.keys(args).some(key => key !== 'view' && key !== 'context')) {
2154
+ return { error: 'session.fullaccess query only accepts args.view', code: 'INVALID_ARGUMENT' };
2155
+ }
2156
+ if (!session || !subject.selfAid)
2157
+ return { error: '当前没有可绑定的 EC 会话', code: 'NO_ACTIVE_SESSION' };
2158
+ if (session.chatType !== 'private')
2159
+ return { error: '/fa 仅支持私聊中的当前 EC 会话', code: 'NOT_SUPPORTED' };
2160
+ const principalId = subject.principalId;
2161
+ if (!principalId || !subject.isDaemonOwner || !isAgentFullAccessOwner(subject.selfAid, principalId)) {
2162
+ return { error: '/fa 需要同一身份同时具备 Agent Owner 和 daemon Owner 权限', code: 'FORBIDDEN' };
2163
+ }
2164
+ const status = sessionFullAccessStatus(session, principalId);
2165
+ return { data: { ...status, sessionId: session.id, scope: isTopicSession(session) ? 'session' : 'agent' } };
2166
+ }
2152
2167
  const storageRequest = { type: 'menu.query', cmd, args };
2153
2168
  if (isAgentStorageMenuRequest(storageRequest)) {
2154
2169
  try {
@@ -2948,6 +2963,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2948
2963
  if (!cmdBase)
2949
2964
  return { error: '缺少命令', code: 'MISSING_CMD' };
2950
2965
  const isLoopUpdate = cmdBase === '/session' && args?.resource === 'loop';
2966
+ const isFullAccessUpdate = cmdBase === '/session' && args?.resource === 'fullaccess';
2951
2967
  if (isLoopUpdate) {
2952
2968
  if (typeof value !== 'boolean' || Object.keys(args ?? {}).some(key => key !== 'resource' && key !== 'idleSeconds' && key !== 'context')) {
2953
2969
  return { error: 'session.loop requires a boolean value and only accepts resource / idleSeconds', code: 'INVALID_ARGUMENT' };
@@ -2959,9 +2975,13 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2959
2975
  return { error: `idleSeconds 必须是 ${MIN_SESSION_LOOP_SECONDS}-${MAX_SESSION_LOOP_SECONDS} 的整数`, code: 'INVALID_ARGUMENT' };
2960
2976
  }
2961
2977
  }
2962
- else if (typeof value !== 'string') {
2978
+ else if (!isFullAccessUpdate && typeof value !== 'string') {
2963
2979
  return { error: 'value 必须是字符串', code: 'INVALID_ARGUMENT' };
2964
2980
  }
2981
+ if (isFullAccessUpdate && (typeof value !== 'boolean'
2982
+ || Object.keys(args ?? {}).some(key => key !== 'resource' && key !== 'context'))) {
2983
+ return { error: 'session.fullaccess requires a boolean value and only accepts resource', code: 'INVALID_ARGUMENT' };
2984
+ }
2965
2985
  if (cmdBase === '/perm') {
2966
2986
  return { error: 'permissionMode 只能通过角色策略编辑器修改', code: 'ROLE_POLICY_MANAGED' };
2967
2987
  }
@@ -3020,6 +3040,53 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
3020
3040
  return menuResultFailure(error);
3021
3041
  }
3022
3042
  }
3043
+ if (isFullAccessUpdate) {
3044
+ if (!session || !subject.selfAid)
3045
+ return { error: '当前没有可绑定的 EC 会话', code: 'NO_ACTIVE_SESSION' };
3046
+ const route = { channel, channelId, selfAid: subject.selfAid, threadId: transportReplyContext?.threadId };
3047
+ let target;
3048
+ try {
3049
+ target = await resolveCurrentSessionLoopTarget(this.sessionManager, route);
3050
+ if (target.session.id !== session.id)
3051
+ return { error: '当前会话已发生变化,请重新查询', code: 'CONFLICT' };
3052
+ target = await refreshSessionLoopTarget(this.sessionManager, route, target);
3053
+ session = target.session;
3054
+ }
3055
+ catch (error) {
3056
+ return menuResultFailure(error);
3057
+ }
3058
+ const principalId = subject.principalId;
3059
+ if (session.chatType !== 'private' || !principalId || !subject.isDaemonOwner
3060
+ || !isAgentFullAccessOwner(subject.selfAid, principalId)) {
3061
+ return { error: '/fa 需要私聊中同一身份同时具备 Agent Owner 和 daemon Owner 权限', code: 'FORBIDDEN' };
3062
+ }
3063
+ try {
3064
+ if (isTopicSession(session)) {
3065
+ this.sessionManager.updateSessionFullAccess(session.id, { enabled: value, authorizedBy: principalId });
3066
+ }
3067
+ else {
3068
+ const config = cfgRead(ConfigTarget.Agent, { self: subject.selfAid }, { cache: false });
3069
+ if (!config)
3070
+ return { error: '找不到当前 Agent 配置', code: 'NOT_FOUND' };
3071
+ config.fullaccess = { enabled: value };
3072
+ cfgWrite(ConfigTarget.Agent, config, { self: subject.selfAid });
3073
+ }
3074
+ auditFullAccessEvent({
3075
+ event: value ? 'fullaccess.session-policy.configured' : 'fullaccess.session-policy.deescalated',
3076
+ source: 'session-policy',
3077
+ actorId: principalId,
3078
+ processRole: 'daemon-owner',
3079
+ agentAid: subject.selfAid,
3080
+ sessionId: session.id,
3081
+ authorizedBy: principalId,
3082
+ reason: isTopicSession(session) ? 'topic session fullaccess updated via Menu' : 'Agent main-session fullaccess updated via Menu',
3083
+ });
3084
+ return { data: { enabled: value, sessionId: session.id, scope: isTopicSession(session) ? 'session' : 'agent' } };
3085
+ }
3086
+ catch (error) {
3087
+ return menuResultFailure(error);
3088
+ }
3089
+ }
3023
3090
  const arg = value.trim();
3024
3091
  if (!arg)
3025
3092
  return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
@@ -4325,6 +4392,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4325
4392
  sessionId: sessionKey,
4326
4393
  reason: 'stop',
4327
4394
  agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
4395
+ agentAid: session.selfAID,
4328
4396
  });
4329
4397
  await this.processor.interruptSession(sessionKey, 'stop');
4330
4398
  return { data: { action: 'stop', success: true } };
@@ -5156,7 +5224,7 @@ async function execMenuForSystemControl(payload, context) {
5156
5224
  }
5157
5225
  /** ECWeb 专用入口:只信任 IPC 信封中的服务端认证上下文,不信任 Menu payload 自报身份。 */
5158
5226
  export async function execMenuForEcweb(payload, trusted) {
5159
- await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS });
5227
+ await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS, allowTransient: true });
5160
5228
  const id = payload?.id ?? '';
5161
5229
  const name = payload?.name;
5162
5230
  const validationError = validateMenuRequest(payload ?? {});
@@ -5200,7 +5268,7 @@ export async function execMenuForEcweb(payload, trusted) {
5200
5268
  * process authorization path. The authenticated envelope peerId remains the
5201
5269
  * requester; token mode maps to a configured owner as its management actor. */
5202
5270
  export async function execMenuForControl(payload, peerId, auth = {}) {
5203
- await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS });
5271
+ await refreshOwnershipForAuthorization(undefined, { maxAgeMs: OWNERSHIP_CACHE_MAX_AGE_MS, allowTransient: true });
5204
5272
  const id = payload?.id ?? '';
5205
5273
  const name = payload?.name;
5206
5274
  const validationError = validateMenuRequest(payload ?? {});
@@ -367,14 +367,33 @@ export function validateMenuRequest(request) {
367
367
  return { code: 'INVALID_ARGUMENT', message: 'idleSeconds is not allowed when session.loop is disabled' };
368
368
  }
369
369
  }
370
- else if (type === 'menu.update' && (typeof request.value !== 'string' || request.value.length === 0)) {
370
+ else if (type === 'menu.update'
371
+ && !((request.name === 'session' || request.cmd === '/session')
372
+ && request.args?.resource === 'fullaccess')
373
+ && (typeof request.value !== 'string' || request.value.length === 0)) {
371
374
  return { code: 'INVALID_ARGUMENT', message: 'Menu request value is required' };
372
375
  }
376
+ const isSessionFullAccessUpdate = type === 'menu.update'
377
+ && (request.name === 'session' || request.cmd === '/session')
378
+ && request.args?.resource === 'fullaccess';
379
+ if (isSessionFullAccessUpdate) {
380
+ if (typeof request.value !== 'boolean')
381
+ return { code: 'INVALID_ARGUMENT', message: 'session.fullaccess value must be a boolean' };
382
+ const args = request.args;
383
+ const unknown = Object.keys(args ?? {}).find(key => !['resource', 'context'].includes(key));
384
+ if (unknown)
385
+ return { code: 'INVALID_ARGUMENT', message: 'session.fullaccess update does not accept args.' + unknown };
386
+ }
373
387
  if (type === 'menu.query' && (request.name === 'session' || request.cmd === '/session')
374
388
  && request.args?.view === 'loop'
375
389
  && Object.keys(request.args).some(key => key !== 'view' && key !== 'context')) {
376
390
  return { code: 'INVALID_ARGUMENT', message: 'session.loop query only accepts args.view' };
377
391
  }
392
+ if (type === 'menu.query' && (request.name === 'session' || request.cmd === '/session')
393
+ && request.args?.view === 'fullaccess'
394
+ && Object.keys(request.args).some(key => key !== 'view' && key !== 'context')) {
395
+ return { code: 'INVALID_ARGUMENT', message: 'session.fullaccess query only accepts args.view' };
396
+ }
378
397
  if (type === 'menu.action' && (typeof request.action !== 'string' || request.action.trim().length === 0)) {
379
398
  return { code: 'INVALID_ARGUMENT', message: 'Menu request action is required' };
380
399
  }
@@ -43,7 +43,7 @@ export function probeSessionActivity(evidence, probes) {
43
43
  return evaluateSessionActivity(collected);
44
44
  }
45
45
  // 支持的命令列表
46
- const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/renew', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mention', '/loop', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair', '/memory', '/skill', '/thread'];
46
+ const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/renew', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mention', '/loop', '/fa', '/fullaccess', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair', '/memory', '/skill', '/thread'];
47
47
  const deprecatedCommands = ['/clear'];
48
48
  commands.push('/diag');
49
49
  const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop', '/pair', '/diag']);
@@ -55,7 +55,14 @@ export function parseFullAccessCommand(content) {
55
55
  return {
56
56
  matched: true,
57
57
  command: match[1],
58
- prompt: match[2].trim(),
58
+ action: match[2].trim() === ''
59
+ ? 'status'
60
+ : match[2].trim() === 'on'
61
+ ? 'on'
62
+ : match[2].trim() === 'off'
63
+ ? 'off'
64
+ : 'invalid',
65
+ raw: match[2].trim(),
59
66
  };
60
67
  }
61
68
  // 命令别名映射
@@ -78,6 +85,13 @@ export function guardGroupSlashCommand(content, chatType) {
78
85
  return undefined;
79
86
  const [inputCommand, ...args] = content.trim().split(/\s+/);
80
87
  const command = aliases[inputCommand] ?? inputCommand;
88
+ if (command === '/fa' || command === '/fullaccess') {
89
+ return {
90
+ kind: 'command.error',
91
+ text: '❌ 群聊中不支持 /fa,请在私聊会话操作',
92
+ reason: 'GROUP_COMMAND_UNSUPPORTED',
93
+ };
94
+ }
81
95
  if (GROUP_READ_ONLY_COMMANDS.has(command)) {
82
96
  if (!args.length || (command === '/model' && args.length === 1 && ['current', 'list', 'check'].includes(args[0]))) {
83
97
  return undefined;
@@ -97,7 +111,7 @@ export function guardGroupSlashCommand(content, chatType) {
97
111
  };
98
112
  }
99
113
  // 命令快速路径前缀(所有命令都不进入消息队列)
100
- const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mention', '/loop', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair', '/memory', '/skill', '/thread'];
114
+ const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mention', '/loop', '/fa', '/fullaccess', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade', '/pair', '/memory', '/skill', '/thread'];
101
115
  quickCommandPrefixes.push('/diag');
102
116
  /**
103
117
  * 计算两个字符串的 Levenshtein 距离(编辑距离)