@ixo/oracle-runtime-workers 0.21.2 → 0.23.0

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 (59) hide show
  1. package/README.md +16 -13
  2. package/package.json +10 -4
  3. package/src/action-proposals/policy.ts +54 -0
  4. package/src/action-proposals/reconcile.ts +87 -0
  5. package/src/action-proposals/store.ts +863 -0
  6. package/src/action-proposals/topic-budget.ts +84 -0
  7. package/src/action-proposals/topic-chain.ts +272 -0
  8. package/src/action-proposals/topic-effects.ts +44 -0
  9. package/src/action-proposals/topic-errors.ts +79 -0
  10. package/src/action-proposals/topic-history.ts +267 -0
  11. package/src/action-proposals/topic-matrix.ts +292 -0
  12. package/src/action-proposals/topic-prepare.ts +299 -0
  13. package/src/action-proposals/topic-store.ts +999 -0
  14. package/src/action-proposals/topic-verify.ts +279 -0
  15. package/src/action-proposals/verify.ts +223 -0
  16. package/src/channels/auth.ts +6 -1
  17. package/src/channels/contract.ts +4 -0
  18. package/src/channels/turns.ts +1 -0
  19. package/src/core/env.ts +107 -5
  20. package/src/core/index.ts +8 -2
  21. package/src/core/main-agent-types.ts +16 -0
  22. package/src/core/main-agent.ts +26 -0
  23. package/src/core/middlewares/tool-execution.ts +87 -46
  24. package/src/core/middlewares/tool-marks.ts +54 -19
  25. package/src/core/runtime-context.ts +27 -1
  26. package/src/core/state.ts +2 -0
  27. package/src/core/tool-scheduler.ts +129 -24
  28. package/src/delivery/plan.ts +43 -2
  29. package/src/delivery/schema.ts +25 -1
  30. package/src/delivery/types.ts +13 -2
  31. package/src/do/active-runs.ts +171 -0
  32. package/src/do/ambient.ts +6 -0
  33. package/src/do/contracts.ts +86 -3
  34. package/src/do/run-coordinator.ts +30 -0
  35. package/src/do/run-request.ts +15 -2
  36. package/src/do/run-store-test-do.ts +10 -0
  37. package/src/do/run-store.ts +66 -13
  38. package/src/do/turn-body.ts +13 -1
  39. package/src/do/user-oracle-do.ts +1420 -17
  40. package/src/hub-actions/client.ts +163 -0
  41. package/src/hub-actions/request.ts +110 -0
  42. package/src/hub-actions/store.ts +485 -0
  43. package/src/matrix/gateway-do.ts +176 -0
  44. package/src/matrix/reply-parts.ts +8 -2
  45. package/src/plugin-api/tool-helper.ts +6 -0
  46. package/src/plugin-api/types.ts +117 -7
  47. package/src/plugins/action-proposals/action-proposals.plugin.ts +486 -0
  48. package/src/plugins/action-proposals/evidence.ts +67 -0
  49. package/src/plugins/agui/agui.plugin.ts +7 -5
  50. package/src/plugins/hub-actions/hub-actions.plugin.ts +244 -0
  51. package/src/plugins/index.ts +6 -0
  52. package/src/plugins/ixo-transaction/wallet-signing.ts +5 -2
  53. package/src/plugins/portal/declared-tools.ts +12 -0
  54. package/src/plugins/portal/portal.plugin.ts +22 -8
  55. package/src/realtime/frontend-call-registry.ts +147 -27
  56. package/src/realtime/realtime-endpoint.ts +335 -41
  57. package/src/realtime/session-socket-hub.ts +90 -19
  58. package/src/shell/app.ts +218 -2
  59. package/src/sqlite/sessions-store.ts +22 -0
