evolcore 0.0.25 → 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 (231) hide show
  1. package/CHANGELOG.md +607 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +160 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent.js +96 -46
  10. package/dist/agents/claude-runner.js +50 -12
  11. package/dist/agents/codex/app-server-client.js +13 -5
  12. package/dist/agents/codex/catalog.js +1 -1
  13. package/dist/agents/codex/diagnostics.js +34 -1
  14. package/dist/agents/codex-runner.js +121 -38
  15. package/dist/agents/ecagent-runner.js +4 -9
  16. package/dist/agents/gemini-runner.js +2 -7
  17. package/dist/agents/runner-types.js +25 -17
  18. package/dist/aun/aid/agentmd.js +81 -8
  19. package/dist/aun/aid/client.js +1 -1
  20. package/dist/aun/aid/identity.js +1 -1
  21. package/dist/aun/aid/index.js +1 -1
  22. package/dist/aun/aid/store.js +17 -13
  23. package/dist/aun/menu-client.js +74 -0
  24. package/dist/aun/msg/group.js +1 -1
  25. package/dist/aun/msg/managed-operation.js +1 -1
  26. package/dist/aun/msg/p2p.js +6 -1
  27. package/dist/aun/msg/upload.js +30 -22
  28. package/dist/channels/aun.js +329 -94
  29. package/dist/cli/agent-command.js +105 -84
  30. package/dist/cli/agent-create.js +112 -10
  31. package/dist/cli/agent.js +16 -5
  32. package/dist/cli/aun-commands.js +297 -296
  33. package/dist/cli/bench.js +20 -20
  34. package/dist/cli/config.js +47 -28
  35. package/dist/cli/contact.js +70 -13
  36. package/dist/cli/ctl-command.js +22 -22
  37. package/dist/cli/daemon-commands.js +105 -40
  38. package/dist/cli/diag.js +31 -0
  39. package/dist/cli/fs-command.js +67 -67
  40. package/dist/cli/handoff-command.js +41 -9
  41. package/dist/cli/index.js +83 -77
  42. package/dist/cli/link-rules.js +21 -21
  43. package/dist/cli/managed-command-guard.js +3 -0
  44. package/dist/cli/menu-client.js +46 -31
  45. package/dist/cli/net-check.js +22 -22
  46. package/dist/cli/queue-command.js +29 -29
  47. package/dist/cli/response.js +30 -30
  48. package/dist/cli/restart-monitor.js +87 -40
  49. package/dist/cli/stats.js +51 -51
  50. package/dist/cli/trigger-command.js +56 -56
  51. package/dist/config/builtin-roles.js +1 -0
  52. package/dist/config/config-operation-service.js +145 -0
  53. package/dist/config/contact-operation-service.js +96 -7
  54. package/dist/config/resolved-config-op.js +27 -7
  55. package/dist/core/agent-application-service.js +2 -1
  56. package/dist/core/auth/authorization-audit.js +20 -12
  57. package/dist/core/auth/operation-authorizer.js +25 -4
  58. package/dist/core/auth/operation-catalog.js +35 -4
  59. package/dist/core/auth/remote-access-store.js +40 -1
  60. package/dist/core/bootstrap-service.js +1 -1
  61. package/dist/core/command/command-handler.js +83 -26
  62. package/dist/core/command/diag-service.js +118 -0
  63. package/dist/core/command/menu-catalog.js +12 -3
  64. package/dist/core/command/menu-handler.js +47 -26
  65. package/dist/core/command/menu-protocol.js +5 -0
  66. package/dist/core/command/menu-token-store.js +41 -8
  67. package/dist/core/command/slash-gate.js +34 -4
  68. package/dist/core/command/slash-handler.js +86 -21
  69. package/dist/core/handoff/runtime.js +205 -27
  70. package/dist/core/handoff/store.js +193 -4
  71. package/dist/core/message/im-renderer.js +72 -21
  72. package/dist/core/message/message-bridge.js +69 -9
  73. package/dist/core/message/message-queue.js +26 -0
  74. package/dist/core/message/message-utils.js +1 -0
  75. package/dist/core/message/response-engine.js +47 -28
  76. package/dist/core/model/catalog-service.js +79 -49
  77. package/dist/core/model/context-window-suffix.js +33 -0
  78. package/dist/core/model/model-catalog.js +1 -1
  79. package/dist/core/model/model-permission.js +12 -3
  80. package/dist/core/permission/ec-command-parser.js +51 -12
  81. package/dist/core/permission/sandbox-runtime.js +36 -14
  82. package/dist/core/permission/tool-policy.js +2 -1
  83. package/dist/index.js +280 -163
  84. package/dist/ipc.js +54 -2
  85. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  86. package/dist/stats/billing.js +41 -15
  87. package/dist/stats/db.js +159 -159
  88. package/dist/stats/eck-vars.js +9 -9
  89. package/dist/stats/price-resolver.js +28 -10
  90. package/dist/stats/query.js +139 -139
  91. package/dist/stats/role-budget.js +6 -6
  92. package/dist/stats/writer.js +51 -51
  93. package/dist/utils/cross-platform.js +30 -1
  94. package/dist/utils/error-dict.json +160 -160
  95. package/dist/utils/error-utils.js +21 -0
  96. package/dist/utils/evolcore-version.js +129 -0
  97. package/dist/utils/logger.js +3 -1
  98. package/dist/utils/macos-autostart.js +22 -22
  99. package/dist/utils/model-prices.jsonl +23 -23
  100. package/dist/utils/npm-ops.js +176 -35
  101. package/dist/utils/welcome.js +22 -22
  102. package/ecagent/LICENSE +21 -21
  103. package/ecagent/NOTICE +11 -11
  104. package/ecagent/README.md +61 -61
  105. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  106. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  107. package/ecagent/dist/harness/messages.js +7 -7
  108. package/kits/docs/GUIDE.md +20 -20
  109. package/kits/docs/INDEX.md +66 -66
  110. package/kits/docs/aun/CHEATSHEET.md +19 -19
  111. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  112. package/kits/docs/channels/aun.md +56 -56
  113. package/kits/docs/channels/feishu.md +56 -56
  114. package/kits/docs/context-assembly.md +364 -364
  115. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  116. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  117. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  118. package/kits/docs/eck_templates/runtime.template.md +19 -19
  119. package/kits/docs/evolcore/INDEX.md +71 -71
  120. package/kits/docs/evolcore/agent-create.md +170 -165
  121. package/kits/docs/evolcore/agent.md +66 -66
  122. package/kits/docs/evolcore/aid.md +60 -60
  123. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  124. package/kits/docs/evolcore/config.md +202 -195
  125. package/kits/docs/evolcore/contact.md +75 -68
  126. package/kits/docs/evolcore/ctl.md +46 -46
  127. package/kits/docs/evolcore/event.md +216 -216
  128. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  129. package/kits/docs/evolcore/fs.md +110 -110
  130. package/kits/docs/evolcore/group-collaboration.md +243 -243
  131. package/kits/docs/evolcore/group-fs.md +17 -17
  132. package/kits/docs/evolcore/group-rules.md +251 -251
  133. package/kits/docs/evolcore/group.md +152 -152
  134. package/kits/docs/evolcore/menu-client.md +42 -34
  135. package/kits/docs/evolcore/model.md +50 -50
  136. package/kits/docs/evolcore/msg.md +166 -165
  137. package/kits/docs/evolcore/response.md +75 -75
  138. package/kits/docs/evolcore/rpc.md +37 -37
  139. package/kits/docs/evolcore/self-summary.md +29 -29
  140. package/kits/docs/evolcore/stats.md +83 -83
  141. package/kits/docs/evolcore/storage.md +50 -50
  142. package/kits/docs/evolcore/trigger.md +575 -575
  143. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  144. package/kits/docs/identity/PATH_OPS.md +16 -16
  145. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  146. package/kits/docs/identity/identity-tools.md +26 -26
  147. package/kits/docs/path-registry.md +62 -62
  148. package/kits/docs/prompt-loading-architecture.md +269 -269
  149. package/kits/docs/venues/aun-group.md +45 -45
  150. package/kits/docs/venues/aun-private.md +10 -10
  151. package/kits/docs/venues/client-desktop.md +10 -10
  152. package/kits/docs/venues/client-mobile.md +10 -10
  153. package/kits/docs/venues/feishu-group.md +13 -13
  154. package/kits/docs/venues/feishu-private.md +9 -9
  155. package/kits/docs/venues/group.md +37 -25
  156. package/kits/docs/venues/private.md +10 -10
  157. package/kits/eck_manifest.auxiliary.json +43 -43
  158. package/kits/eck_manifest.json +190 -190
  159. package/kits/eck_message_manifest.json +74 -74
  160. package/kits/migrations/README-role-config-v4.md +32 -32
  161. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  162. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  163. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  164. package/kits/migrations/rename-config-file.mjs +99 -99
  165. package/kits/rules/01-overview.md +162 -162
  166. package/kits/rules/02-navigation.md +96 -96
  167. package/kits/rules/03-identity.md +36 -36
  168. package/kits/rules/04-relation.md +75 -75
  169. package/kits/rules/05-venue.md +60 -60
  170. package/kits/rules/06-channel.md +63 -63
  171. package/kits/schemas/_meta.json +53 -53
  172. package/kits/schemas/agent-config.schema.1.json +177 -177
  173. package/kits/schemas/agent-config.schema.10.json +395 -395
  174. package/kits/schemas/agent-config.schema.11.json +421 -421
  175. package/kits/schemas/agent-config.schema.12.json +427 -427
  176. package/kits/schemas/agent-config.schema.2.json +239 -239
  177. package/kits/schemas/agent-config.schema.3.json +119 -119
  178. package/kits/schemas/agent-config.schema.4.json +208 -208
  179. package/kits/schemas/agent-config.schema.5.json +326 -326
  180. package/kits/schemas/agent-config.schema.6.json +322 -322
  181. package/kits/schemas/agent-config.schema.7.json +303 -303
  182. package/kits/schemas/agent-config.schema.8.json +304 -304
  183. package/kits/schemas/agent-config.schema.9.json +364 -364
  184. package/kits/schemas/contact-book.schema.1.json +36 -36
  185. package/kits/schemas/contact-book.schema.2.json +43 -43
  186. package/kits/schemas/contact-book.schema.3.json +67 -67
  187. package/kits/schemas/daemon.schema.1.json +91 -91
  188. package/kits/schemas/daemon.schema.2.json +101 -101
  189. package/kits/schemas/daemon.schema.3.json +123 -123
  190. package/kits/schemas/daemon.schema.4.json +136 -136
  191. package/kits/schemas/daemon.schema.5.json +135 -135
  192. package/kits/schemas/daemon.schema.6.json +131 -131
  193. package/kits/schemas/defaults.schema.1.json +81 -81
  194. package/kits/schemas/defaults.schema.2.json +85 -85
  195. package/kits/schemas/defaults.schema.3.json +73 -73
  196. package/kits/schemas/defaults.schema.4.json +107 -107
  197. package/kits/schemas/defaults.schema.5.json +119 -119
  198. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  199. package/kits/schemas/migrations/README.md +30 -30
  200. package/kits/schemas/relation-config.schema.1.json +158 -158
  201. package/kits/schemas/relation-config.schema.2.json +73 -73
  202. package/kits/schemas/relation-config.schema.3.json +50 -50
  203. package/kits/schemas/relation-config.schema.4.json +47 -47
  204. package/kits/schemas/relation-config.schema.5.json +47 -47
  205. package/kits/schemas/relation-config.schema.6.json +46 -46
  206. package/kits/schemas/relation-config.schema.7.json +59 -59
  207. package/kits/schemas/relation-config.schema.8.json +103 -103
  208. package/kits/schemas/role-config.schema.1.json +201 -201
  209. package/kits/schemas/role-registry.schema.1.json +35 -35
  210. package/kits/schemas/single-session.schema.1.json +31 -31
  211. package/kits/schemas/single-session.schema.2.json +57 -57
  212. package/kits/schemas/single-session.schema.3.json +32 -32
  213. package/kits/templates/bootstrap-welcome.md +17 -17
  214. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  215. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  216. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  217. package/kits/templates/message-fragments/inject-default.md +2 -2
  218. package/kits/templates/message-fragments/item.md +2 -2
  219. package/kits/templates/roles/admin.json +92 -92
  220. package/kits/templates/roles/member.json +96 -96
  221. package/kits/templates/roles/owner.json +9 -9
  222. package/kits/templates/roles/visitor.json +85 -85
  223. package/kits/templates/system-fragments/baseagent.md +14 -14
  224. package/kits/templates/system-fragments/bootstrap.md +31 -29
  225. package/kits/templates/system-fragments/channel.md +64 -64
  226. package/kits/templates/system-fragments/commands.md +28 -28
  227. package/kits/templates/system-fragments/identity.md +13 -13
  228. package/kits/templates/system-fragments/relation.md +19 -19
  229. package/kits/templates/system-fragments/session.md +59 -59
  230. package/kits/templates/system-fragments/venue.md +31 -31
  231. package/package.json +100 -98
