evolcore 0.0.35 → 0.0.36

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 (224) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +134 -48
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +109 -34
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -0,0 +1,687 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolvePaths } from '../../paths.js';
4
+ import { isValidAid } from '../../aun/aid/validation.js';
5
+ import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
6
+ import { configuredOwnerList as readConfiguredOwnerList } from '../../config/owner-config.js';
7
+ import { logger } from '../../utils/logger.js';
8
+ import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
9
+ export const OWNERSHIP_CACHE_MAX_AGE_MS = 30 * 60_000;
10
+ export const OWNERSHIP_AUTHORIZATION_MAX_AGE_MS = 5 * 60_000;
11
+ const OWNERSHIP_PENDING_REFRESH_MS = 5_000;
12
+ const CREATION_BIND_OPERATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13
+ /** Only use the task returned by this create's bind call. Never select a latest task. */
14
+ export function creationOwnershipBinding(task, subjectAid, ownerAid) {
15
+ if (!isValidAid(subjectAid) || !isValidAid(ownerAid)
16
+ || !task || typeof task !== 'object' || Array.isArray(task))
17
+ return undefined;
18
+ const t = task;
19
+ if (t.operation !== 'bind' || t.sub_aid !== subjectAid || t.owner_aid !== ownerAid
20
+ || t.new_owner_aid !== undefined || subjectAid === ownerAid
21
+ || typeof t.operation_id !== 'string' || !CREATION_BIND_OPERATION_ID.test(t.operation_id)
22
+ || t.base_relation_version !== 0 || t.target_relation_version !== 1
23
+ || (t.status !== 'pending' && t.status !== 'completed'))
24
+ return undefined;
25
+ return {
26
+ source: 'agent_create', operation: 'bind', operationId: t.operation_id,
27
+ subjectAid, ownerAid, baseRelationVersion: 0, targetRelationVersion: 1,
28
+ };
29
+ }
30
+ export class OwnershipError extends Error {
31
+ code;
32
+ constructor(code, message) {
33
+ super(message);
34
+ this.code = code;
35
+ this.name = 'OwnershipError';
36
+ }
37
+ }
38
+ export class OwnerResolver {
39
+ options;
40
+ subjects = new Map();
41
+ daemonAid;
42
+ /** Local owner configuration remains an independent authorization source during rollout. */
43
+ configuredOwners = new Map();
44
+ configuredOwnerFileRevision = new Map();
45
+ listeners = new Set();
46
+ constructor(options = {}) {
47
+ this.options = options;
48
+ }
49
+ setDaemonAid(aid) {
50
+ this.assertAid(aid);
51
+ this.daemonAid = aid;
52
+ }
53
+ getDaemonAid() {
54
+ return this.daemonAid;
55
+ }
56
+ setConfiguredOwners(subjectAid, owners) {
57
+ this.assertAid(subjectAid);
58
+ const normalized = [...new Set(owners
59
+ .map(owner => String(owner || '').trim())
60
+ .filter(owner => isValidAid(owner) && owner !== subjectAid))];
61
+ const previous = this.configuredOwners.get(subjectAid) ?? [];
62
+ if (JSON.stringify(previous) === JSON.stringify(normalized))
63
+ return;
64
+ if (normalized.length > 0)
65
+ this.configuredOwners.set(subjectAid, normalized);
66
+ else
67
+ this.configuredOwners.delete(subjectAid);
68
+ const subject = this.subjects.get(subjectAid);
69
+ if (subject) {
70
+ this.invalidate(subject, 'Configured Owners changed; fresh consensus is required');
71
+ this.schedule(subjectAid, subject);
72
+ }
73
+ }
74
+ setAgentConfiguredOwners(subjectAid, owners) {
75
+ this.assertAid(subjectAid);
76
+ if (subjectAid === this.daemonAid)
77
+ return;
78
+ this.setConfiguredOwners(subjectAid, owners);
79
+ }
80
+ configuredOwnerList(subjectAid) {
81
+ if (!subjectAid)
82
+ return [];
83
+ this.refreshConfiguredOwnersFromDisk(subjectAid);
84
+ return [...(this.configuredOwners.get(subjectAid) ?? [])];
85
+ }
86
+ isOwner(subjectAid, ownerAid) {
87
+ if (!subjectAid || !ownerAid)
88
+ return false;
89
+ return this.owners(subjectAid).includes(ownerAid);
90
+ }
91
+ assertLocalSubject(subjectAid) {
92
+ this.assertAid(subjectAid);
93
+ if (!this.subjects.get(subjectAid)?.client) {
94
+ throw new OwnershipError('SUBJECT_UNAVAILABLE', 'Subject must have local credentials and an existing SDK client');
95
+ }
96
+ }
97
+ onChange(listener) {
98
+ this.listeners.add(listener);
99
+ return () => { this.listeners.delete(listener); };
100
+ }
101
+ attachClient(subjectAid, client, connected = false) {
102
+ this.assertAid(subjectAid);
103
+ const clientAid = client.currentAid;
104
+ if (!clientAid || clientAid.aid !== subjectAid
105
+ || !clientAid.isCertValid() || !clientAid.isPrivateKeyValid()) {
106
+ throw new OwnershipError('SUBJECT_MISMATCH', 'Ownership client identity does not match the subject AID');
107
+ }
108
+ this.detachClient(subjectAid);
109
+ const subject = this.subject(subjectAid);
110
+ subject.client = client;
111
+ const connectionState = String(client.state ?? '');
112
+ subject.view.connected = connected || client.isReady === true || connectionState === 'ready' || connectionState === 'connected';
113
+ const generation = subject.generation;
114
+ for (const event of ['ownership.request', 'ownership.pending', 'ownership.progress', 'ownership.completed',
115
+ 'ownership.rejected', 'ownership.stopped', 'ownership.error', 'ownership.action_required']) {
116
+ subject.subscriptions.push(client.on(event, payload => {
117
+ if (subject.client !== client || subject.generation !== generation)
118
+ return;
119
+ const record = this.record(payload);
120
+ if (!record || record.sub_aid !== subjectAid)
121
+ return;
122
+ if (typeof record.operation_id === 'string') {
123
+ const key = JSON.stringify([event, record.operation_id, record.step, record.status,
124
+ record.owner_aid, record.relation_version, record.code, record.reason]);
125
+ if (subject.taskEvents.has(key))
126
+ return;
127
+ subject.taskEvents.add(key);
128
+ if (subject.taskEvents.size > 256)
129
+ subject.taskEvents.delete(subject.taskEvents.values().next().value);
130
+ }
131
+ this.invalidate(subject, 'Ownership changed; fresh consensus is required');
132
+ const task = this.task(record);
133
+ logger.info(`[ownership] ${event} ${JSON.stringify({ subjectAid, ownerAid: task?.owner_aid,
134
+ operationId: task?.operation_id, taskStatus: task?.status, step: task?.step,
135
+ code: task?.code, reason: task?.reason })}`);
136
+ if (task && subject.view.requestedOwnerAid === task.owner_aid
137
+ && (!subject.view.task || (subject.view.task.operation_id === task.operation_id
138
+ && (subject.view.task.status === 'pending' || subject.view.task.status === task.status)))) {
139
+ subject.view.task = task;
140
+ subject.view.requestStatus = 'submitted';
141
+ }
142
+ try {
143
+ this.persist(subjectAid, subject);
144
+ }
145
+ catch { }
146
+ this.schedule(subjectAid, subject);
147
+ }));
148
+ }
149
+ subject.subscriptions.push(client.on('agentmd.changed', payload => {
150
+ if (subject.client !== client || subject.generation !== generation
151
+ || typeof payload !== 'string' || !isValidAid(payload))
152
+ return;
153
+ this.invalidateRelatedSubjects(payload);
154
+ }));
155
+ subject.subscriptions.push(client.on('storage.object_changed', payload => {
156
+ if (subject.client !== client || subject.generation !== generation)
157
+ return;
158
+ const record = this.record(payload);
159
+ if (!record || record.owner_aid !== subjectAid || typeof record.object_key !== 'string')
160
+ return;
161
+ const objectKey = record.object_key.replace(/^\/+/, '');
162
+ if (objectKey === 'agent.md')
163
+ this.invalidateRelatedSubjects(subjectAid);
164
+ else {
165
+ const match = /^sub_aids\/([^/]+)\/owner\.md$/.exec(objectKey);
166
+ if (match)
167
+ this.invalidateRelatedSubjects(subjectAid, match[1]);
168
+ }
169
+ }));
170
+ for (const event of ['state_change', 'connection.state']) {
171
+ subject.subscriptions.push(client.on(event, payload => {
172
+ if (subject.client !== client || subject.generation !== generation)
173
+ return;
174
+ const record = this.record(payload);
175
+ const state = typeof payload === 'string' ? payload : record?.state ?? record?.status;
176
+ if (state === 'ready' || state === 'connected')
177
+ this.setConnected(subjectAid, client, true);
178
+ else if (typeof state === 'string')
179
+ this.setConnected(subjectAid, client, false);
180
+ }));
181
+ }
182
+ if (subject.view.connected)
183
+ this.schedule(subjectAid, subject);
184
+ }
185
+ setConnected(subjectAid, client, connected) {
186
+ const subject = this.subjects.get(subjectAid);
187
+ if (!subject || subject.client !== client)
188
+ return;
189
+ if (subject.view.connected === connected)
190
+ return;
191
+ if (!connected) {
192
+ // Transport loss does not revoke unexpired consensus. Retire work from
193
+ // the old connection without clearing the cached authorization.
194
+ subject.epoch += 1;
195
+ if (subject.timer)
196
+ clearTimeout(subject.timer);
197
+ subject.timer = undefined;
198
+ if (subject.pendingRefreshTimer)
199
+ clearTimeout(subject.pendingRefreshTimer);
200
+ subject.pendingRefreshTimer = undefined;
201
+ }
202
+ subject.view.connected = connected;
203
+ if (connected)
204
+ this.schedule(subjectAid, subject);
205
+ }
206
+ detachClient(subjectAid, client) {
207
+ const subject = this.subjects.get(subjectAid);
208
+ if (!subject || (client && subject.client !== client))
209
+ return;
210
+ subject.generation += 1;
211
+ if (subject.timer)
212
+ clearTimeout(subject.timer);
213
+ subject.timer = undefined;
214
+ if (subject.pendingRefreshTimer)
215
+ clearTimeout(subject.pendingRefreshTimer);
216
+ subject.pendingRefreshTimer = undefined;
217
+ subject.taskEvents.clear();
218
+ for (const subscription of subject.subscriptions.splice(0)) {
219
+ try {
220
+ subscription.unsubscribe();
221
+ }
222
+ catch { }
223
+ }
224
+ subject.client = undefined;
225
+ subject.view.connected = false;
226
+ this.invalidate(subject, 'AUN client is unavailable');
227
+ }
228
+ owner(subjectAid) {
229
+ if (!subjectAid)
230
+ return undefined;
231
+ const view = this.subjects.get(subjectAid)?.view;
232
+ return view?.consensusStatus === 'consensus'
233
+ && (view.validUntil ?? 0) > this.now() ? view.ownerAid : undefined;
234
+ }
235
+ owners(subjectAid) {
236
+ if (!subjectAid)
237
+ return [];
238
+ return [...new Set([
239
+ ...this.configuredOwnerList(subjectAid),
240
+ ...this.protocolOwners(subjectAid),
241
+ ])];
242
+ }
243
+ protocolOwners(subjectAid) {
244
+ const owner = this.owner(subjectAid);
245
+ return owner ? [owner] : [];
246
+ }
247
+ primaryOwner(subjectAid) {
248
+ return this.owners(subjectAid)[0];
249
+ }
250
+ knownOwners(subjectAid) {
251
+ if (!subjectAid)
252
+ return [];
253
+ const configuredOwners = this.configuredOwnerList(subjectAid);
254
+ const ownerAid = this.subjects.get(subjectAid)?.view.relation?.ownerAid;
255
+ return [...new Set([...configuredOwners, ...(ownerAid ? [ownerAid] : [])])];
256
+ }
257
+ knownOwner(subjectAid) {
258
+ return this.knownOwners(subjectAid)[0];
259
+ }
260
+ daemonOwners() {
261
+ return this.owners(this.daemonAid);
262
+ }
263
+ configuredRevision(subjectAid) {
264
+ return JSON.stringify(this.configuredOwnerList(subjectAid));
265
+ }
266
+ revision(subjectAid) {
267
+ const view = this.subjects.get(subjectAid)?.view;
268
+ return JSON.stringify([
269
+ view?.revision ?? 0,
270
+ this.owner(subjectAid),
271
+ view?.consensusStatus ?? 'unavailable',
272
+ this.configuredOwnerList(subjectAid),
273
+ ]);
274
+ }
275
+ snapshot(subjectAid) {
276
+ const subject = this.subject(subjectAid);
277
+ const view = JSON.parse(JSON.stringify(subject.view));
278
+ if (view.consensusStatus === 'consensus' && !this.owner(subjectAid)) {
279
+ view.ownerAid = undefined;
280
+ view.synchronization = { status: 'unavailable' };
281
+ }
282
+ const now = this.now();
283
+ view.authorizationStatus = view.consensusStatus === 'consensus'
284
+ && view.checkedAt !== undefined && view.checkedAt <= now
285
+ ? view.ownerAid && now - view.checkedAt < OWNERSHIP_AUTHORIZATION_MAX_AGE_MS ? 'verified' : 'expired'
286
+ : 'unavailable';
287
+ if (subject.persistenceError) {
288
+ view.requestStatus = 'error';
289
+ view.reason = subject.persistenceError;
290
+ view.synchronization.status = 'unavailable';
291
+ view.authorizationStatus = 'unavailable';
292
+ }
293
+ return view;
294
+ }
295
+ async refresh(subjectAid, options = {}) {
296
+ const subject = this.subject(subjectAid);
297
+ this.refreshConfiguredOwnersFromDisk(subjectAid);
298
+ if (subject.timer)
299
+ clearTimeout(subject.timer);
300
+ subject.timer = undefined;
301
+ const refreshing = subject.refreshing?.epoch === subject.epoch ? subject.refreshing.promise : undefined;
302
+ const maxAgeMs = options.maxAgeMs ?? this.options.lifetimeMs ?? OWNERSHIP_CACHE_MAX_AGE_MS;
303
+ const isFresh = () => {
304
+ const { checkedAt, validUntil, consensusStatus } = subject.view;
305
+ const now = this.now();
306
+ return consensusStatus !== 'unavailable' && consensusStatus !== 'not_applicable'
307
+ && checkedAt !== undefined && checkedAt <= now && (validUntil ?? 0) > now
308
+ && now - checkedAt < maxAgeMs;
309
+ };
310
+ if (!refreshing && !options.force && isFresh()) {
311
+ return this.snapshot(subjectAid);
312
+ }
313
+ const promise = refreshing ?? this.refreshSubject(subjectAid, subject);
314
+ subject.refreshing = { epoch: subject.epoch, promise };
315
+ try {
316
+ await promise;
317
+ if (!subject.view.connected && subject.view.consensusStatus === 'consensus' && !isFresh()) {
318
+ this.invalidate(subject, 'Ownership cache expired while disconnected');
319
+ }
320
+ return this.snapshot(subjectAid);
321
+ }
322
+ finally {
323
+ if (subject.refreshing?.promise === promise)
324
+ subject.refreshing = undefined;
325
+ this.schedulePendingRefresh(subjectAid, subject);
326
+ }
327
+ }
328
+ async refreshSubject(subjectAid, subject) {
329
+ const client = subject.client;
330
+ if (!client || !subject.view.connected) {
331
+ this.invalidate(subject, 'Subject AID has no connected SDK client');
332
+ return this.snapshot(subjectAid);
333
+ }
334
+ const generation = subject.generation;
335
+ const epoch = subject.epoch;
336
+ const query = ++subject.query;
337
+ let timeout;
338
+ try {
339
+ const consensus = await Promise.race([
340
+ client.checkOwnershipConsensus(subjectAid),
341
+ new Promise((_resolve, reject) => {
342
+ timeout = setTimeout(() => reject(new Error('Ownership consensus check timed out')), this.options.timeoutMs ?? 8_000);
343
+ timeout.unref?.();
344
+ }),
345
+ ]);
346
+ if (subject.client !== client || subject.generation !== generation || subject.epoch !== epoch
347
+ || subject.query !== query || !subject.view.connected)
348
+ return this.snapshot(subjectAid);
349
+ const ownerAid = consensus.status === 'consensus' && consensus.sub_aid === subjectAid
350
+ && typeof consensus.owner_aid === 'string' && isValidAid(consensus.owner_aid)
351
+ && consensus.owner_aid !== subjectAid ? consensus.owner_aid : undefined;
352
+ if (consensus.sub_aid !== subjectAid || (consensus.status === 'consensus' && !ownerAid)) {
353
+ throw new Error('Ownership consensus contains a mismatched subject or invalid owner');
354
+ }
355
+ const previousOwner = this.owner(subjectAid);
356
+ const previousStatus = subject.view.consensusStatus;
357
+ const checkedAt = this.now();
358
+ const pending = subject.view.task?.status === 'pending' || subject.view.requestStatus === 'requesting';
359
+ subject.view = {
360
+ ...subject.view,
361
+ ownerAid,
362
+ relation: consensus.status === 'consensus' || consensus.status === 'unbound' || consensus.status === 'released'
363
+ ? { consensusStatus: consensus.status, ownerAid, checkedAt, relationVersion: consensus.relation_version }
364
+ : subject.view.relation,
365
+ authorizationStatus: ownerAid ? 'verified' : 'unavailable',
366
+ consensusStatus: consensus.status,
367
+ consensus: JSON.parse(JSON.stringify(consensus)),
368
+ checkedAt,
369
+ validUntil: checkedAt + (this.options.lifetimeMs ?? OWNERSHIP_CACHE_MAX_AGE_MS),
370
+ revision: subject.view.revision + (previousOwner !== ownerAid
371
+ || subject.view.consensusStatus !== consensus.status
372
+ || subject.view.consensus?.relation_version !== consensus.relation_version
373
+ || subject.view.consensus?.lock !== consensus.lock ? 1 : 0),
374
+ reason: consensus.reason,
375
+ synchronization: {
376
+ status: consensus.status === 'unavailable' || consensus.status === 'not_applicable' ? 'unavailable'
377
+ : consensus.status === 'invalid' ? 'conflict'
378
+ : ownerAid && subject.view.requestedOwnerAid && subject.view.requestedOwnerAid !== ownerAid ? 'conflict'
379
+ : consensus.status === 'single_sided' || (!ownerAid && pending) ? 'pending' : 'synced',
380
+ runtimeOwnerAid: ownerAid,
381
+ },
382
+ };
383
+ if (previousOwner !== ownerAid || previousStatus !== consensus.status)
384
+ this.notify(subjectAid, ownerAid);
385
+ if (ownerAid === subject.view.requestedOwnerAid && subject.view.requestStatus === 'interrupted') {
386
+ subject.view.requestStatus = 'submitted';
387
+ this.persist(subjectAid, subject);
388
+ }
389
+ }
390
+ catch (error) {
391
+ if (subject.client === client && subject.generation === generation && subject.epoch === epoch && subject.query === query) {
392
+ this.invalidate(subject, error instanceof Error ? error.message : String(error));
393
+ }
394
+ }
395
+ finally {
396
+ if (timeout)
397
+ clearTimeout(timeout);
398
+ }
399
+ return this.snapshot(subjectAid);
400
+ }
401
+ async bind(subjectAid, ownerAid) {
402
+ this.assertLocalSubject(subjectAid);
403
+ this.assertAid(ownerAid);
404
+ if (ownerAid === subjectAid)
405
+ throw new OwnershipError('INVALID_OWNER', 'Subject cannot own itself');
406
+ const subject = this.subject(subjectAid);
407
+ if (subject.persistenceError)
408
+ throw new OwnershipError('persistence_unavailable', subject.persistenceError);
409
+ if (subject.binding) {
410
+ if (subject.binding.ownerAid !== ownerAid)
411
+ throw new OwnershipError('BIND_CONFLICT', 'Another owner binding is in progress');
412
+ return subject.binding.promise;
413
+ }
414
+ const promise = this.submitBinding(subjectAid, ownerAid, subject);
415
+ subject.binding = { ownerAid, promise };
416
+ try {
417
+ return await promise;
418
+ }
419
+ finally {
420
+ if (subject.binding?.promise === promise)
421
+ subject.binding = undefined;
422
+ }
423
+ }
424
+ stop() {
425
+ for (const subjectAid of this.subjects.keys())
426
+ this.detachClient(subjectAid);
427
+ this.configuredOwners.clear();
428
+ }
429
+ async submitBinding(subjectAid, ownerAid, subject) {
430
+ const current = await this.refresh(subjectAid, { force: true });
431
+ if (current.consensusStatus === 'consensus') {
432
+ if (current.ownerAid === ownerAid)
433
+ return current;
434
+ throw new OwnershipError('OWNER_ALREADY_BOUND', 'Subject already has a protocol owner; transfer is not supported');
435
+ }
436
+ const recovering = (subject.view.task?.status === 'pending' || subject.view.requestStatus === 'interrupted')
437
+ && subject.view.requestedOwnerAid === ownerAid;
438
+ if (!['unbound', 'released'].includes(current.consensusStatus)
439
+ && !(recovering && current.consensusStatus === 'single_sided')) {
440
+ throw new OwnershipError('CONSENSUS_REQUIRED', 'Binding requires a fresh unbound or released consensus result');
441
+ }
442
+ if (subject.view.task?.status === 'pending') {
443
+ if (subject.view.task.owner_aid !== ownerAid)
444
+ throw new OwnershipError('BIND_CONFLICT', 'A different binding task is pending');
445
+ }
446
+ if (subject.view.requestStatus === 'interrupted' && subject.view.requestedOwnerAid !== ownerAid) {
447
+ throw new OwnershipError('BIND_UNCERTAIN', 'Retry the interrupted request with its original owner before changing the target');
448
+ }
449
+ const client = subject.client;
450
+ if (!client || !subject.view.connected)
451
+ throw new OwnershipError('SUBJECT_UNAVAILABLE', 'Subject AID SDK client is not ready');
452
+ const generation = subject.generation;
453
+ subject.view = { ...subject.view, requestedOwnerAid: ownerAid, requestStatus: 'requesting', task: undefined };
454
+ this.persist(subjectAid, subject);
455
+ try {
456
+ const task = this.task(await client.bindOwneraid(ownerAid));
457
+ if (subject.client !== client || subject.generation !== generation) {
458
+ throw new OwnershipError('CLIENT_REPLACED', 'Subject SDK client changed during binding; the request may have been submitted');
459
+ }
460
+ if (!task || task.sub_aid !== subjectAid || task.owner_aid !== ownerAid || task.operation !== 'bind') {
461
+ throw new OwnershipError('TASK_MISMATCH', 'SDK returned a binding task for a different subject or owner');
462
+ }
463
+ subject.view.task = task;
464
+ subject.view.requestStatus = 'submitted';
465
+ this.persist(subjectAid, subject);
466
+ this.invalidate(subject, 'Binding submitted; waiting for explicit owner approval and fresh consensus');
467
+ this.schedule(subjectAid, subject);
468
+ return this.snapshot(subjectAid);
469
+ }
470
+ catch (error) {
471
+ if (subject.client === client && subject.generation === generation) {
472
+ subject.view.requestStatus = 'interrupted';
473
+ subject.view.reason = error instanceof Error ? error.message : String(error);
474
+ this.persist(subjectAid, subject);
475
+ }
476
+ throw error;
477
+ }
478
+ }
479
+ subject(subjectAid) {
480
+ this.assertAid(subjectAid);
481
+ let subject = this.subjects.get(subjectAid);
482
+ if (subject)
483
+ return subject;
484
+ let saved = null;
485
+ let persistenceError;
486
+ try {
487
+ const filename = this.statePath(subjectAid);
488
+ saved = atomicReadJson(filename);
489
+ const savedTask = saved?.task !== undefined ? this.task(saved.task) : undefined;
490
+ if ((saved === null && fs.existsSync(filename)) || (saved && (saved.version !== 1 || saved.subjectAid !== subjectAid
491
+ || (saved.requestedOwnerAid !== undefined && (typeof saved.requestedOwnerAid !== 'string' || !isValidAid(saved.requestedOwnerAid)))
492
+ || (saved.task !== undefined && (!savedTask || savedTask.sub_aid !== subjectAid || savedTask.owner_aid !== saved.requestedOwnerAid))
493
+ || (saved.requestStatus !== undefined && !['requesting', 'submitted', 'interrupted', 'error'].includes(String(saved.requestStatus)))))) {
494
+ throw new Error('Invalid persisted ownership task');
495
+ }
496
+ }
497
+ catch (error) {
498
+ saved = null;
499
+ persistenceError = `Ownership task storage is unavailable: ${error instanceof Error ? error.message : String(error)}`;
500
+ }
501
+ const task = saved?.subjectAid === subjectAid ? this.task(saved.task) : undefined;
502
+ subject = {
503
+ generation: 0, epoch: 0, query: 0, subscriptions: [], taskEvents: new Set(), persistenceError,
504
+ view: {
505
+ subjectAid, consensusStatus: 'unavailable', authorizationStatus: 'unavailable', connected: false, revision: 0,
506
+ task: task?.sub_aid === subjectAid ? task : undefined,
507
+ requestedOwnerAid: saved?.subjectAid === subjectAid && typeof saved.requestedOwnerAid === 'string'
508
+ && isValidAid(saved.requestedOwnerAid) ? saved.requestedOwnerAid : undefined,
509
+ requestStatus: saved?.requestStatus === 'requesting' || saved?.requestStatus === 'interrupted' ? 'interrupted'
510
+ : task ? 'submitted' : undefined,
511
+ synchronization: { status: 'unavailable' },
512
+ },
513
+ };
514
+ this.subjects.set(subjectAid, subject);
515
+ return subject;
516
+ }
517
+ invalidate(subject, reason) {
518
+ const previousOwner = subject.view.ownerAid;
519
+ subject.epoch += 1;
520
+ subject.view = {
521
+ ...subject.view, ownerAid: undefined, consensusStatus: 'unavailable', authorizationStatus: 'unavailable', validUntil: undefined,
522
+ revision: subject.view.revision + 1, reason, synchronization: { status: 'unavailable' },
523
+ };
524
+ if (previousOwner)
525
+ this.notify(subject.view.subjectAid);
526
+ }
527
+ notify(subjectAid, ownerAid) {
528
+ for (const listener of this.listeners) {
529
+ queueMicrotask(() => { try {
530
+ listener(subjectAid, ownerAid);
531
+ }
532
+ catch { } });
533
+ }
534
+ }
535
+ invalidateRelatedSubjects(changedAid, targetAid) {
536
+ for (const [subjectAid, subject] of this.subjects) {
537
+ if (!subject.client || !subject.view.connected || (targetAid && targetAid !== subjectAid))
538
+ continue;
539
+ const ownerAid = subject.view.relation?.ownerAid;
540
+ if ((!targetAid && changedAid === subjectAid) || changedAid === ownerAid
541
+ || changedAid === subject.view.requestedOwnerAid) {
542
+ this.invalidate(subject, 'Ownership material changed; fresh consensus is required');
543
+ this.schedule(subjectAid, subject);
544
+ }
545
+ }
546
+ }
547
+ schedule(subjectAid, subject) {
548
+ if (subject.timer || !subject.client || !subject.view.connected)
549
+ return;
550
+ subject.timer = setTimeout(() => {
551
+ subject.timer = undefined;
552
+ void this.refresh(subjectAid, { force: true }).catch(error => {
553
+ logger.warn(`[ownership] Event refresh failed for ${subjectAid}: ${error instanceof Error ? error.message : String(error)}`);
554
+ });
555
+ }, 0);
556
+ subject.timer.unref?.();
557
+ }
558
+ schedulePendingRefresh(subjectAid, subject) {
559
+ // The owner's final publication can become readable after the last SDK
560
+ // event. A cached unbound/single-sided observation must not strand an
561
+ // approved creation in ownerpending until the next connection or command.
562
+ // Only fresh SDK consensus can authorize the existing owner-bound handler;
563
+ // this never resubmits a binding or infers ownership from task completion.
564
+ const needsRefresh = subject.client && subject.view.connected
565
+ && subject.view.requestedOwnerAid && subject.view.consensusStatus !== 'consensus'
566
+ && (subject.view.task?.status === 'pending' || subject.view.task?.status === 'completed'
567
+ || subject.view.requestStatus === 'requesting' || subject.view.requestStatus === 'interrupted');
568
+ if (!needsRefresh) {
569
+ if (subject.pendingRefreshTimer)
570
+ clearTimeout(subject.pendingRefreshTimer);
571
+ subject.pendingRefreshTimer = undefined;
572
+ return;
573
+ }
574
+ if (subject.pendingRefreshTimer)
575
+ return;
576
+ subject.pendingRefreshTimer = setTimeout(() => {
577
+ subject.pendingRefreshTimer = undefined;
578
+ void this.refresh(subjectAid, { force: true }).catch(error => {
579
+ logger.warn(`[ownership] Pending consensus refresh failed for ${subjectAid}: ${error instanceof Error ? error.message : String(error)}`);
580
+ });
581
+ }, OWNERSHIP_PENDING_REFRESH_MS);
582
+ subject.pendingRefreshTimer.unref?.();
583
+ }
584
+ statePath(subjectAid) {
585
+ return path.join(this.options.stateDirectory?.() ?? path.join(resolvePaths().root, 'data', 'ownership'), `${subjectAid}.json`);
586
+ }
587
+ persist(subjectAid, subject) {
588
+ if (!subject.persistenceError) {
589
+ try {
590
+ const filename = this.statePath(subjectAid);
591
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
592
+ atomicWriteJson(filename, {
593
+ version: 1, subjectAid, requestedOwnerAid: subject.view.requestedOwnerAid,
594
+ requestStatus: subject.view.requestStatus, task: subject.view.task,
595
+ });
596
+ return;
597
+ }
598
+ catch (error) {
599
+ subject.persistenceError = `Ownership task storage is unavailable: ${error instanceof Error ? error.message : String(error)}`;
600
+ }
601
+ }
602
+ throw new OwnershipError('persistence_unavailable', subject.persistenceError);
603
+ }
604
+ now() {
605
+ return this.options.now?.() ?? Date.now();
606
+ }
607
+ refreshConfiguredOwnersFromDisk(subjectAid) {
608
+ const file = subjectAid === this.daemonAid
609
+ ? resolvePaths().daemonConfig
610
+ : path.join(resolvePaths().agentsDir, subjectAid, 'config.json');
611
+ let revision = 'missing';
612
+ try {
613
+ const stat = fs.statSync(file);
614
+ revision = `${stat.size}:${stat.mtimeMs}`;
615
+ }
616
+ catch { }
617
+ if (this.configuredOwnerFileRevision.get(subjectAid) === revision)
618
+ return;
619
+ const hadFileRevision = this.configuredOwnerFileRevision.has(subjectAid);
620
+ this.configuredOwnerFileRevision.set(subjectAid, revision);
621
+ if (revision === 'missing') {
622
+ if (hadFileRevision)
623
+ this.setConfiguredOwners(subjectAid, []);
624
+ return;
625
+ }
626
+ try {
627
+ const raw = atomicReadJson(file);
628
+ this.setConfiguredOwners(subjectAid, readConfiguredOwnerList(raw));
629
+ }
630
+ catch {
631
+ // The regular config loader reports malformed files; keep this optional
632
+ // compatibility read fail-closed without interrupting ownership checks.
633
+ }
634
+ }
635
+ assertAid(aid) {
636
+ if (!isValidAid(aid) || aid.trim() !== aid)
637
+ throw new OwnershipError('INVALID_AID', `Invalid AID: ${aid}`);
638
+ }
639
+ record(value) {
640
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
641
+ }
642
+ task(value) {
643
+ const record = this.record(value);
644
+ if (!record || typeof record.operation_id !== 'string' || !record.operation_id
645
+ || record.operation !== 'bind' || typeof record.sub_aid !== 'string' || !isValidAid(record.sub_aid)
646
+ || typeof record.owner_aid !== 'string' || !isValidAid(record.owner_aid)
647
+ || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(String(record.status))
648
+ || typeof record.step !== 'string')
649
+ return undefined;
650
+ return JSON.parse(JSON.stringify(record));
651
+ }
652
+ }
653
+ export const ownerResolver = new OwnerResolver();
654
+ export const getProtocolOwner = (subjectAid) => ownerResolver.owner(subjectAid);
655
+ export const getPrimaryOwner = (subjectAid) => ownerResolver.primaryOwner(subjectAid);
656
+ export const getProtocolOwners = (subjectAid) => ownerResolver.protocolOwners(subjectAid);
657
+ export const getOwners = (subjectAid) => ownerResolver.owners(subjectAid);
658
+ export const getConfiguredOwners = (subjectAid) => ownerResolver.configuredOwnerList(subjectAid);
659
+ export const getDaemonOwners = () => ownerResolver.daemonOwners();
660
+ export async function refreshOwnershipForAuthorization(subjectAid, options = {}) {
661
+ const daemonAid = ownerResolver.getDaemonAid();
662
+ const ownershipSubjectAid = daemonAid && subjectAid === `${daemonAid}#daemon` ? daemonAid : subjectAid;
663
+ const subjects = new Set([ownershipSubjectAid, ...(!ownershipSubjectAid || options.includeDaemon !== false ? [daemonAid] : [])]
664
+ .filter((aid) => !!aid));
665
+ await Promise.all([...subjects].map(aid => ownerResolver.refresh(aid, {
666
+ maxAgeMs: options.maxAgeMs ?? OWNERSHIP_AUTHORIZATION_MAX_AGE_MS,
667
+ force: options.force,
668
+ })));
669
+ }
670
+ export async function assertOwnershipAuthorization(context) {
671
+ const authorization = context?.executionPermission;
672
+ const ownerRole = context?.role === 'owner' && context.actor?.operatorChannelType !== 'daemon';
673
+ const ownerActor = hasTrustedPrincipal(context?.actor) ? context.actor.principalId : undefined;
674
+ if (!authorization && !ownerRole)
675
+ return;
676
+ await refreshOwnershipForAuthorization(context?.selfAid ?? authorization?.agentAid);
677
+ if (authorization) {
678
+ if (!getDaemonOwners().includes(authorization.authorizedBy)
679
+ || (authorization.source === 'agent-policy'
680
+ && !ownerResolver.isOwner(authorization.agentAid ?? '', authorization.authorizedBy))) {
681
+ throw new OwnershipError('OWNER_UNAVAILABLE', 'Current Owner no longer authorizes this execution');
682
+ }
683
+ }
684
+ else if (!context?.selfAid || !ownerResolver.isOwner(context.selfAid, ownerActor)) {
685
+ throw new OwnershipError('OWNER_UNAVAILABLE', 'Current Owner could not be verified; start a new task with the current role');
686
+ }
687
+ }