evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -5,6 +5,8 @@ import { agentConfig } from '../paths.js';
5
5
  import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
6
  import { ContactMutationError } from './contact-book-store.js';
7
7
  import { addContact, mutateContactWithOperation } from './contact-request-service.js';
8
+ import { expirePendingContactRequests, reviewContactRequest } from './contact-request-service.js';
9
+ import { listStaticAgentOwners } from './peer-role-resolver.js';
8
10
  export function resolveContactCommand(argv) {
9
11
  const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
10
12
  const selfAid = String(argValue(args, '--self') || '').trim();
@@ -15,12 +17,41 @@ export function resolveContactCommand(argv) {
15
17
  if (extra.length > 0)
16
18
  return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
17
19
  if (command === 'list') {
18
- if (subcommand !== undefined || !args.includes('--blocked')) {
19
- return invalid('INVALID_ARGUMENT', 'Only ec contact list --blocked is currently supported');
20
+ if (subcommand !== undefined) {
21
+ return invalid('INVALID_ARGUMENT', 'contact list does not accept a positional argument');
20
22
  }
23
+ const filter = args.includes('--blocked')
24
+ ? 'blocked'
25
+ : args.includes('--pending')
26
+ ? 'pending'
27
+ : args.includes('--all') ? 'all' : undefined;
28
+ if (!filter)
29
+ return invalid('INVALID_ARGUMENT', 'contact list requires --blocked, --pending, or --all');
21
30
  return {
22
31
  ok: true,
23
- command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
32
+ command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, filter, canonicalArgv: canonicalArgv(argv) },
33
+ };
34
+ }
35
+ if (command === 'approve' || command === 'reject' || command === 'block-request') {
36
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
37
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
38
+ }
39
+ const requestId = String(argValue(args, '--request-id') || '').trim();
40
+ if (!requestId)
41
+ return invalid('INVALID_ARGUMENT', `${command} requires --request-id <request-id>`);
42
+ const expectedContactRevision = argValue(args, '--contact-revision')?.trim() || undefined;
43
+ return {
44
+ ok: true,
45
+ command: {
46
+ kind: 'review-request',
47
+ operationId: 'contact.review',
48
+ selfAid,
49
+ primaryId: subcommand,
50
+ requestId,
51
+ decision: command === 'approve' ? 'approve' : command === 'reject' ? 'reject' : 'block',
52
+ expectedContactRevision,
53
+ canonicalArgv: canonicalArgv(argv),
54
+ },
24
55
  };
25
56
  }
26
57
  if (command === 'block' || command === 'unblock') {
@@ -62,13 +93,63 @@ export async function executeResolvedContactCommand(command, actor = 'local-cli'
62
93
  }
63
94
  try {
64
95
  if (command.kind === 'list-blocked') {
96
+ await expirePendingContactRequests(command.selfAid);
65
97
  const snapshot = getContactSnapshot(command.selfAid);
98
+ const ids = command.filter === 'blocked'
99
+ ? [...snapshot.blockedIndex]
100
+ : [...snapshot.contacts.entries()]
101
+ .filter(([, entry]) => command.filter === 'all' || entry.status === 'pending')
102
+ .map(([id]) => id);
103
+ const items = ids.sort().map(id => contactItem(command.selfAid, id));
104
+ if (command.filter === 'blocked') {
105
+ return {
106
+ ok: true,
107
+ operation: 'list-blocked',
108
+ selfAid: command.selfAid,
109
+ contactRevision: snapshot.contactRevision,
110
+ items,
111
+ };
112
+ }
66
113
  return {
67
114
  ok: true,
68
- operation: 'list-blocked',
115
+ operation: 'list-contacts',
69
116
  selfAid: command.selfAid,
117
+ filter: command.filter,
70
118
  contactRevision: snapshot.contactRevision,
71
- items: [...snapshot.blockedIndex].sort().map(id => resolveContactView(command.selfAid, id)),
119
+ items,
120
+ };
121
+ }
122
+ if (command.kind === 'review-request') {
123
+ const expectedContactRevision = command.expectedContactRevision ?? getContactSnapshot(command.selfAid).contactRevision;
124
+ const reviewerAid = actor === 'local-cli'
125
+ ? listStaticAgentOwners(command.selfAid)[0]
126
+ : actor;
127
+ if (!reviewerAid) {
128
+ return { ok: false, code: 'NO_OWNER', error: `Agent has no owner: ${command.selfAid}` };
129
+ }
130
+ const reviewed = await reviewContactRequest({
131
+ selfAid: command.selfAid,
132
+ primaryId: command.primaryId,
133
+ requestId: command.requestId,
134
+ expectedContactRevision,
135
+ approverId: reviewerAid,
136
+ actorId: reviewerAid,
137
+ decision: command.decision,
138
+ });
139
+ if (reviewed.code !== 'approved' && reviewed.code !== 'rejected' && reviewed.code !== 'blocked') {
140
+ const code = reviewed.code === 'request-expired' ? 'REQUEST_EXPIRED'
141
+ : reviewed.code === 'revision-conflict' ? 'CONFLICT'
142
+ : reviewed.code === 'forbidden' ? 'PERMISSION_DENIED' : 'REQUEST_STALE';
143
+ return { ok: false, code, error: reviewed.code, data: { contactRevision: reviewed.contactRevision } };
144
+ }
145
+ return {
146
+ ok: true,
147
+ operation: 'review-request',
148
+ selfAid: command.selfAid,
149
+ primaryId: command.primaryId,
150
+ requestId: command.requestId,
151
+ result: reviewed.code,
152
+ contactRevision: reviewed.contactRevision,
72
153
  };
73
154
  }
74
155
  if (command.kind === 'set-blocked') {
@@ -129,16 +210,24 @@ function stripOptions(args) {
129
210
  const result = [];
130
211
  for (let index = 0; index < args.length; index += 1) {
131
212
  const arg = args[index];
132
- if (arg === '--self' || arg === '--format') {
213
+ if (arg === '--self' || arg === '--format' || arg === '--request-id' || arg === '--contact-revision') {
133
214
  index += 1;
134
215
  continue;
135
216
  }
136
- if (arg === '--blocked')
217
+ if (arg === '--blocked' || arg === '--pending' || arg === '--all')
137
218
  continue;
138
219
  result.push(arg);
139
220
  }
140
221
  return result;
141
222
  }
223
+ function contactItem(selfAid, primaryId) {
224
+ const view = resolveContactView(selfAid, primaryId);
225
+ const entry = getContactSnapshot(selfAid).contacts.get(primaryId);
226
+ return {
227
+ ...view,
228
+ ...(entry?.pendingRequest ? { pendingRequest: { ...entry.pendingRequest } } : {}),
229
+ };
230
+ }
142
231
  function canonicalArgv(argv) {
143
232
  return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
144
233
  }
@@ -2,6 +2,7 @@ import { normalizeCliArgv } from '../cli/cli-argv.js';
2
2
  import { parseConfigSelector } from '../cli/config-selector.js';
3
3
  import { parseConfigFieldValue, resolveConfigFieldRule, } from './config-field-policy.js';
4
4
  import { ConfigError, routeFieldPath } from './config-manager.js';
5
+ import { isValidAid } from '../aun/aid/validation.js';
5
6
  const SELECTOR_OPTIONS = {
6
7
  '--self': 'value',
7
8
  '--peer': 'value',
@@ -96,11 +97,12 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
96
97
  reason: 'permissionMode is defined by roles; use the role policy editor',
97
98
  };
98
99
  }
99
- if (field === 'role') {
100
+ const isRelationRole = field === 'role';
101
+ if (isRelationRole && selector.scope !== 'relation') {
100
102
  return {
101
103
  ok: false,
102
- code: 'ROLE_POLICY_MANAGED',
103
- reason: 'Relation role assignments are managed through the role assignment service',
104
+ code: 'RELATION_SELECTOR_REQUIRED',
105
+ reason: `config ${subcommand} role needs a relation target: --self <aid> --peer <peer-aid>`,
104
106
  };
105
107
  }
106
108
  if (subcommand !== 'get' && (field === 'roles' || field.startsWith('roles.'))) {
@@ -128,12 +130,14 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
128
130
  reason: `Config field ${field} does not support object writes`,
129
131
  };
130
132
  }
131
- const commandScope = selector.scope === 'defaults' ? 'process' : selector.scope;
133
+ const commandScope = isRelationRole ? 'agent' : selector.scope === 'defaults' ? 'process' : selector.scope;
132
134
  const isSafe = fieldRule.class === 'safe-scalar' || fieldRule.class === 'safe-readonly-object';
133
135
  const isManagementOperation = commandScope === 'process' || !isSafe;
134
- const operationId = isManagementOperation
135
- ? subcommand === 'get' ? 'config.read' : 'config.write'
136
- : `config.${subcommand}`;
136
+ const operationId = isRelationRole
137
+ ? subcommand === 'get' ? 'role.relation.read' : subcommand === 'set' ? 'role.assign' : 'role.revoke'
138
+ : isManagementOperation
139
+ ? subcommand === 'get' ? 'config.read' : 'config.write'
140
+ : `config.${subcommand}`;
137
141
  let route;
138
142
  const routeRequired = subcommand !== 'get' || !isManagementOperation;
139
143
  try {
@@ -166,7 +170,30 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
166
170
  const rawValue = subcommand === 'set' ? business.values[1] : undefined;
167
171
  let value;
168
172
  if (subcommand === 'set') {
169
- if (fieldRule.class === 'safe-scalar') {
173
+ if (field === 'owners') {
174
+ const parsedOwners = parseOwnersValue(rawValue);
175
+ if (!parsedOwners.ok)
176
+ return parsedOwners;
177
+ value = parsedOwners.value;
178
+ }
179
+ else if (isRelationRole) {
180
+ const raw = rawValue.trim();
181
+ if (!raw)
182
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role cannot be empty' };
183
+ try {
184
+ const parsed = JSON.parse(raw);
185
+ if (typeof parsed === 'string')
186
+ value = parsed;
187
+ else if (parsed === null)
188
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'config set role requires a role ID; use config unset role to revoke it' };
189
+ else
190
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'Config field role must be a role ID or null' };
191
+ }
192
+ catch {
193
+ value = raw;
194
+ }
195
+ }
196
+ else if (fieldRule.class === 'safe-scalar') {
170
197
  const parsedValue = parseConfigFieldValue(field, rawValue);
171
198
  if (!parsedValue.ok)
172
199
  return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: parsedValue.reason };
@@ -486,6 +513,37 @@ function coerceConfigValue(raw, route) {
486
513
  return Number(raw);
487
514
  return raw;
488
515
  }
516
+ function parseOwnersValue(raw) {
517
+ let values;
518
+ const trimmed = raw.trim();
519
+ if (trimmed.startsWith('[')) {
520
+ try {
521
+ values = JSON.parse(trimmed);
522
+ }
523
+ catch {
524
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'owners must be a valid JSON array of AIDs' };
525
+ }
526
+ if (!Array.isArray(values)) {
527
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'owners must be a JSON array of AIDs' };
528
+ }
529
+ }
530
+ else {
531
+ values = [raw];
532
+ }
533
+ if (!Array.isArray(values) || values.length === 0) {
534
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: 'owners must contain at least one AID' };
535
+ }
536
+ const owners = [];
537
+ for (const value of values) {
538
+ if (typeof value !== 'string' || !value.trim() || !isValidAid(value.trim())) {
539
+ return { ok: false, code: 'INVALID_CONFIG_VALUE', reason: `Invalid owner AID: ${JSON.stringify(value)}` };
540
+ }
541
+ const owner = value.trim();
542
+ if (!owners.includes(owner))
543
+ owners.push(owner);
544
+ }
545
+ return { ok: true, value: owners };
546
+ }
489
547
  function configErrorResult(error) {
490
548
  if (error instanceof ConfigError)
491
549
  return { ok: false, code: error.code, reason: error.message };
@@ -85,13 +85,14 @@ export class AgentApplicationService {
85
85
  unique_peer_count: stats?.uniquePeerCount ?? 0,
86
86
  }
87
87
  : null;
88
- const config = agent.config;
88
+ const config = (loadAgent(aid) ?? agent.config);
89
89
  const identity = readAgentMdIdentity(aid);
90
90
  const paths = resolvePaths();
91
91
  return {
92
92
  ok: true,
93
93
  aid,
94
94
  status: info.status || 'stopped',
95
+ lifecycle: config?.lifecycle ?? null,
95
96
  identity,
96
97
  config: {
97
98
  baseagent: info.baseagent || null,
@@ -333,23 +333,31 @@ function buildAuditRecord(event) {
333
333
  : executed
334
334
  ? (event.exitCode === undefined || event.exitCode === 0 ? 'completed' : 'failed')
335
335
  : 'authorized');
336
- const lifecycleId = event.callId ?? event.correlationId ?? event.requestId;
336
+ const ecwebRequestId = event.source === 'ecweb' ? event.ecwebRequestId : undefined;
337
+ const requestId = event.requestId ?? ecwebRequestId;
338
+ const correlationId = event.correlationId ?? requestId;
339
+ const lifecycleId = event.callId ?? correlationId ?? requestId;
337
340
  return {
338
341
  ts: event.ts,
339
- eventKey: event.eventKey ?? (lifecycleId
340
- ? buildAuthorizationEventKey({
341
- sessionId: event.sessionId,
342
- callId: event.callId,
343
- correlationId: event.correlationId,
344
- requestId: event.requestId,
345
- operation: event.operation,
346
- })
347
- : undefined),
342
+ eventKey: event.eventKey ?? (ecwebRequestId
343
+ ? `ecweb:${crypto.createHash('sha256').update(JSON.stringify([
344
+ ecwebRequestId, event.targetAgentAid ?? event.selfAid, event.operation,
345
+ ])).digest('hex').slice(0, 20)}`
346
+ : lifecycleId
347
+ ? buildAuthorizationEventKey({
348
+ sessionId: event.sessionId,
349
+ callId: event.callId,
350
+ correlationId,
351
+ requestId,
352
+ operation: event.operation,
353
+ })
354
+ : undefined),
348
355
  eventPhase: event.eventPhase,
349
356
  callId: event.callId,
350
- correlationId: event.correlationId ?? event.requestId,
351
- requestId: event.requestId,
357
+ correlationId,
358
+ requestId,
352
359
  sessionId: event.sessionId,
360
+ ...(event.source === 'ecweb' && !event.sessionId ? { sessionScope: 'not_applicable' } : {}),
353
361
  agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
354
362
  agentName: resolveAgentName(event),
355
363
  permissionMode: normalizeAuditPermissionMode(event.permissionMode),
@@ -13,6 +13,7 @@ const DAEMON_OWNER_PROCESS_OPERATIONS = new Set([
13
13
  'agent.disable',
14
14
  'system.restart',
15
15
  'system.upgrade',
16
+ 'system.paircode',
16
17
  'gateway.read',
17
18
  'gateway.write',
18
19
  'stats.peers',
@@ -192,7 +193,8 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
192
193
  };
193
194
  }
194
195
  // Any authenticated role may inspect the current Agent through the daemon's
195
- // redacted view. Cross-Agent inspection is handled by requiresDaemonOwner().
196
+ // redacted view. Management roles may inspect another Agent's redacted view;
197
+ // mutation and reload remain target-scoped below.
196
198
  if (operation === 'agent.show' && isCurrentAgentTarget(ctx)) {
197
199
  return {
198
200
  allow: true,
@@ -203,6 +205,25 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
203
205
  matchedRule: 'self-agent-read',
204
206
  };
205
207
  }
208
+ // Cross-Agent reads are a management/control-plane capability. A custom
209
+ // role must not widen this boundary merely by granting itself agent.show.
210
+ if (operation === 'agent.show'
211
+ && !isCurrentAgentTarget(ctx)
212
+ && !isManagementRole(role)
213
+ && !hasDaemonOwnerRole(ctx)) {
214
+ return denyDecision(ctx, 'NOT_ALLOWED', `${operation} may only target the current Agent from a managed Agent task.`, operation, intent.scope, opMeta.dangerous, undefined, 'TARGET_SELF_ONLY');
215
+ }
216
+ if (operation === 'agent.persona' && isCurrentAgentTarget(ctx)
217
+ && (role === 'owner' || hasDaemonOwnerRole(ctx))) {
218
+ return {
219
+ allow: true,
220
+ operation,
221
+ scope: intent.scope,
222
+ role,
223
+ dangerous: opMeta.dangerous,
224
+ matchedRule: role === 'owner' ? 'agent-owner-persona' : 'daemon-owner-persona',
225
+ };
226
+ }
206
227
  if (resolvedConfigCommand) {
207
228
  const targetCheck = checkResolvedConfigTarget(ctx, resolvedConfigCommand);
208
229
  if (!targetCheck.ok) {
@@ -215,7 +236,7 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
215
236
  if (!isManagementRole(role) && intent.scope !== 'relation') {
216
237
  return denyDecision(ctx, 'SCOPE_MISMATCH', `Role ${role} may only access relation-scoped config`, operation, intent.scope, opMeta.dangerous);
217
238
  }
218
- if (role === 'owner') {
239
+ if (role === 'owner' && isCurrentAgentTarget(ctx)) {
219
240
  return {
220
241
  allow: true,
221
242
  operation,
@@ -225,6 +246,22 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
225
246
  matchedRule: 'agent-owner',
226
247
  };
227
248
  }
249
+ // The daemon control identity is allowed to inspect any Agent config,
250
+ // including sensitive fields. This is intentionally handled before
251
+ // role-definition matching because daemon-owner subjects do not need an
252
+ // Agent relation role to read the daemon's local configuration store.
253
+ if (resolvedConfigCommand.kind === 'field'
254
+ && resolvedConfigCommand.subcommand === 'get'
255
+ && hasDaemonOwnerRole(ctx)) {
256
+ return {
257
+ allow: true,
258
+ operation,
259
+ scope: intent.scope,
260
+ role: 'daemon-owner',
261
+ dangerous: opMeta.dangerous,
262
+ matchedRule: 'daemon-owner-config-read',
263
+ };
264
+ }
228
265
  }
229
266
  const roleDef = getRoleDefinition(role, ctx.selfAid);
230
267
  if (!roleDef) {
@@ -357,10 +394,25 @@ function requiresDaemonOwner(ctx, resolvedConfigCommand) {
357
394
  || resolvedConfigCommand.configScope === 'process'
358
395
  || resolvedConfigCommand.configScope === 'defaults'))
359
396
  return true;
397
+ // Sensitive reads of another Agent's config are daemon-scoped. Agent
398
+ // owner/admin roles may inspect safe fields cross-Agent, but only the
399
+ // daemon control identity may access config.read targets that are not the
400
+ // current Agent.
401
+ if (operation === 'config.read'
402
+ && resolvedConfigCommand?.kind === 'field'
403
+ && resolvedConfigCommand.configScope === 'agent'
404
+ && !isCurrentAgentTarget(ctx)) {
405
+ return true;
406
+ }
360
407
  if (operation === 'agent.reload' && hasDaemonOwnerRole(ctx)) {
361
408
  return true;
362
409
  }
363
- if (operation === 'agent.show' || operation === 'agent.reload') {
410
+ // Avatar publication is an Agent-scoped mutation for Agent channels, but
411
+ // the trusted control plane may publish it for a selected Agent target.
412
+ if (operation === 'ec.aid.avatar' && hasDaemonOwnerRole(ctx)) {
413
+ return true;
414
+ }
415
+ if (operation === 'agent.reload') {
364
416
  return !isCurrentAgentTarget(ctx);
365
417
  }
366
418
  if (operation.startsWith('trigger.')) {
@@ -529,6 +581,12 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
529
581
  return { ok: false, reason: 'Resolved config operation is required' };
530
582
  const field = resolvedConfigOp.field;
531
583
  if (!resolvedConfigOp.route || (resolvedConfigOp.fieldRule.class !== 'safe-scalar' && resolvedConfigOp.fieldRule.class !== 'safe-readonly-object')) {
584
+ if (policy === 'behavior-read'
585
+ && !isCurrentAgentTarget(ctx)
586
+ && hasDaemonOwnerRole(ctx)
587
+ && resolvedConfigOp.route) {
588
+ return { ok: true };
589
+ }
532
590
  return { ok: false, reason: `Config field ${field} is not available to user roles` };
533
591
  }
534
592
  if (policy === 'role-overridable-write'
@@ -541,8 +599,12 @@ function checkConfigFieldPolicy(ctx, policy, resolvedConfigOp) {
541
599
  const fieldPermission = roleDef
542
600
  ? resolveRoleFieldPermission(roleDef.permissions || {}, field)
543
601
  : undefined;
544
- if (policy === 'behavior-read')
602
+ if (policy === 'behavior-read') {
603
+ // Only the daemon control identity may read sensitive config fields from
604
+ // another Agent. Agent owners retain unrestricted reads for their own
605
+ // target via the self-target fast path above.
545
606
  return { ok: true };
607
+ }
546
608
  if (fieldPermission && !fieldPermission.allowOverride) {
547
609
  return { ok: false, reason: `Role ${ctx.role} cannot override ${field}` };
548
610
  }
@@ -579,12 +641,19 @@ function checkResolvedConfigTarget(ctx, command) {
579
641
  return { ok: true };
580
642
  }
581
643
  if (command.configScope === 'agent') {
644
+ // Management roles may read another Agent's non-sensitive config fields;
645
+ // sensitive fields are rejected by the field policy. Writes stay self-only.
646
+ if (command.subcommand === 'get' && (isManagementRole(ctx.role) || hasDaemonOwnerRole(ctx))) {
647
+ return { ok: true };
648
+ }
582
649
  if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
583
650
  return { ok: false, reason: 'Only the current agent can be targeted' };
584
651
  }
585
652
  return { ok: true };
586
653
  }
587
- if (ctx.allowExplicitRelationTarget && ctx.role === 'owner') {
654
+ if (ctx.allowExplicitRelationTarget
655
+ && (ctx.role === 'owner' || ctx.role === 'admin')
656
+ && (command.kind === 'field' && command.operationId.startsWith('role.'))) {
588
657
  if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
589
658
  return { ok: false, reason: 'Only the current agent relation can be targeted' };
590
659
  }
@@ -748,10 +817,10 @@ function checkOwnPeer(ctx) {
748
817
  }
749
818
  if (ctx.actorId) {
750
819
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
751
- return { ok: false, reason: 'Only the actor peer can be targeted' };
820
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
752
821
  }
753
822
  if (requestedPeerKeyId && requestedPeerKeyId !== ctx.actorId) {
754
- return { ok: false, reason: 'Only the actor peer can be targeted' };
823
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerKeyId) };
755
824
  }
756
825
  }
757
826
  if (!expectedPeerKey)
@@ -769,10 +838,20 @@ function checkOwnPeer(ctx) {
769
838
  candidateKeys.add(`${legacyChannelType}::${argPeer}`);
770
839
  }
771
840
  if (!candidateKeys.has(expectedPeerKey)) {
772
- return { ok: false, reason: 'Only the actor peer can be targeted' };
841
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId ?? requestedPeerKeyId) };
773
842
  }
774
843
  return { ok: true };
775
844
  }
845
+ function ownPeerOnlyDenialReason(ctx, requestedPeer) {
846
+ if (ctx.intent.operation !== 'ec.msg.send' && ctx.intent.operation !== 'ec.msg.file') {
847
+ return 'Only the actor peer can be targeted';
848
+ }
849
+ const currentPeer = ctx.actorId || peerIdFromArg(ctx.peerKey || '') || '当前私聊对端';
850
+ const target = requestedPeer || '指定目标';
851
+ return `当前托管会话只能联系当前私聊对端(${currentPeer}),不能联系第三方目标(${target})。`
852
+ + ' 如果本次请求来自 handoff,请执行 ec handoff return <handoff-id> "<失败原因>"(已有命令)将失败原因反馈给原会话;'
853
+ + '否则请让目标先直接发起会话,或由 Agent owner 为当前角色配置允许 relay/第三方外联后再发送。';
854
+ }
776
855
  function peerIdFromArg(value) {
777
856
  try {
778
857
  return parsePeerKey(value).channelId;
@@ -100,6 +100,13 @@ const HANDOFF_WRITE_OPERATIONS = [{
100
100
  defaultScopes: ['relation', 'agent'],
101
101
  description: 'Return the current handoff result',
102
102
  sources: ['agent-tool'],
103
+ }, {
104
+ id: 'ec.handoff.recover',
105
+ category: 'write-agent',
106
+ dangerous: true,
107
+ defaultScopes: ['agent'],
108
+ description: 'Recover an open handoff after its target session changed',
109
+ sources: ['agent-tool'],
103
110
  }];
104
111
  const QUEUE_OPERATIONS = [['read', 'Read the current Agent message queue'], ['clear', 'Clear the current Agent message queue'], ['cancel', 'Cancel a current Agent queued message'], ['interrupt', 'Interrupt a current Agent session']].map(([kind, description]) => ({
105
112
  id: `ec.queue.${kind}`,
@@ -446,7 +453,7 @@ const OPERATIONS = [
446
453
  dangerous: false,
447
454
  defaultScopes: ['agent'],
448
455
  description: 'Assign a role to a private or group relation',
449
- sources: ['menu', 'ecweb', 'control'],
456
+ sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
450
457
  },
451
458
  {
452
459
  id: 'role.revoke',
@@ -454,7 +461,7 @@ const OPERATIONS = [
454
461
  dangerous: false,
455
462
  defaultScopes: ['agent'],
456
463
  description: 'Revoke a private or group relation role assignment',
457
- sources: ['menu', 'ecweb', 'control'],
464
+ sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
458
465
  },
459
466
  {
460
467
  id: 'role.policy.read',
@@ -470,7 +477,7 @@ const OPERATIONS = [
470
477
  dangerous: false,
471
478
  defaultScopes: ['agent'],
472
479
  description: 'Read private and group relation role assignments',
473
- sources: ['menu', 'ecweb', 'control'],
480
+ sources: ['menu', 'menu.cli', 'ecweb', 'control', 'agent-tool'],
474
481
  },
475
482
  {
476
483
  id: 'role.policy.write',
@@ -485,7 +492,7 @@ const OPERATIONS = [
485
492
  category: 'read',
486
493
  dangerous: false,
487
494
  defaultScopes: ['agent'],
488
- description: 'Read the Agent contact block list',
495
+ description: 'Read Agent contacts, pending requests, and blocked contacts',
489
496
  sources: ['agent-tool'],
490
497
  },
491
498
  {
@@ -504,6 +511,14 @@ const OPERATIONS = [
504
511
  description: 'Block or unblock a canonical contact across bound private channels',
505
512
  sources: ['agent-tool'],
506
513
  },
514
+ {
515
+ id: 'contact.review',
516
+ category: 'write-agent',
517
+ dangerous: false,
518
+ defaultScopes: ['agent'],
519
+ description: 'Review pending Agent contact requests',
520
+ sources: ['agent-tool'],
521
+ },
507
522
  {
508
523
  id: 'connect.read',
509
524
  category: 'read',
@@ -601,6 +616,14 @@ const OPERATIONS = [
601
616
  description: '获取 agent 配置(含凭证)',
602
617
  sources: ['menu', 'menu.cli', 'control'],
603
618
  },
619
+ {
620
+ id: 'agent.persona',
621
+ category: 'write-agent',
622
+ dangerous: false,
623
+ defaultScopes: ['agent'],
624
+ description: '读取或更新当前 Agent 的 persona.md',
625
+ sources: ['agent-tool', 'control'],
626
+ },
604
627
  {
605
628
  id: 'agent.create',
606
629
  category: 'process',
@@ -658,6 +681,14 @@ const OPERATIONS = [
658
681
  description: '查看系统状态(敏感信息)',
659
682
  sources: ['slash', 'menu', 'menu.cli', 'agent-tool'],
660
683
  },
684
+ {
685
+ id: 'system.paircode',
686
+ category: 'process',
687
+ dangerous: true,
688
+ defaultScopes: ['process'],
689
+ description: '获取 daemon 远程控制配对码',
690
+ sources: ['menu', 'control'],
691
+ },
661
692
  {
662
693
  id: 'activity.current',
663
694
  category: 'read',
@@ -944,6 +975,14 @@ const OPERATIONS = [
944
975
  description: 'Sign and publish the current Agent local agent.md',
945
976
  sources: ['agent-tool'],
946
977
  },
978
+ {
979
+ id: 'ec.aid.avatar',
980
+ category: 'write-agent',
981
+ dangerous: true,
982
+ defaultScopes: ['agent'],
983
+ description: 'Upload and publish the current Agent avatar',
984
+ sources: ['agent-tool', 'menu', 'menu.cli'],
985
+ },
947
986
  {
948
987
  id: 'ec.msg.send',
949
988
  category: 'write-own',
@@ -952,6 +991,14 @@ const OPERATIONS = [
952
991
  description: '通过 ec msg send 发送私聊消息',
953
992
  sources: ['agent-tool'],
954
993
  },
994
+ {
995
+ id: 'ec.menu.request',
996
+ category: 'write-agent',
997
+ dangerous: false,
998
+ defaultScopes: ['relation'],
999
+ description: 'Send an authenticated remote Control Menu request through the current Agent daemon channel',
1000
+ sources: ['agent-tool'],
1001
+ },
955
1002
  {
956
1003
  id: 'ec.msg.file',
957
1004
  category: 'write-own',