@@ -5,6 +5,9 @@ import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, list
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { resolvePaths, agentDir, agentRelationConfig } from '../paths.js';
8
+ import { roleMenuQuery, roleMenuUpdate } from '../core/command/role-menu.js';
9
+ import { parsePeerKey } from '../core/relation/peer-identity.js';
10
+ import { isExplicitGroupId } from '../aun/group-identity.js';
8
11
  export const AUTHENTICATED_CONFIG_ROLE_ENV = 'EVOLCORE_AUTHENTICATED_CONFIG_ROLE';
9
12
  export function executeResolvedConfigCommand(command, options = {}) {
10
13
  try {
@@ -23,6 +26,134 @@ export function executeResolvedConfigCommand(command, options = {}) {
23
26
  return { ok: false, code: 'CONFIG_ERROR', error: error instanceof Error ? error.message : String(error) };
24
27
  }
25
28
  }
29
+ /**
30
+ * Execute a config command that may use an asynchronous domain service.
31
+ * Relation role assignments are intentionally handled here instead of by
32
+ * mutateConfig(), so Role Menu remains the single write path for validation,
33
+ * revisions, journaling, and audit.
34
+ */
35
+ export async function executeResolvedConfigCommandAsync(command, options = {}) {
36
+ try {
37
+ if (isRelationRoleCommand(command)) {
38
+ return await executeRelationRoleCommand(command, options);
39
+ }
40
+ return executeResolvedConfigCommand(command, options);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof ConfigError)
44
+ return { ok: false, code: error.code, error: error.message, ...(error.data !== undefined ? { data: error.data } : {}) };
45
+ const roleError = error;
46
+ return {
47
+ ok: false,
48
+ code: roleError?.code || 'CONFIG_ERROR',
49
+ error: roleError?.message || String(error),
50
+ ...(roleError?.data !== undefined ? { data: roleError.data } : {}),
51
+ };
52
+ }
53
+ }
54
+ function isRelationRoleCommand(command) {
55
+ return command.kind === 'field'
56
+ && command.field === 'role'
57
+ && command.configScope === 'relation'
58
+ && (command.operationId === 'role.relation.read'
59
+ || command.operationId === 'role.assign'
60
+ || command.operationId === 'role.revoke');
61
+ }
62
+ async function executeRelationRoleCommand(op, options) {
63
+ if (!op.self || !op.peerKey) {
64
+ return { ok: false, code: 'PEER_REQUIRED', error: 'role requires --self <aid> --peer <peer-aid>' };
65
+ }
66
+ const target = roleTargetFromPeerKey(op.peerKey);
67
+ const context = options.roleContext ?? {
68
+ self: op.self,
69
+ actorRole: options.role || 'owner',
70
+ actorRoleSource: 'local-cli',
71
+ isDaemonOwner: false,
72
+ fromControlChannel: false,
73
+ aunAvailable: true,
74
+ };
75
+ if (context.self !== op.self) {
76
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Role target does not match the receiving Agent' };
77
+ }
78
+ const targetArgs = { self: op.self, view: 'target', target };
79
+ const current = await roleMenuQuery(context, targetArgs);
80
+ const explicitRole = current?.assignment?.explicitRole
81
+ ?? current?.assignment?.explicitDefaultRole
82
+ ?? null;
83
+ const effectiveRole = current?.effectiveRole?.roleId
84
+ ?? current?.baselineRole?.roleId
85
+ ?? null;
86
+ const effectiveRoleSource = current?.effectiveRole?.source
87
+ ?? current?.baselineRole?.source
88
+ ?? 'none';
89
+ const roleDetails = { explicitRole, effectiveRole, effectiveRoleSource };
90
+ if (op.subcommand === 'get') {
91
+ return {
92
+ ok: true,
93
+ subcommand: 'get',
94
+ field: op.field,
95
+ value: explicitRole,
96
+ scope: op.configScope,
97
+ roleDetails,
98
+ };
99
+ }
100
+ const nextValue = op.subcommand === 'unset' ? null : op.value;
101
+ const updated = await roleMenuUpdate(context, {
102
+ self: op.self,
103
+ resource: 'assignment',
104
+ target,
105
+ expectedPolicyRevision: current.policyRevision,
106
+ expectedRelationRevision: current.relationRevision,
107
+ }, JSON.stringify(nextValue));
108
+ const updatedExplicitRole = updated?.assignment?.explicitRole
109
+ ?? updated?.assignment?.explicitDefaultRole
110
+ ?? null;
111
+ const updatedEffectiveRole = updated?.effectiveRole?.roleId
112
+ ?? updated?.baselineRole?.roleId
113
+ ?? null;
114
+ const updatedEffectiveRoleSource = updated?.effectiveRole?.source
115
+ ?? updated?.baselineRole?.source
116
+ ?? 'none';
117
+ const updatedDetails = {
118
+ explicitRole: updatedExplicitRole,
119
+ effectiveRole: updatedEffectiveRole,
120
+ effectiveRoleSource: updatedEffectiveRoleSource,
121
+ };
122
+ if (op.subcommand === 'unset') {
123
+ return {
124
+ ok: true,
125
+ subcommand: 'unset',
126
+ field: op.field,
127
+ removed: updated.changed === true,
128
+ scope: op.configScope,
129
+ roleDetails: updatedDetails,
130
+ };
131
+ }
132
+ return {
133
+ ok: true,
134
+ subcommand: 'set',
135
+ field: op.field,
136
+ value: updatedExplicitRole,
137
+ scope: op.configScope,
138
+ file: 'relation-config',
139
+ roleDetails: updatedDetails,
140
+ };
141
+ }
142
+ function roleTargetFromPeerKey(peerKey) {
143
+ let parsed;
144
+ try {
145
+ parsed = parsePeerKey(peerKey);
146
+ }
147
+ catch {
148
+ throw new ConfigError('INVALID_SELECTOR', `Invalid relation peer key: ${peerKey}`);
149
+ }
150
+ if (parsed.channelType !== 'aun') {
151
+ throw new ConfigError('NOT_SUPPORTED', 'Relation role assignments currently support AUN peers only');
152
+ }
153
+ return isExplicitGroupId(parsed.channelId)
154
+ ? { kind: 'group', groupId: parsed.channelId }
155
+ : { kind: 'private', peerAid: parsed.channelId };
156
+ }
26
157
  export function executeResolvedConfigOperation(op, options = {}) {
27
158
  try {
28
159
  const missingAgent = selectedAgentError(op);
@@ -298,6 +429,13 @@ function getWithSchemaDefault(op, absent, note) {
298
429
  return { ok: true, subcommand: 'get', field: op.field, value: absent, scope: op.configScope, ...nt };
299
430
  }
300
431
  function executeSet(op, options) {
432
+ if (op.field === 'role') {
433
+ return {
434
+ ok: false,
435
+ code: 'ROLE_POLICY_MANAGED',
436
+ error: 'Relation role assignments require the asynchronous role assignment service',
437
+ };
438
+ }
301
439
  if (!op.route)
302
440
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
303
441
  const selector = selectorFor(op, options);
@@ -341,6 +479,13 @@ function executeSet(op, options) {
341
479
  };
342
480
  }
343
481
  function executeUnset(op, options) {
482
+ if (op.field === 'role') {
483
+ return {
484
+ ok: false,
485
+ code: 'ROLE_POLICY_MANAGED',
486
+ error: 'Relation role assignments require the asynchronous role assignment service',
487
+ };
488
+ }
344
489
  if (!op.route)
345
490
  return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
346
491
  const selector = selectorFor(op, options);
@@ -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
  }
@@ -97,11 +97,12 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
97
97
  reason: 'permissionMode is defined by roles; use the role policy editor',
98
98
  };
99
99
  }
