@ixo/oracle-runtime-workers 0.28.2 → 0.29.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.28.2",
3
+ "version": "0.29.0",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -33,7 +33,7 @@
33
33
  "!src/plugins/pod-creator/test-fixtures.ts"
34
34
  ],
35
35
  "dependencies": {
36
- "@ixo/action-protocol": "0.5.0",
36
+ "@ixo/action-protocol": "0.6.0",
37
37
  "@blocknote/server-util": "0.29.1",
38
38
  "@composio/core": "0.10.0",
39
39
  "@composio/langchain": "0.9.2",
@@ -65,8 +65,8 @@
65
65
  "@ixo/impactxclient-sdk": "^3.0.0",
66
66
  "protobufjs": "^7.6.5",
67
67
  "@ixo/topic-protocol": "1.0.0-rc.8",
68
- "@ixo/common": "1.7.1",
69
68
  "@ixo/ixo-transaction": "0.2.0",
69
+ "@ixo/common": "1.7.1",
70
70
  "@ixo/ucan": "2.3.0"
71
71
  },
72
72
  "devDependencies": {
@@ -165,7 +165,7 @@ export function draftReplyPlan(input: {
165
165
  for (const call of step.toolCalls) {
166
166
  if (
167
167
  call.name === 'request_hub_action' ||
168
- call.name === 'request_hub_transfer'
168
+ call.name === 'request_chain_transaction'
169
169
  ) {
170
170
  try {
171
171
  const result = JSON.parse(input.toolResults.get(call.id) ?? 'null');
@@ -47,11 +47,10 @@ import type {
47
47
  } from '@ixo/ixo-transaction/action-proposals';
48
48
  import { ChannelTurns } from '../channels/turns';
49
49
  import { HubActionClient } from '../hub-actions/client';
50
- import { prepareHubAction } from '../hub-actions/request';
50
+ import { chainTxOperation, prepareHubAction } from '../hub-actions/request';
51
51
  import {
52
52
  bankReceiptSummary,
53
- describeAmount,
54
- toBaseUnits,
53
+ bankStepsInBaseUnits,
55
54
  } from '../hub-actions/transfer';
56
55
  import {
57
56
  HUB_CHECK_AFTER_EXPIRY_MS,
@@ -64,7 +63,6 @@ import {
64
63
  type ActionCapabilitiesV1,
65
64
  type HubActionOptionsV2,
66
65
  type HubActionOptionsV3,
67
- parseOperation,
68
66
  v1Receipt,
69
67
  type ActionOperationV1,
70
68
  type ActionOutcomeV1,
@@ -430,7 +428,11 @@ import {
430
428
  resolveTurnDelegation,
431
429
  WorkersUcanService,
432
430
  } from './ucan-service';
433
- import type { UcanDelegation, UserContextData } from '../plugin-api/types';
431
+ import type {
432
+ ChainTxMessageInput,
433
+ UcanDelegation,
434
+ UserContextData,
435
+ } from '../plugin-api/types';
434
436
 
435
437
  /**
436
438
  * The gateway's topic read errors that describe what the homeserver
@@ -2269,7 +2271,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2269
2271
  requestKey: string;
2270
2272
  operation: ActionOperationV1;
2271
2273
  }) => this.requestHubAction(input),
2272
- transfer: (input) => this.transferHubAction(input),
2274
+ chainTransaction: (input) =>
2275
+ this.chainTransactionHubAction(input),
2273
2276
  status: (ownerDid: string, actionId: string) =>
2274
2277
  this.hubActionStatusForTool(ownerDid, actionId),
2275
2278
  },
@@ -3337,7 +3340,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3337
3340
  };
3338
3341
  }
3339
3342
 
3340
- /** `hub_action_options` and the transfer path: version 3, with the owner's tokens. */
3343
+ /** `hub_action_options` and a chain transaction's bank steps: version 3, with the owner's tokens. */
3341
3344
  private async hubActionOptions(
3342
3345
  input: {
3343
3346
  ownerDid: string;
@@ -3508,7 +3511,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3508
3511
  }
3509
3512
 
3510
3513
  /**
3511
- * `checked`: the transfer path already ran the turn-origin refusal and
3514
+ * `checked`: the chain-transaction path already ran the turn-origin refusal and
3512
3515
  * validated the binding in this same call.
3513
3516
  */
3514
3517
  private async requestHubAction(
@@ -3555,15 +3558,17 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3555
3558
  }
3556
3559
 
3557
3560
  /**
3558
- * A bank send of one of the owner's listed tokens. The token comes from
3559
- * a fresh options read (its balance must be current), the amount is
3560
- * converted to base units with the listed decimals (unverified tokens
3561
- * list 0: base units), refused when zero or above the balance; the
3562
- * network is the account's capability, the chain id this Worker's own
3563
- * for that network, the sender the owner's account. No fee is sent: the
3564
- * Hub simulates gas and sets it.
3561
+ * One chain transaction of the model's messages. The network is the
3562
+ * account's capability, the chain id this Worker's own for that
3563
+ * network; the signer and the owner's identity in claims messages come
3564
+ * from the owner's DID (`chainTxOperation`). The model names none of
3565
+ * them. Bank steps name a listed token and a display amount: with at
3566
+ * least one, the owner's options are read once, fresh (the balances
3567
+ * must be current), and each step is converted to base units and
3568
+ * checked against the listed balance (`bankStepsInBaseUnits`). No fee
3569
+ * is sent: the Hub simulates gas and sets it.
3565
3570
  */
3566
- private async transferHubAction(input: {
3571
+ private async chainTransactionHubAction(input: {
3567
3572
  ownerDid: string;
3568
3573
  sessionId: string;
3569
3574
  runId: string;
@@ -3572,73 +3577,39 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3572
3577
  bindingId: string;
3573
3578
  bindingRevision: number;
3574
3579
  requestKey: string;
3575
- toAddress: string;
3576
- denom: string;
3577
- amount: string;
3580
+ messages: ChainTxMessageInput[];
3578
3581
  memo: string;
3579
3582
  }) {
3580
3583
  if (!this.hubActionStore || input.ownerDid !== this.userDid)
3581
3584
  throw new Error('Hub actions are unavailable for this channel');
3582
3585
  await this.assertHubTurnOrigin(input);
3583
3586
  await this.assertHubBinding(input);
3584
- const options = await this.hubActionOptions(input, true);
3585
- const tokens = options.transfer?.tokens;
3586
- if (!tokens?.length)
3587
- throw new HubActionRefused(
3588
- 'refused',
3589
- 'Bank sends are not available for this owner right now (no token list from Auth Hub).',
3590
- );
3591
- const token = tokens.find((item) => item.denom === input.denom);
3592
- if (!token)
3593
- throw new HubActionRefused(
3594
- 'refused',
3595
- `That token is not in the owner's list; use a denom exactly as hub_action_options lists it (${tokens
3596
- .map((item) => item.denom)
3597
- .slice(0, 10)
3598
- .join(', ')}).`,
3599
- );
3600
- const amountBaseUnits = toBaseUnits(input.amount, token.decimals);
3601
- if (amountBaseUnits > BigInt(token.balanceBaseUnits))
3602
- throw new HubActionRefused(
3603
- 'refused',
3604
- `The owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than this transfer.`,
3605
- );
3606
3587
  const capabilities = await this.cachedHubCapabilities();
3607
- if (!capabilities.network)
3588
+ if (
3589
+ !capabilities.network ||
3590
+ !capabilities.chainTx ||
3591
+ !capabilities.operations.includes('ixo.chain.tx.v1')
3592
+ )
3608
3593
  throw new HubActionRefused(
3609
3594
  'refused',
3610
- 'Bank sends are not enabled for this oracle account.',
3595
+ 'Chain transactions are not enabled for this oracle account; retrying or changing the fields will not help.',
3611
3596
  );
3612
- const network = HUB_NETWORK_CHAIN[capabilities.network];
3613
3597
  const chainId = chainIdsFromConfig(chainIdConfigSchema.parse(this.env))[
3614
- network
3598
+ HUB_NETWORK_CHAIN[capabilities.network]
3615
3599
  ];
3616
- let operation: ActionOperationV1;
3617
- try {
3618
- operation = parseOperation({
3619
- kind: 'ixo.bank.send.v1',
3620
- chainId,
3621
- network: capabilities.network,
3622
- fromAddress: input.ownerDid.replace(/^did:ixo:/, ''),
3623
- toAddress: input.toAddress,
3624
- amountBaseUnits: amountBaseUnits.toString(),
3625
- denom: token.denom,
3626
- memo: input.memo,
3627
- });
3628
- } catch (error) {
3629
- // Only the protocol refusing the operation built from the model's
3630
- // fields (an invalid recipient or memo) is the model's to fix.
3631
- throw new HubActionRefused(
3632
- 'refused',
3633
- `The transfer is incomplete (${error instanceof Error ? error.message : String(error)}).`,
3634
- );
3635
- }
3636
- const reference = await this.requestHubAction(
3637
- { ...input, operation },
3638
- true,
3639
- );
3640
- await this.hubActionStore.setToken(reference.id, token);
3641
- return reference;
3600
+ const tokens = input.messages.some(
3601
+ (message) => message.type === 'cosmos.bank.send',
3602
+ )
3603
+ ? ((await this.hubActionOptions(input, true)).transfer?.tokens ?? null)
3604
+ : null;
3605
+ const operation = chainTxOperation({
3606
+ ownerDid: input.ownerDid,
3607
+ chainId,
3608
+ network: capabilities.network,
3609
+ memo: input.memo,
3610
+ messages: bankStepsInBaseUnits(input.messages, tokens),
3611
+ });
3612
+ return this.requestHubAction({ ...input, operation }, true);
3642
3613
  }
3643
3614
 
3644
3615
  /**
@@ -11,7 +11,11 @@ import {
11
11
  type HubActionOptionsV2,
12
12
  type HubActionOptionsV3,
13
13
  } from '@ixo/action-protocol';
14
- import { hubPrepareRefusal } from './transfer';
14
+ import {
15
+ hubPrepareRefusal,
16
+ prepareSubjectOf,
17
+ type HubPrepareSubject,
18
+ } from './transfer';
15
19
 
16
20
  export interface HubActionStatus {
17
21
  reference: ActionReferenceV1;
@@ -31,15 +35,27 @@ export interface HubActionTransport {
31
35
 
32
36
  /**
33
37
  * The protocol version this runtime declares on every operation call
34
- * (capabilities, POST operations, GET status, options). Version 3 is the
35
- * 0.5.0 contract: capabilities `transfer.limits`, any-denom bank sends, v2
36
- * receipts with `denom` and `amountBaseUnits`, options with the owner's
37
- * tokens. Version 2 callers (runtime 0.23.0) keep the 0.4.1 shapes; the
38
- * envelopes are still tagged `ixo.auth-outcome/2` and
38
+ * (capabilities, POST operations, GET status, options). Version 4 is the
39
+ * 0.6.0 contract: version 3 plus chain transactions (`ixo.chain.tx.v1` in
40
+ * capabilities with its `chainTx` entry, prepared through POST, receipts
41
+ * with `steps`); options answer as at version 3 (`ixo.auth-options/3`).
42
+ * Version 3 is the 0.5.0 contract: capabilities `transfer.limits`,
43
+ * any-denom bank sends, v2 receipts with `denom` and `amountBaseUnits`,
44
+ * options with the owner's tokens. Version 2 callers (runtime 0.23.0) keep
45
+ * the 0.4.1 shapes. The envelopes stay tagged `ixo.auth-outcome/2` and
39
46
  * `ixo.auth-capabilities/2`, which `@ixo/action-protocol` parses.
40
47
  */
41
- const HUB_PROTOCOL_VERSION = '3';
48
+ const HUB_PROTOCOL_VERSION = '4';
42
49
  const HUB_PROTOCOL_QUERY = `version=${HUB_PROTOCOL_VERSION}`;
50
+ /** Every Hub call but a chain-transaction prepare. */
51
+ const HUB_TIMEOUT_MS = 10_000;
52
+ /**
53
+ * A chain-transaction prepare: Auth Hub bounds its own chain reads and
54
+ * simulation at about 8 s and then answers 503 `transfer_unavailable`
55
+ * (retryable); this leaves room for that answer to arrive instead of an
56
+ * abort here.
57
+ */
58
+ export const HUB_CHAIN_TX_PREPARE_TIMEOUT_MS = 15_000;
43
59
 
44
60
  function isRecord(value: unknown): value is Record<string, unknown> {
45
61
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -66,10 +82,12 @@ export class HubActionClient {
66
82
  this.origin = url.origin;
67
83
  }
68
84
 
85
+ /** `subject`: what a refused prepare is called in the model's words. */
69
86
  private async send(
70
87
  path: string,
71
88
  body?: unknown,
72
- timeoutMs = 10_000,
89
+ timeoutMs = HUB_TIMEOUT_MS,
90
+ subject?: HubPrepareSubject,
73
91
  ): Promise<unknown> {
74
92
  const request = new Request(new URL(path, this.origin), {
75
93
  method: body === undefined ? 'GET' : 'POST',
@@ -87,16 +105,22 @@ export class HubActionClient {
87
105
  if (
88
106
  body !== undefined &&
89
107
  (response.status === 400 ||
108
+ response.status === 404 ||
90
109
  response.status === 409 ||
110
+ response.status === 429 ||
91
111
  response.status === 503)
92
112
  ) {
93
- // A prepare the Hub refused with a typed code (a transfer that would
94
- // fail, a fee above the ceiling, not enough funds, a busy or
95
- // unavailable transfer path, an operation the account may not use):
96
- // the model relays it in words.
113
+ // A prepare the Hub refused with a typed code (a transfer or
114
+ // transaction that would fail, a fee above the ceiling, not enough
115
+ // funds, a message the Hub did not accept, a missing chain
116
+ // authorization, a busy or unavailable transfer path, an operation
117
+ // the account may not use, a kind turned off, a protocol mismatch,
118
+ // no account for the owner, a rate limit): the model relays it in
119
+ // words.
97
120
  const refusal = hubPrepareRefusal(
98
121
  response.status,
99
122
  await response.json().catch(() => null),
123
+ subject,
100
124
  );
101
125
  if (refusal) throw refusal;
102
126
  }
@@ -112,15 +136,18 @@ export class HubActionClient {
112
136
  expiresAt: number;
113
137
  operation: ActionOperationV1;
114
138
  }): Promise<ActionReferenceV1> {
115
- // `version=3` on every operation call: the Hub answers older shapes
116
- // (no any-denom sends, no receipt amounts) without it, and stores the
117
- // version per action for the outcomes it pushes.
139
+ // The protocol version on every operation call: the Hub answers older
140
+ // shapes (no chain transactions, no any-denom sends, no receipt
141
+ // amounts) without it, and stores the version per action for the
142
+ // outcomes it pushes.
143
+ const operation = parseOperation(input.operation);
118
144
  const response = await this.send(
119
145
  `/api/internal/actions/operations?${HUB_PROTOCOL_QUERY}`,
120
- {
121
- ...input,
122
- operation: parseOperation(input.operation),
123
- },
146
+ { ...input, operation },
147
+ operation.kind === 'ixo.chain.tx.v1'
148
+ ? HUB_CHAIN_TX_PREPARE_TIMEOUT_MS
149
+ : HUB_TIMEOUT_MS,
150
+ prepareSubjectOf(operation.kind),
124
151
  );
125
152
  if (!response || typeof response !== 'object' || !('reference' in response))
126
153
  throw new Error('Hub action response has no reference');
@@ -139,7 +166,8 @@ export class HubActionClient {
139
166
  bindingId: string;
140
167
  bindingRevision: number;
141
168
  }): Promise<HubActionOptionsV3> {
142
- // Options version 3: version 2 plus the owner's bank tokens
169
+ // Options version 3 (asked at the protocol version; version 4 answers
170
+ // the same): version 2 plus the owner's bank tokens
143
171
  // (`transfer.tokens`), which any-token transfers are chosen from.
144
172
  const query = new URLSearchParams({
145
173
  bindingId: input.bindingId,
@@ -181,7 +209,10 @@ export class HubActionClient {
181
209
  }
182
210
 
183
211
  /** `timeoutMs`: the poll path passes a short one (a slow Hub must not hold a poll). */
184
- async status(id: string, timeoutMs = 10_000): Promise<HubActionStatus> {
212
+ async status(
213
+ id: string,
214
+ timeoutMs = HUB_TIMEOUT_MS,
215
+ ): Promise<HubActionStatus> {
185
216
  if (!/^[A-Za-z0-9_-]{32}$/.test(id))
186
217
  throw new Error('Invalid Hub action id');
187
218
  const raw = await this.send(
@@ -1,12 +1,91 @@
1
1
  import {
2
2
  canonicalJson,
3
+ parseOperation,
3
4
  type ActionCapabilitiesV1,
4
5
  type ActionOperationV1,
5
6
  type ActionReferenceV1,
7
+ type ChainTxMessage,
8
+ type ChainTxOperation,
6
9
  } from '@ixo/action-protocol';
7
10
  import type { HubActionClient } from './client';
8
11
  import { HubActionRefused, type HubActionOrigin } from './store';
9
12
 
13
+ /**
14
+ * A chain-transaction message as the owner object completes it: bank
15
+ * amounts already in base units (`bankStepsInBaseUnits`), the owner's
16
+ * identity in claims messages still to add (`chainTxOperation`).
17
+ */
18
+ export type ChainTxHostMessage =
19
+ | Extract<
20
+ ChainTxMessage,
21
+ { type: 'cosmos.bank.send' } | { type: 'ixo.token.transfer' }
22
+ >
23
+ | Omit<Extract<ChainTxMessage, { type: 'ixo.claims.submit' }>, 'agentDid'>
24
+ | Omit<
25
+ Extract<ChainTxMessage, { type: 'ixo.claims.evaluate' }>,
26
+ 'agentDid' | 'agentAddress'
27
+ >;
28
+
29
+ /**
30
+ * The chain transaction (`ixo.chain.tx.v1`) of the model's messages: the
31
+ * signer is the owner's account (their DID is `did:ixo:<address>`), and
32
+ * the owner's identity in claims messages is the signer's: `agentDid` is
33
+ * `did:ixo:<signer>` (the DID the runtime authenticates to the Hub as) and
34
+ * an evaluation's `agentAddress` is the signer (the chain records the
35
+ * evaluator, and pays it, by that address; Auth Hub requires both). The
36
+ * protocol parses the result; a field the runtime sets (signer, network,
37
+ * chain id) is refused in fixed words, a field from the model's messages
38
+ * in the protocol's (`Invalid messages[0].tokens[0].id`).
39
+ */
40
+ export function chainTxOperation(input: {
41
+ ownerDid: string;
42
+ chainId: string;
43
+ network: ChainTxOperation['network'];
44
+ memo: string;
45
+ messages: ChainTxHostMessage[];
46
+ }): ActionOperationV1 {
47
+ // Anything but `did:ixo:<address>` leaves no usable signer, which the
48
+ // protocol refuses below.
49
+ const signer = input.ownerDid.startsWith('did:ixo:')
50
+ ? input.ownerDid.slice('did:ixo:'.length)
51
+ : '';
52
+ const agentDid = `did:ixo:${signer}`;
53
+ // Identity last: a stray identity field cannot survive.
54
+ const messages: ChainTxMessage[] = input.messages.map((message) =>
55
+ message.type === 'ixo.claims.submit'
56
+ ? { ...message, agentDid }
57
+ : message.type === 'ixo.claims.evaluate'
58
+ ? { ...message, agentDid, agentAddress: signer }
59
+ : message,
60
+ );
61
+ try {
62
+ return parseOperation({
63
+ kind: 'ixo.chain.tx.v1',
64
+ chainId: input.chainId,
65
+ network: input.network,
66
+ signer,
67
+ memo: input.memo,
68
+ messages,
69
+ });
70
+ } catch (error) {
71
+ const detail = error instanceof Error ? error.message : String(error);
72
+ // The signer (the owner's account), the network and the chain id
73
+ // (this oracle's configuration) are the runtime's: no argument changes
74
+ // them.
75
+ if (/^Invalid (signer|network|chainId)$/.test(detail))
76
+ throw new HubActionRefused(
77
+ 'refused',
78
+ `This transaction cannot be prepared for this owner and oracle (${detail}: the runtime sets it from the owner's account and this oracle's configuration); retrying or changing the fields will not help.`,
79
+ );
80
+ // The protocol refusing a message built from the model's fields (an
81
+ // address, an amount, an id) is the model's to fix.
82
+ throw new HubActionRefused(
83
+ 'refused',
84
+ `The transaction is incomplete (${detail}).`,
85
+ );
86
+ }
87
+ }
88
+
10
89
  export interface PreparedHubActionRequest extends HubActionOrigin {
11
90
  provider: string;
12
91
  operation: ActionOperationV1;
@@ -53,6 +132,29 @@ export function assertOperationEnabled(
53
132
  'refused',
54
133
  `This oracle may move at most ${cap} base units of ${operation.denom} per transfer.`,
55
134
  );
135
+ } else if (operation.kind === 'ixo.chain.tx.v1') {
136
+ // A pre-check of what the account's capabilities list: its network,
137
+ // the message types and the batch size. The Hub enforces each again
138
+ // (and everything else: signer, addresses, simulation, fee).
139
+ const chainTx = capabilities.chainTx;
140
+ if (!chainTx || capabilities.network !== operation.network)
141
+ throw new HubActionRefused(
142
+ 'refused',
143
+ 'Chain transactions are not available for this oracle account.',
144
+ );
145
+ if (operation.messages.length > chainTx.maxMessages)
146
+ throw new HubActionRefused(
147
+ 'refused',
148
+ `This account allows at most ${chainTx.maxMessages} messages in one transaction.`,
149
+ );
150
+ const unlisted = operation.messages.find(
151
+ (message) => !chainTx.messageTypes.includes(message.type),
152
+ );
153
+ if (unlisted)
154
+ throw new HubActionRefused(
155
+ 'refused',
156
+ `This account cannot use ${unlisted.type} messages (it may use ${chainTx.messageTypes.join(', ')}).`,
157
+ );
56
158
  } else if (operation.kind === 'resource.read.v1') {
57
159
  if (
58
160
  operation.ownerDid !== ownerDid ||