@@ -0,0 +1,299 @@
1
+ import { createTopicId, createTopicOperationId } from '@ixo/topic-protocol';
2
+ import { withTopicReadBudget } from './topic-budget';
3
+ import { z } from 'zod';
4
+ import { prepareCreatePlan } from '@ixo/ixo-transaction/topic-create-plan';
5
+ import { buildTopicGrant } from '@ixo/ixo-transaction/topic-grants';
6
+ import { IxoAddressSchema } from '@ixo/ixo-transaction/action-proposals';
7
+ import {
8
+ TopicProposalSubjectSchema,
9
+ type TopicProposalSubject,
10
+ } from '@ixo/ixo-transaction/topic-proposals';
11
+ import {
12
+ resolveTopicIdentity,
13
+ readTopicRoom,
14
+ type TopicMatrixReader,
15
+ } from './topic-matrix';
16
+ import { readTopicAuthority } from './topic-history';
17
+ import { TopicChainReader } from './topic-chain';
18
+ import { digestTopicProposalEffect } from './topic-store';
19
+ import { ActionProposalConflict } from './store';
20
+ const network = z.enum(['devnet', 'testnet', 'mainnet']);
21
+ const did = z.string().startsWith('did:ixo:').max(256);
22
+ export const TopicCandidateSchema = z.discriminatedUnion('action', [
23
+ z.strictObject({
24
+ action: z.literal('create_topic'),
25
+ network,
26
+ roomId: z.string().startsWith('!').max(512),
27
+ title: z.string().trim().min(1).max(160),
28
+ participants: z.array(did).min(1).max(32),
29
+ roles: z
30
+ .array(
31
+ z.strictObject({
32
+ roleId: z.enum([
33
+ 'worker',
34
+ 'decision-authority',
35
+ 'action-requester',
36
+ 'action-confirmer',
37
+ 'settlement-controller',
38
+ ]),
39
+ assignees: z.array(did).min(1).max(32),
40
+ }),
41
+ )
42
+ .max(32)
43
+ .default([]),
44
+ tags: z.array(z.string().min(1).max(100)).max(32).default([]),
45
+ }),
46
+ z.strictObject({
47
+ action: z.literal('grant_topic_access'),
48
+ network,
49
+ roomId: z.string().startsWith('!').max(512),
50
+ topicId: z.string().min(1).max(255),
51
+ grants: z
52
+ .array(
53
+ z.strictObject({
54
+ did,
55
+ role: z.enum(['submit', 'evaluate']),
56
+ agentQuota: z.string().regex(/^[1-9]\d*$/),
57
+ expiresAt: z.iso.datetime(),
58
+ beforeDate: z.iso.datetime().optional(),
59
+ }),
60
+ )
61
+ .min(1)
62
+ .max(8),
63
+ }),
64
+ ]);
65
+ export type TopicCandidate = z.infer<typeof TopicCandidateSchema>;
66
+ export type TopicPreparationContext = {
67
+ ownerDid: string;
68
+ sessionId: string;
69
+ runId: string;
70
+ invocationId: string;
71
+ blocksync: string | undefined;
72
+ rpc: string | undefined;
73
+ chainId: string;
74
+ reader: TopicMatrixReader;
75
+ fetcher?: typeof fetch;
76
+ now?: number;
77
+ };
78
+ export async function prepareTopicSubject(
79
+ raw: unknown,
80
+ evidenceRefs: string[],
81
+ ctx: TopicPreparationContext,
82
+ ): Promise<TopicProposalSubject> {
83
+ return withTopicReadBudget(
84
+ ctx.reader,
85
+ ctx.fetcher ?? fetch,
86
+ (reader, fetcher) =>
87
+ prepareTopicSubjectWithinBudget(raw, evidenceRefs, {
88
+ ...ctx,
89
+ reader,
90
+ fetcher,
91
+ }),
92
+ );
93
+ }
94
+ async function prepareTopicSubjectWithinBudget(
95
+ raw: unknown,
96
+ evidenceRefs: string[],
97
+ ctx: TopicPreparationContext,
98
+ ): Promise<TopicProposalSubject> {
99
+ const candidate = TopicCandidateSchema.parse(raw);
100
+ if (candidate.network === 'mainnet')
101
+ throw new Error('Mainnet topic preparation is disabled');
102
+ const now = ctx.now ?? Date.now();
103
+ const createdAt = new Date(now).toISOString();
104
+ const id = `ap_${crypto.randomUUID()}`;
105
+ const owner = await resolveTopicIdentity(
106
+ ctx.ownerDid,
107
+ ctx.blocksync,
108
+ ctx.fetcher,
109
+ );
110
+ const room = await readTopicRoom(
111
+ ctx.reader,
112
+ candidate.roomId,
113
+ owner.matrixUserId,
114
+ );
115
+ const base = {
116
+ v: 2 as const,
117
+ id,
118
+ ownerDid: ctx.ownerDid,
119
+ origin: {
120
+ sessionId: ctx.sessionId,
121
+ runId: ctx.runId,
122
+ invocationId: ctx.invocationId,
123
+ },
124
+ evidenceRefs,
125
+ createdAt,
126
+ expiresAt: new Date(now + 15 * 60 * 1000).toISOString(),
127
+ };
128
+ if (candidate.action === 'create_topic') {
129
+ if (!candidate.participants.includes(ctx.ownerDid))
130
+ throw new Error('Explicit participants must include the owner');
131
+ const participants = await Promise.all(
132
+ candidate.participants.map((p) =>
133
+ resolveTopicIdentity(p, ctx.blocksync, ctx.fetcher),
134
+ ),
135
+ );
136
+ const bindings = new Map(participants.map((p) => [p.did, p.matrixUserId]));
137
+ const roles = candidate.roles.map((role) => ({
138
+ roleId: role.roleId,
139
+ assignees: role.assignees.map((did) => {
140
+ const user = bindings.get(did);
141
+ if (!user)
142
+ throw new Error('Every role must name an explicit participant');
143
+ return user;
144
+ }),
145
+ }));
146
+ const plan = await prepareCreatePlan({
147
+ draft: {
148
+ title: candidate.title,
149
+ ownerId: owner.matrixUserId,
150
+ participants: participants.map((p) => ({ id: p.matrixUserId })),
151
+ roles,
152
+ tags: candidate.tags,
153
+ },
154
+ roomId: candidate.roomId,
155
+ actor: owner.matrixUserId,
156
+ identity: {
157
+ topicId: createTopicId(),
158
+ operationId: createTopicOperationId(),
159
+ createdAt,
160
+ transactionIdPrefix: `qa_${id.slice(3)}`,
161
+ },
162
+ evaluationTime: {
163
+ at: createdAt,
164
+ source: 'server',
165
+ trusted: true,
166
+ freshUntil: base.expiresAt,
167
+ },
168
+ encrypted: room.encrypted,
169
+ proposalId: id,
170
+ });
171
+ return TopicProposalSubjectSchema.parse({
172
+ ...base,
173
+ title: `Create topic: ${candidate.title}`,
174
+ action: {
175
+ name: 'create_topic',
176
+ schemaVersion: '1',
177
+ network: candidate.network,
178
+ roomId: candidate.roomId,
179
+ owner,
180
+ participants,
181
+ plan,
182
+ dependencies: room.dependencies,
183
+ },
184
+ });
185
+ }
186
+ if (!ctx.rpc) throw new Error('Trusted chain endpoint unavailable');
187
+ const signerAddress = IxoAddressSchema.parse(
188
+ ctx.ownerDid.replace(/^did:ixo:/, ''),
189
+ );
190
+ const topic = await readTopicAuthority(
191
+ ctx.reader,
192
+ candidate.roomId,
193
+ candidate.topicId,
194
+ owner.matrixUserId,
195
+ room.state,
196
+ );
197
+ if (!topic.body.claimBinding)
198
+ throw new Error('The authenticated topic has no claim collection binding');
199
+ const chain = new TopicChainReader(ctx.rpc, ctx.chainId, ctx.fetcher);
200
+ const collection = await chain.collection(
201
+ topic.body.claimBinding.entityDid,
202
+ topic.body.claimBinding.collectionId,
203
+ signerAddress,
204
+ );
205
+ const dependencies: TopicProposalSubject['action']['dependencies'] = [
206
+ ...room.dependencies,
207
+ ...topic.dependencies,
208
+ ...collection.dependencies,
209
+ ];
210
+ const unique = new Set<string>();
211
+ for (const requested of candidate.grants) {
212
+ const key = `${requested.did}:${requested.role}`;
213
+ if (unique.has(key)) throw new Error('Duplicate grant');
214
+ unique.add(key);
215
+ if (
216
+ Date.parse(requested.expiresAt) <= now ||
217
+ Date.parse(requested.expiresAt) > now + 3 * 365 * 86400000
218
+ )
219
+ throw new Error('Grant expiry must be within three years');
220
+ if (requested.beforeDate && Date.parse(requested.beforeDate) <= now)
221
+ throw new Error('Evaluator beforeDate must be in the future');
222
+ }
223
+ // Each grant needs a DID lookup and a chain query; they are independent,
224
+ // so they run together inside the read budget instead of one after another.
225
+ const resolved = await Promise.all(
226
+ candidate.grants.map(async (requested) => {
227
+ const address = IxoAddressSchema.parse(
228
+ requested.did.replace(/^did:ixo:/, ''),
229
+ );
230
+ const [grantee, authorization] = await Promise.all([
231
+ resolveTopicIdentity(requested.did, ctx.blocksync, ctx.fetcher),
232
+ chain.authorization(collection.adminAddress, address, requested.role),
233
+ ]);
234
+ return { requested, address, grantee, authorization };
235
+ }),
236
+ );
237
+ const grants = [];
238
+ for (const { requested, address, grantee, authorization } of resolved) {
239
+ const roleId = requested.role === 'submit' ? 'worker' : 'evaluator';
240
+ if (
241
+ !topic.body.roles.some(
242
+ (role) =>
243
+ role.roleId === roleId &&
244
+ role.assignees.includes(grantee.matrixUserId),
245
+ )
246
+ )
247
+ throw new Error(
248
+ 'The grant recipient must hold the explicit claim role in the authenticated topic body',
249
+ );
250
+ dependencies.push(authorization.dependency);
251
+ let message;
252
+ try {
253
+ message = buildTopicGrant({
254
+ role: requested.role,
255
+ ownerAddress: signerAddress,
256
+ entityDid: topic.body.claimBinding.entityDid,
257
+ adminAddress: collection.adminAddress,
258
+ granteeAddress: address,
259
+ collectionId: topic.body.claimBinding.collectionId,
260
+ agentQuota: requested.agentQuota,
261
+ expiresAt: requested.expiresAt,
262
+ ...(requested.beforeDate ? { beforeDate: requested.beforeDate } : {}),
263
+ ...(authorization.existing ? { existing: authorization.existing } : {}),
264
+ });
265
+ } catch (error) {
266
+ // The builder's refusals (an existing grant under another expiry, an
267
+ // evaluator date outside the grant) are the owner's to read; a shape
268
+ // it rejects is not.
269
+ throw new ActionProposalConflict(
270
+ error instanceof Error && !(error instanceof z.ZodError)
271
+ ? error.message
272
+ : 'Grant cannot be built',
273
+ );
274
+ }
275
+ grants.push({ grantee, address, role: requested.role, message });
276
+ }
277
+ return TopicProposalSubjectSchema.parse({
278
+ ...base,
279
+ title: 'Grant topic claim access',
280
+ action: {
281
+ name: 'grant_topic_access',
282
+ schemaVersion: '1',
283
+ network: candidate.network,
284
+ roomId: candidate.roomId,
285
+ topicId: candidate.topicId,
286
+ owner,
287
+ authority: topic.authority,
288
+ chainId: ctx.chainId,
289
+ signerAddress,
290
+ entityDid: topic.body.claimBinding.entityDid,
291
+ adminAddress: collection.adminAddress,
292
+ collectionId: topic.body.claimBinding.collectionId,
293
+ grants,
294
+ memo: `Qi topic access ${id}`,
295
+ dependencies,
296
+ },
297
+ });
298
+ }
299
+ export const topicCandidateEvidenceDigest = digestTopicProposalEffect;