100
- if (field === 'role') {
100
+ const isRelationRole = field === 'role';
101
+ if (isRelationRole && selector.scope !== 'relation') {
101
102
  return {
102
103
  ok: false,
103
- code: 'ROLE_POLICY_MANAGED',
104
- 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>`,
105
106
  };
106
107
  }
107
108
  if (subcommand !== 'get' && (field === 'roles' || field.startsWith('roles.'))) {
@@ -129,12 +130,14 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
129
130
  reason: `Config field ${field} does not support object writes`,
130
131
  };
131
132
  }
132
- const commandScope = selector.scope === 'defaults' ? 'process' : selector.scope;
133
+ const commandScope = isRelationRole ? 'agent' : selector.scope === 'defaults' ? 'process' : selector.scope;
133
134
  const isSafe = fieldRule.class === 'safe-scalar' || fieldRule.class === 'safe-readonly-object';
134
135
  const isManagementOperation = commandScope === 'process' || !isSafe;
135
- const operationId = isManagementOperation
136
- ? subcommand === 'get' ? 'config.read' : 'config.write'
137
- : `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}`;
138
141
  let route;
139
142
  const routeRequired = subcommand !== 'get' || !isManagementOperation;
140
143
  try {
@@ -173,6 +176,23 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
173
176
  return parsedOwners;
174
177
  value = parsedOwners.value;
175
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
+ }
176
196
  else if (fieldRule.class === 'safe-scalar') {
177
197
  const parsedValue = parseConfigFieldValue(field, rawValue);
178
198
  if (!parsedValue.ok)
@@ -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',
@@ -204,6 +205,14 @@ function authorizeCommandInternal(ctx, resolvedConfigCommand) {
204
205
  matchedRule: 'self-agent-read',
205
206
  };
206
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
+ }
207
216
  if (operation === 'agent.persona' && isCurrentAgentTarget(ctx)
208
217
  && (role === 'owner' || hasDaemonOwnerRole(ctx))) {
209
218
  return {
@@ -642,7 +651,9 @@ function checkResolvedConfigTarget(ctx, command) {
642
651
  }
643
652
  return { ok: true };
644
653
  }
645
- 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.'))) {
646
657
  if (!ctx.selfAid || !command.self || command.self !== ctx.selfAid) {
647
658
  return { ok: false, reason: 'Only the current agent relation can be targeted' };
648
659
  }
@@ -806,10 +817,10 @@ function checkOwnPeer(ctx) {
806
817
  }
807
818
  if (ctx.actorId) {
808
819
  if (requestedPeerId && requestedPeerId !== ctx.actorId) {
809
- return { ok: false, reason: 'Only the actor peer can be targeted' };
820
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId) };
810
821
  }
811
822
  if (requestedPeerKeyId && requestedPeerKeyId !== ctx.actorId) {
812
- return { ok: false, reason: 'Only the actor peer can be targeted' };
823
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerKeyId) };
813
824
  }
814
825
  }
815
826
  if (!expectedPeerKey)
@@ -827,10 +838,20 @@ function checkOwnPeer(ctx) {
827
838
  candidateKeys.add(`${legacyChannelType}::${argPeer}`);
828
839
  }
829
840
  if (!candidateKeys.has(expectedPeerKey)) {
830
- return { ok: false, reason: 'Only the actor peer can be targeted' };
841
+ return { ok: false, reason: ownPeerOnlyDenialReason(ctx, requestedPeerId ?? requestedPeerKeyId) };
831
842
  }
832
843
  return { ok: true };
833
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
+ }
834
855
  function peerIdFromArg(value) {
835
856
  try {
836
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',
@@ -666,6 +681,14 @@ const OPERATIONS = [
666
681
  description: '查看系统状态(敏感信息)',
667
682
  sources: ['slash', 'menu', 'menu.cli', 'agent-tool'],
668
683
  },
684
+ {
685
+ id: 'system.paircode',
686
+ category: 'process',
687
+ dangerous: true,
688
+ defaultScopes: ['process'],
689
+ description: '获取 daemon 远程控制配对码',
690
+ sources: ['menu', 'control'],
691
+ },
669
692
  {
670
693
  id: 'activity.current',
671
694
  category: 'read',
@@ -968,6 +991,14 @@ const OPERATIONS = [
968
991
  description: '通过 ec msg send 发送私聊消息',
969
992
  sources: ['agent-tool'],
970
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
+ },
971
1002
  {
972
1003
  id: 'ec.msg.file',
973
1004
  category: 'write-own',
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { resolvePaths } from '../../paths.js';
5
5
  export const REMOTE_ACCESS_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
6
+ export const DIAGNOSTIC_TOKEN_TTL_MS = 60 * 60 * 1000;
6
7
  // 与 owner 绑定二维码(BindService)的默认有效期保持一致:10 分钟。
7
8
  export const REMOTE_ACCESS_PAIRING_TTL_MS = 10 * 60 * 1000;
8
9
  export function remoteAccessTokenStorePath() {
@@ -42,7 +43,8 @@ function pruneExpired(store, now) {
42
43
  const before = store.tokens.length;
43
44
  store.tokens = store.tokens.filter(record => typeof record?.token === 'string'
44
45
  && typeof record.lastActive === 'number'
45
- && now - record.lastActive < REMOTE_ACCESS_TOKEN_TTL_MS);
46
+ && Number.isFinite(record.lastActive)
47
+ && now - record.lastActive < (record.diagnostic ? DIAGNOSTIC_TOKEN_TTL_MS : REMOTE_ACCESS_TOKEN_TTL_MS));
46
48
  return store.tokens.length !== before;
47
49
  }
48
50
  function validLabel(value) {
@@ -85,6 +87,43 @@ export class RemoteAccessService {
85
87
  pairingCode = '';
86
88
  pairingExpiresAt = 0;
87
89
  pairingAuthorizedBy;
90
+ /** Synchronous reservation: concurrent invitations cannot issue two grants. */
91
+ issueDiagnosticToken(controlAid, agentAid, ownerAid, now = Date.now()) {
92
+ const store = loadStore();
93
+ pruneExpired(store, now);
94
+ if (store.tokens.some(record => record.diagnostic?.controlAid === controlAid)) {
95
+ throw Object.assign(new Error('本机已有有效诊断授权,请先执行 /diag stop 或 ec diag stop。'), { code: 'DIAG_ALREADY_ACTIVE' });
96
+ }
97
+ const record = {
98
+ token: crypto.randomBytes(32).toString('hex'), createdAt: now, lastActive: now,
99
+ label: 'diagnostic', authorizedBy: ownerAid,
100
+ diagnostic: { controlAid, agentAid, ownerAid, caseId: crypto.randomUUID() },
101
+ };
102
+ store.tokens.push(record);
103
+ saveStore(store);
104
+ return record;
105
+ }
106
+ getDiagnosticToken(controlAid, now = Date.now()) {
107
+ const store = loadStore();
108
+ if (pruneExpired(store, now))
109
+ saveStore(store);
110
+ return store.tokens.find(record => record.diagnostic?.controlAid === controlAid);
111
+ }
112
+ /** Expected token prevents a failed older send from revoking a newer grant. */
113
+ revokeDiagnosticToken(controlAid, expectedToken) {
114
+ const store = loadStore();
115
+ const pruned = pruneExpired(store, Date.now());
116
+ const index = store.tokens.findIndex(record => record.diagnostic?.controlAid === controlAid
117
+ && (expectedToken === undefined || record.token === expectedToken));
118
+ if (index < 0) {
119
+ if (pruned)
120
+ saveStore(store);
121
+ return undefined;
122
+ }
123
+ const [record] = store.tokens.splice(index, 1);
124
+ saveStore(store);
125
+ return record;
126
+ }
88
127
  getPairingCode(authorizedBy, now = Date.now()) {
89
128
  if (!this.pairingCode || now >= this.pairingExpiresAt) {
90
129
  this.pairingCode = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');