@ixo/oracle-runtime-workers 0.24.0 → 0.25.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 +5 -4
- package/src/action-proposals/store.ts +0 -40
- package/src/action-proposals/topic-store.ts +0 -42
- package/src/delivery/plan.ts +4 -1
- package/src/do/contracts.ts +0 -1
- package/src/do/user-oracle-do.ts +256 -70
- package/src/hub-actions/client.ts +58 -5
- package/src/hub-actions/outcome-signature.ts +227 -0
- package/src/hub-actions/request.ts +16 -7
- package/src/hub-actions/store.ts +61 -37
- package/src/hub-actions/transfer.ts +182 -0
- package/src/plugin-api/types.ts +25 -2
- package/src/plugins/hub-actions/hub-actions.plugin.ts +154 -25
- package/src/shell/app.ts +36 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.
|
|
36
|
+
"@ixo/action-protocol": "0.5.0",
|
|
37
37
|
"@blocknote/server-util": "0.29.1",
|
|
38
38
|
"@composio/core": "0.10.0",
|
|
39
39
|
"@composio/langchain": "0.9.2",
|
|
@@ -65,12 +65,13 @@
|
|
|
65
65
|
"protobufjs": "^7.6.5",
|
|
66
66
|
"@ixo/topic-protocol": "1.0.0-rc.8",
|
|
67
67
|
"@ixo/common": "1.7.1",
|
|
68
|
-
"@ixo/
|
|
69
|
-
"@ixo/
|
|
68
|
+
"@ixo/ucan": "2.3.0",
|
|
69
|
+
"@ixo/ixo-transaction": "0.2.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
73
73
|
"@cloudflare/workers-types": "^5.20260825.1",
|
|
74
|
+
"@ixo/action-protocol-041": "npm:@ixo/action-protocol@0.4.1",
|
|
74
75
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
75
76
|
"happy-dom": "^19.0.0",
|
|
76
77
|
"typescript": "^5.9.3",
|
|
@@ -76,12 +76,6 @@ export const ACTION_PROPOSAL_UNRESOLVED_RETENTION_MS = 180 * 24 * 60 * 60_000;
|
|
|
76
76
|
* still possible, so the link stays used up.
|
|
77
77
|
*/
|
|
78
78
|
export const ACTION_PROPOSAL_EXECUTION_WINDOW_MS = 60 * 60_000;
|
|
79
|
-
/** Cards one agent run may prepare; more is a looping run, refused. */
|
|
80
|
-
export const ACTION_PROPOSALS_PER_RUN = 8;
|
|
81
|
-
/** Cards still awaiting the owner (live `prepared`, `claimed`, hash-less `unknown`) per owner. */
|
|
82
|
-
export const ACTION_PROPOSALS_OPEN_PER_OWNER = 16;
|
|
83
|
-
/** A `prepared` card counts as open this long after creation (its TTL plus clock skew). */
|
|
84
|
-
export const OPEN_PREPARED_MS = 20 * 60_000;
|
|
85
79
|
|
|
86
80
|
/**
|
|
87
81
|
* A card's claim time from its lifecycle JSON, NULL when the JSON is not
|
|
@@ -256,11 +250,6 @@ export class ActionProposalStore {
|
|
|
256
250
|
}
|
|
257
251
|
if (Date.parse(subject.expiresAt) <= this.now())
|
|
258
252
|
throw new ActionProposalConflict('Proposal expired');
|
|
259
|
-
await this.assertCapacity(
|
|
260
|
-
subject.ownerDid,
|
|
261
|
-
subject.origin.sessionId,
|
|
262
|
-
subject.origin.runId,
|
|
263
|
-
);
|
|
264
253
|
await this.db.run(
|
|
265
254
|
`INSERT INTO action_proposals (id,owner_did,invocation_id,session_id,run_id,subject,digest,lifecycle,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
|
266
255
|
[
|
|
@@ -293,35 +282,6 @@ export class ActionProposalStore {
|
|
|
293
282
|
});
|
|
294
283
|
}
|
|
295
284
|
|
|
296
|
-
/**
|
|
297
|
-
* A looping or injected run must not mint cards without bound: at most
|
|
298
|
-
* `ACTION_PROPOSALS_PER_RUN` per run and `ACTION_PROPOSALS_OPEN_PER_OWNER`
|
|
299
|
-
* still awaiting the owner. A resumed tool call never reaches here (it
|
|
300
|
-
* returns its original card above).
|
|
301
|
-
*/
|
|
302
|
-
private async assertCapacity(
|
|
303
|
-
ownerDid: string,
|
|
304
|
-
sessionId: string,
|
|
305
|
-
runId: string,
|
|
306
|
-
): Promise<void> {
|
|
307
|
-
const run = await this.db.get<{ n: number }>(
|
|
308
|
-
`SELECT COUNT(*) AS n FROM action_proposals WHERE owner_did=? AND session_id=? AND run_id=?`,
|
|
309
|
-
[ownerDid, sessionId, runId],
|
|
310
|
-
);
|
|
311
|
-
if ((run?.n ?? 0) >= ACTION_PROPOSALS_PER_RUN)
|
|
312
|
-
throw new ActionProposalConflict(
|
|
313
|
-
'Too many action cards in this request; finish the open ones first',
|
|
314
|
-
);
|
|
315
|
-
const open = await this.db.get<{ n: number }>(
|
|
316
|
-
`SELECT COUNT(*) AS n FROM action_proposals WHERE owner_did=? AND ((status='prepared' AND created_at>?) OR status='claimed' OR (status='unknown' AND transaction_hash IS NULL))`,
|
|
317
|
-
[ownerDid, this.now() - OPEN_PREPARED_MS],
|
|
318
|
-
);
|
|
319
|
-
if ((open?.n ?? 0) >= ACTION_PROPOSALS_OPEN_PER_OWNER)
|
|
320
|
-
throw new ActionProposalConflict(
|
|
321
|
-
'Too many action cards are open; finish or decline them first',
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
285
|
async get(ownerDid: string, id: string): Promise<ActionProposal | null> {
|
|
326
286
|
const row = await this.db.get<{
|
|
327
287
|
subject: string;
|
|
@@ -24,9 +24,6 @@ import {
|
|
|
24
24
|
ActionProposalConflict,
|
|
25
25
|
ACTION_PROPOSAL_RETENTION_MS,
|
|
26
26
|
ACTION_PROPOSAL_UNRESOLVED_RETENTION_MS,
|
|
27
|
-
ACTION_PROPOSALS_OPEN_PER_OWNER,
|
|
28
|
-
ACTION_PROPOSALS_PER_RUN,
|
|
29
|
-
OPEN_PREPARED_MS,
|
|
30
27
|
RECEIPT_MAX_ATTEMPTS,
|
|
31
28
|
RECEIPT_RETRY_BASE_MS,
|
|
32
29
|
RECEIPT_RETRY_MAX_MS,
|
|
@@ -163,11 +160,6 @@ export class TopicProposalStore {
|
|
|
163
160
|
}
|
|
164
161
|
if (Date.parse(subject.expiresAt) <= this.now())
|
|
165
162
|
throw new ActionProposalConflict('Proposal expired');
|
|
166
|
-
await this.assertCapacity(
|
|
167
|
-
subject.ownerDid,
|
|
168
|
-
subject.origin.sessionId,
|
|
169
|
-
subject.origin.runId,
|
|
170
|
-
);
|
|
171
163
|
const steps: TopicStepId[] =
|
|
172
164
|
subject.action.name === 'create_topic'
|
|
173
165
|
? ['anchor', 'body', 'operation', 'snapshot']
|
|
@@ -201,40 +193,6 @@ export class TopicProposalStore {
|
|
|
201
193
|
return TopicProposalV2Schema.parse({ ...subject, digest, lifecycle });
|
|
202
194
|
});
|
|
203
195
|
}
|
|
204
|
-
/**
|
|
205
|
-
* At most `ACTION_PROPOSALS_PER_RUN` cards per run and
|
|
206
|
-
* `ACTION_PROPOSALS_OPEN_PER_OWNER` open ones (prepared in the last 20
|
|
207
|
-
* minutes, or claimed / unknown in the last two hours: the claim window
|
|
208
|
-
* plus the execution window). The owner object runs this before the
|
|
209
|
-
* authority read a refused card would waste; `create` runs it again.
|
|
210
|
-
*/
|
|
211
|
-
async assertCapacity(
|
|
212
|
-
owner: string,
|
|
213
|
-
sessionId: string,
|
|
214
|
-
runId: string,
|
|
215
|
-
): Promise<void> {
|
|
216
|
-
const now = this.now();
|
|
217
|
-
const run = await this.db.get<{ n: number }>(
|
|
218
|
-
`SELECT COUNT(*) AS n FROM topic_action_proposals WHERE owner_did=? AND session_id=? AND run_id=?`,
|
|
219
|
-
[owner, sessionId, runId],
|
|
220
|
-
);
|
|
221
|
-
if ((run?.n ?? 0) >= ACTION_PROPOSALS_PER_RUN)
|
|
222
|
-
throw new ActionProposalConflict(
|
|
223
|
-
'Too many action cards in this request; finish the open ones first',
|
|
224
|
-
);
|
|
225
|
-
const open = await this.db.get<{ n: number }>(
|
|
226
|
-
`SELECT COUNT(*) AS n FROM topic_action_proposals WHERE owner_did=? AND ((status='prepared' AND created_at>?) OR (status IN ('claimed','unknown') AND created_at>?))`,
|
|
227
|
-
[
|
|
228
|
-
owner,
|
|
229
|
-
now - OPEN_PREPARED_MS,
|
|
230
|
-
now - OPEN_PREPARED_MS - TOPIC_EXECUTION_WINDOW_MS - 40 * 60_000,
|
|
231
|
-
],
|
|
232
|
-
);
|
|
233
|
-
if ((open?.n ?? 0) >= ACTION_PROPOSALS_OPEN_PER_OWNER)
|
|
234
|
-
throw new ActionProposalConflict(
|
|
235
|
-
'Too many action cards are open; finish or decline them first',
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
196
|
async get(owner: string, id: string): Promise<TopicActionProposal | null> {
|
|
239
197
|
const row = await this.db.get<{
|
|
240
198
|
subject: string;
|
package/src/delivery/plan.ts
CHANGED
|
@@ -163,7 +163,10 @@ export function draftReplyPlan(input: {
|
|
|
163
163
|
else parts.push({ kind: 'spill', key: `step-${i}`, spill: shape.spill });
|
|
164
164
|
}
|
|
165
165
|
for (const call of step.toolCalls) {
|
|
166
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
call.name === 'request_hub_action' ||
|
|
168
|
+
call.name === 'request_hub_transfer'
|
|
169
|
+
) {
|
|
167
170
|
try {
|
|
168
171
|
const result = JSON.parse(input.toolResults.get(call.id) ?? 'null');
|
|
169
172
|
if (result?.status === 'prepared') {
|
package/src/do/contracts.ts
CHANGED
|
@@ -85,7 +85,6 @@ export interface OracleWorkerEnv {
|
|
|
85
85
|
AUTH_HUB_URL?: string;
|
|
86
86
|
AUTH_HUB_CHANNEL_SERVICE_KEY?: string;
|
|
87
87
|
HUB_ACTIONS_ENABLED?: string;
|
|
88
|
-
AUTH_HUB_ACTION_OUTCOME_KEY?: string;
|
|
89
88
|
|
|
90
89
|
// --- matrix -------------------------------------------------------------
|
|
91
90
|
MATRIX_BASE_URL: string;
|
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -39,6 +39,11 @@ import type {
|
|
|
39
39
|
import { ChannelTurns } from '../channels/turns';
|
|
40
40
|
import { HubActionClient } from '../hub-actions/client';
|
|
41
41
|
import { prepareHubAction } from '../hub-actions/request';
|
|
42
|
+
import {
|
|
43
|
+
bankReceiptSummary,
|
|
44
|
+
describeAmount,
|
|
45
|
+
toBaseUnits,
|
|
46
|
+
} from '../hub-actions/transfer';
|
|
42
47
|
import {
|
|
43
48
|
HUB_CHECK_AFTER_EXPIRY_MS,
|
|
44
49
|
HubActionRefused,
|
|
@@ -49,6 +54,9 @@ import {
|
|
|
49
54
|
canonicalJson,
|
|
50
55
|
type ActionCapabilitiesV1,
|
|
51
56
|
type HubActionOptionsV2,
|
|
57
|
+
type HubActionOptionsV3,
|
|
58
|
+
parseOperation,
|
|
59
|
+
v1Receipt,
|
|
52
60
|
type ActionOperationV1,
|
|
53
61
|
type ActionOutcomeV1,
|
|
54
62
|
} from '@ixo/action-protocol';
|
|
@@ -599,6 +607,25 @@ const ACTION_TICK_BUDGET_MS = 15_000;
|
|
|
599
607
|
const ACTION_PRUNE_INTERVAL_MS = 60 * 60_000;
|
|
600
608
|
/** How long the Hub's capabilities answer is reused per object. */
|
|
601
609
|
const HUB_CAPABILITIES_CACHE_MS = 60_000;
|
|
610
|
+
/** A Hub network name as the chain-id config key it is configured under. */
|
|
611
|
+
/**
|
|
612
|
+
* Whether two Hub outcomes are the same event. Receipts are compared in the
|
|
613
|
+
* 0.4.1 shape: an action prepared at protocol version 2 (runtime 0.23.0) gets
|
|
614
|
+
* its callback without the 0.5.0 bank fields (`denom`, `amountBaseUnits`),
|
|
615
|
+
* while the confirming status read (version 3) carries them.
|
|
616
|
+
*/
|
|
617
|
+
function sameHubOutcome(a: ActionOutcomeV1, b: ActionOutcomeV1): boolean {
|
|
618
|
+
return (
|
|
619
|
+
canonicalJson({ ...a, receipt: v1Receipt(a.receipt) }) ===
|
|
620
|
+
canonicalJson({ ...b, receipt: v1Receipt(b.receipt) })
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const HUB_NETWORK_CHAIN = {
|
|
625
|
+
'ixo mainnet': 'mainnet',
|
|
626
|
+
'ixo testnet': 'testnet',
|
|
627
|
+
'ixo devnet': 'devnet',
|
|
628
|
+
} as const;
|
|
602
629
|
/** How long a `hub_action_options` answer vouches for a Gmail connection. */
|
|
603
630
|
const HUB_OPTIONS_REUSE_MS = 10 * 60_000;
|
|
604
631
|
/** A channel poll waits this long for Auth Hub, once per poll at most. */
|
|
@@ -1002,8 +1029,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1002
1029
|
at: number;
|
|
1003
1030
|
} | null = null;
|
|
1004
1031
|
/** The last `hub_action_options` answer for this owner (Gmail connection checks). */
|
|
1005
|
-
private lastHubOptions: {
|
|
1006
|
-
|
|
1032
|
+
private lastHubOptions: {
|
|
1033
|
+
value: HubActionOptionsV2 | HubActionOptionsV3;
|
|
1034
|
+
at: number;
|
|
1035
|
+
} | null = null;
|
|
1007
1036
|
private topicProposals: TopicProposalStore | null = null;
|
|
1008
1037
|
/** `assertTopicDependenciesMemo`: card id and digest -> when it passed. */
|
|
1009
1038
|
private topicAuthorityMemo: Map<string, number> | undefined;
|
|
@@ -2187,8 +2216,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2187
2216
|
requestKey: string;
|
|
2188
2217
|
operation: ActionOperationV1;
|
|
2189
2218
|
}) => this.requestHubAction(input),
|
|
2219
|
+
transfer: (input) => this.transferHubAction(input),
|
|
2190
2220
|
status: (ownerDid: string, actionId: string) =>
|
|
2191
|
-
this.
|
|
2221
|
+
this.hubActionStatusForTool(ownerDid, actionId),
|
|
2192
2222
|
},
|
|
2193
2223
|
}
|
|
2194
2224
|
: {}),
|
|
@@ -3207,11 +3237,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3207
3237
|
this.markDirty();
|
|
3208
3238
|
await this.requestHousekeeping(Date.now() + 1000);
|
|
3209
3239
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3240
|
+
// The gateway parses with @ixo/action-protocol 0.4.1, whose bank
|
|
3241
|
+
// receipt is exact: the 0.5.0 `denom`/`amountBaseUnits` fields are
|
|
3242
|
+
// dropped here (the owner-facing amount line comes from
|
|
3243
|
+
// hub_action_status instead), so the emitted shape stays 0.4.1.
|
|
3244
|
+
const hubActionOutcomes = (
|
|
3245
|
+
await this.hubOutcomesForRun(ownerDid, result.sessionId, result.runId)
|
|
3246
|
+
).map((outcome) => ({ ...outcome, receipt: v1Receipt(outcome.receipt) }));
|
|
3215
3247
|
return {
|
|
3216
3248
|
...result,
|
|
3217
3249
|
...(actionOutcomes.length ? { actionOutcomes } : {}),
|
|
@@ -3219,14 +3251,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3219
3251
|
};
|
|
3220
3252
|
}
|
|
3221
3253
|
|
|
3222
|
-
|
|
3254
|
+
/** `hub_action_options` and the transfer path: version 3, with the owner's tokens. */
|
|
3255
|
+
private async hubActionOptions(
|
|
3256
|
+
input: {
|
|
3257
|
+
ownerDid: string;
|
|
3258
|
+
provider: string;
|
|
3259
|
+
bindingId: string;
|
|
3260
|
+
bindingRevision: number;
|
|
3261
|
+
},
|
|
3262
|
+
bindingChecked = false,
|
|
3263
|
+
): Promise<HubActionOptionsV3> {
|
|
3264
|
+
const client = this.hubActionClient;
|
|
3265
|
+
if (!client || input.ownerDid !== this.userDid)
|
|
3266
|
+
throw new Error('Hub action options are unavailable for this owner');
|
|
3267
|
+
return this.readHubOptions(input, bindingChecked, (binding) =>
|
|
3268
|
+
client.options(binding),
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* Read the owner's options for the trusted binding (validated here
|
|
3274
|
+
* unless the caller already validated it in this same call) and keep
|
|
3275
|
+
* the read for the Gmail connection check.
|
|
3276
|
+
*/
|
|
3277
|
+
private async readHubOptions<
|
|
3278
|
+
T extends HubActionOptionsV2 | HubActionOptionsV3,
|
|
3279
|
+
>(
|
|
3280
|
+
input: {
|
|
3281
|
+
ownerDid: string;
|
|
3282
|
+
provider: string;
|
|
3283
|
+
bindingId: string;
|
|
3284
|
+
bindingRevision: number;
|
|
3285
|
+
},
|
|
3286
|
+
bindingChecked: boolean,
|
|
3287
|
+
read: (binding: {
|
|
3288
|
+
bindingId: string;
|
|
3289
|
+
bindingRevision: number;
|
|
3290
|
+
}) => Promise<T>,
|
|
3291
|
+
): Promise<T> {
|
|
3292
|
+
if (!bindingChecked) await this.assertHubBinding(input);
|
|
3293
|
+
const options = await read(input);
|
|
3294
|
+
if (
|
|
3295
|
+
options.ownerDid !== input.ownerDid ||
|
|
3296
|
+
options.bindingId !== input.bindingId ||
|
|
3297
|
+
options.bindingRevision !== input.bindingRevision
|
|
3298
|
+
)
|
|
3299
|
+
throw new Error(
|
|
3300
|
+
'Hub action options do not match this owner and binding',
|
|
3301
|
+
);
|
|
3302
|
+
this.lastHubOptions = { value: options, at: Date.now() };
|
|
3303
|
+
return options;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
private async assertHubBinding(input: {
|
|
3223
3307
|
ownerDid: string;
|
|
3224
3308
|
provider: string;
|
|
3225
3309
|
bindingId: string;
|
|
3226
3310
|
bindingRevision: number;
|
|
3227
|
-
}) {
|
|
3228
|
-
if (!this.hubActionClient || input.ownerDid !== this.userDid)
|
|
3229
|
-
throw new Error('Hub action options are unavailable for this owner');
|
|
3311
|
+
}): Promise<void> {
|
|
3230
3312
|
await assertActiveChannelBinding(
|
|
3231
3313
|
{
|
|
3232
3314
|
userDid: input.ownerDid,
|
|
@@ -3238,17 +3320,34 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3238
3320
|
},
|
|
3239
3321
|
this.env,
|
|
3240
3322
|
);
|
|
3241
|
-
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
/**
|
|
3326
|
+
* Only a run an inbound channel message started may prepare a review:
|
|
3327
|
+
* a scheduled or background run has no user asking for it. Checked
|
|
3328
|
+
* before any Hub read.
|
|
3329
|
+
*/
|
|
3330
|
+
private async assertHubTurnOrigin(input: {
|
|
3331
|
+
ownerDid: string;
|
|
3332
|
+
sessionId: string;
|
|
3333
|
+
runId: string;
|
|
3334
|
+
}): Promise<void> {
|
|
3335
|
+
const record = await this.runStore?.get(input.runId);
|
|
3336
|
+
const turn = record
|
|
3337
|
+
? (JSON.parse(record.request) as StoredRunRequest).turn
|
|
3338
|
+
: null;
|
|
3242
3339
|
if (
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3340
|
+
!turn ||
|
|
3341
|
+
turn.client !== 'channel' ||
|
|
3342
|
+
!turn.channel?.remoteMessageRef ||
|
|
3343
|
+
turn.identity.userDid !== input.ownerDid ||
|
|
3344
|
+
turn.sessionId !== input.sessionId ||
|
|
3345
|
+
turn.sessionId.startsWith(SYNTHETIC_SESSION_PREFIX)
|
|
3246
3346
|
)
|
|
3247
|
-
throw new
|
|
3248
|
-
'
|
|
3347
|
+
throw new HubActionRefused(
|
|
3348
|
+
'limit',
|
|
3349
|
+
'Reviews are prepared only in reply to a message from the user.',
|
|
3249
3350
|
);
|
|
3250
|
-
this.lastHubOptions = { value: options, at: Date.now() };
|
|
3251
|
-
return options;
|
|
3252
3351
|
}
|
|
3253
3352
|
|
|
3254
3353
|
/**
|
|
@@ -3280,7 +3379,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3280
3379
|
cached.value.bindingId === input.bindingId &&
|
|
3281
3380
|
cached.value.bindingRevision === input.bindingRevision
|
|
3282
3381
|
? cached.value
|
|
3283
|
-
: await this.
|
|
3382
|
+
: await this.readHubOptions(input, true, (binding) =>
|
|
3383
|
+
this.hubActionClient!.connectionOptions(binding),
|
|
3384
|
+
);
|
|
3284
3385
|
if (
|
|
3285
3386
|
!options.connections.some(
|
|
3286
3387
|
(c) =>
|
|
@@ -3320,50 +3421,32 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3320
3421
|
return value;
|
|
3321
3422
|
}
|
|
3322
3423
|
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3424
|
+
/**
|
|
3425
|
+
* `checked`: the transfer path already ran the turn-origin refusal and
|
|
3426
|
+
* validated the binding in this same call.
|
|
3427
|
+
*/
|
|
3428
|
+
private async requestHubAction(
|
|
3429
|
+
input: {
|
|
3430
|
+
ownerDid: string;
|
|
3431
|
+
sessionId: string;
|
|
3432
|
+
runId: string;
|
|
3433
|
+
requestId: string;
|
|
3434
|
+
provider: string;
|
|
3435
|
+
bindingId: string;
|
|
3436
|
+
bindingRevision: number;
|
|
3437
|
+
requestKey: string;
|
|
3438
|
+
operation: ActionOperationV1;
|
|
3439
|
+
},
|
|
3440
|
+
checked = false,
|
|
3441
|
+
) {
|
|
3334
3442
|
const store = this.hubActionStore;
|
|
3335
3443
|
const client = this.hubActionClient;
|
|
3336
3444
|
if (!store || !client || input.ownerDid !== this.userDid)
|
|
3337
3445
|
throw new Error('Hub actions are unavailable for this channel');
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
? (JSON.parse(record.request) as StoredRunRequest).turn
|
|
3343
|
-
: null;
|
|
3344
|
-
if (
|
|
3345
|
-
!turn ||
|
|
3346
|
-
turn.client !== 'channel' ||
|
|
3347
|
-
!turn.channel?.remoteMessageRef ||
|
|
3348
|
-
turn.identity.userDid !== input.ownerDid ||
|
|
3349
|
-
turn.sessionId !== input.sessionId ||
|
|
3350
|
-
turn.sessionId.startsWith(SYNTHETIC_SESSION_PREFIX)
|
|
3351
|
-
)
|
|
3352
|
-
throw new HubActionRefused(
|
|
3353
|
-
'limit',
|
|
3354
|
-
'Reviews are prepared only in reply to a message from the user.',
|
|
3355
|
-
);
|
|
3356
|
-
await assertActiveChannelBinding(
|
|
3357
|
-
{
|
|
3358
|
-
userDid: input.ownerDid,
|
|
3359
|
-
channel: {
|
|
3360
|
-
provider: input.provider,
|
|
3361
|
-
bindingId: input.bindingId,
|
|
3362
|
-
bindingRevision: input.bindingRevision,
|
|
3363
|
-
},
|
|
3364
|
-
},
|
|
3365
|
-
this.env,
|
|
3366
|
-
);
|
|
3446
|
+
if (!checked) {
|
|
3447
|
+
await this.assertHubTurnOrigin(input);
|
|
3448
|
+
await this.assertHubBinding(input);
|
|
3449
|
+
}
|
|
3367
3450
|
await this.assertGmailOperationAllowed(input);
|
|
3368
3451
|
const reference = await prepareHubAction(
|
|
3369
3452
|
{
|
|
@@ -3385,6 +3468,107 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3385
3468
|
return reference;
|
|
3386
3469
|
}
|
|
3387
3470
|
|
|
3471
|
+
/**
|
|
3472
|
+
* A bank send of one of the owner's listed tokens. The token comes from
|
|
3473
|
+
* a fresh options read (its balance must be current), the amount is
|
|
3474
|
+
* converted to base units with the listed decimals (unverified tokens
|
|
3475
|
+
* list 0: base units), refused when zero or above the balance; the
|
|
3476
|
+
* network is the account's capability, the chain id this Worker's own
|
|
3477
|
+
* for that network, the sender the owner's account. No fee is sent: the
|
|
3478
|
+
* Hub simulates gas and sets it.
|
|
3479
|
+
*/
|
|
3480
|
+
private async transferHubAction(input: {
|
|
3481
|
+
ownerDid: string;
|
|
3482
|
+
sessionId: string;
|
|
3483
|
+
runId: string;
|
|
3484
|
+
requestId: string;
|
|
3485
|
+
provider: string;
|
|
3486
|
+
bindingId: string;
|
|
3487
|
+
bindingRevision: number;
|
|
3488
|
+
requestKey: string;
|
|
3489
|
+
toAddress: string;
|
|
3490
|
+
denom: string;
|
|
3491
|
+
amount: string;
|
|
3492
|
+
memo: string;
|
|
3493
|
+
}) {
|
|
3494
|
+
if (!this.hubActionStore || input.ownerDid !== this.userDid)
|
|
3495
|
+
throw new Error('Hub actions are unavailable for this channel');
|
|
3496
|
+
await this.assertHubTurnOrigin(input);
|
|
3497
|
+
await this.assertHubBinding(input);
|
|
3498
|
+
const options = await this.hubActionOptions(input, true);
|
|
3499
|
+
const tokens = options.transfer?.tokens;
|
|
3500
|
+
if (!tokens?.length)
|
|
3501
|
+
throw new HubActionRefused(
|
|
3502
|
+
'refused',
|
|
3503
|
+
'Bank sends are not available for this owner right now (no token list from Auth Hub).',
|
|
3504
|
+
);
|
|
3505
|
+
const token = tokens.find((item) => item.denom === input.denom);
|
|
3506
|
+
if (!token)
|
|
3507
|
+
throw new HubActionRefused(
|
|
3508
|
+
'refused',
|
|
3509
|
+
`That token is not in the owner's list; use a denom exactly as hub_action_options lists it (${tokens
|
|
3510
|
+
.map((item) => item.denom)
|
|
3511
|
+
.slice(0, 10)
|
|
3512
|
+
.join(', ')}).`,
|
|
3513
|
+
);
|
|
3514
|
+
const amountBaseUnits = toBaseUnits(input.amount, token.decimals);
|
|
3515
|
+
if (amountBaseUnits > BigInt(token.balanceBaseUnits))
|
|
3516
|
+
throw new HubActionRefused(
|
|
3517
|
+
'refused',
|
|
3518
|
+
`The owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than this transfer.`,
|
|
3519
|
+
);
|
|
3520
|
+
const capabilities = await this.cachedHubCapabilities();
|
|
3521
|
+
if (!capabilities.network)
|
|
3522
|
+
throw new HubActionRefused(
|
|
3523
|
+
'refused',
|
|
3524
|
+
'Bank sends are not enabled for this oracle account.',
|
|
3525
|
+
);
|
|
3526
|
+
const network = HUB_NETWORK_CHAIN[capabilities.network];
|
|
3527
|
+
const chainId = chainIdsFromConfig(chainIdConfigSchema.parse(this.env))[
|
|
3528
|
+
network
|
|
3529
|
+
];
|
|
3530
|
+
let operation: ActionOperationV1;
|
|
3531
|
+
try {
|
|
3532
|
+
operation = parseOperation({
|
|
3533
|
+
kind: 'ixo.bank.send.v1',
|
|
3534
|
+
chainId,
|
|
3535
|
+
network: capabilities.network,
|
|
3536
|
+
fromAddress: input.ownerDid.replace(/^did:ixo:/, ''),
|
|
3537
|
+
toAddress: input.toAddress,
|
|
3538
|
+
amountBaseUnits: amountBaseUnits.toString(),
|
|
3539
|
+
denom: token.denom,
|
|
3540
|
+
memo: input.memo,
|
|
3541
|
+
});
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
// Only the protocol refusing the operation built from the model's
|
|
3544
|
+
// fields (an invalid recipient or memo) is the model's to fix.
|
|
3545
|
+
throw new HubActionRefused(
|
|
3546
|
+
'refused',
|
|
3547
|
+
`The transfer is incomplete (${error instanceof Error ? error.message : String(error)}).`,
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
const reference = await this.requestHubAction(
|
|
3551
|
+
{ ...input, operation },
|
|
3552
|
+
true,
|
|
3553
|
+
);
|
|
3554
|
+
await this.hubActionStore.setToken(reference.id, token);
|
|
3555
|
+
return reference;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
/**
|
|
3559
|
+
* `hub_action_status`: the outcome plus, for a bank send, the
|
|
3560
|
+
* owner-facing line (amount from the token stored at preparation).
|
|
3561
|
+
*/
|
|
3562
|
+
private async hubActionStatusForTool(
|
|
3563
|
+
ownerDid: string,
|
|
3564
|
+
actionId: string,
|
|
3565
|
+
): Promise<{ outcome: ActionOutcomeV1 | null; summary: string | null }> {
|
|
3566
|
+
const outcome = await this.statusHubAction(ownerDid, actionId);
|
|
3567
|
+
if (!outcome) return { outcome, summary: null };
|
|
3568
|
+
const token = await this.hubActionStore!.token(ownerDid, actionId);
|
|
3569
|
+
return { outcome, summary: bankReceiptSummary(outcome.receipt, token) };
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3388
3572
|
private async statusHubAction(
|
|
3389
3573
|
ownerDid: string,
|
|
3390
3574
|
actionId: string,
|
|
@@ -3475,15 +3659,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3475
3659
|
input.reference.id,
|
|
3476
3660
|
);
|
|
3477
3661
|
if (!held || !this.hubActionClient) throw new Error('Unknown Hub action');
|
|
3662
|
+
if (canonicalJson(input.reference) !== canonicalJson(held.reference))
|
|
3663
|
+
throw new Error('Hub outcome does not match the prepared action');
|
|
3664
|
+
// A replay of an outcome already confirmed and stored (a Hub retry,
|
|
3665
|
+
// a callback delivered twice) is acknowledged without a Hub read.
|
|
3666
|
+
const stored = await this.hubActionStore!.outcomeAt(
|
|
3667
|
+
input.ownerDid,
|
|
3668
|
+
input.reference.id,
|
|
3669
|
+
input.sequence,
|
|
3670
|
+
);
|
|
3671
|
+
if (stored && sameHubOutcome(stored, input)) return;
|
|
3478
3672
|
const remote = await this.hubActionClient.status(input.reference.id);
|
|
3479
3673
|
if (
|
|
3480
3674
|
!remote.outcome ||
|
|
3481
3675
|
remote.ownerDid !== input.ownerDid ||
|
|
3482
3676
|
canonicalJson(remote.reference) !== canonicalJson(held.reference) ||
|
|
3483
|
-
canonicalJson(input.reference) !== canonicalJson(held.reference) ||
|
|
3484
3677
|
remote.outcome.sequence < input.sequence ||
|
|
3485
3678
|
(remote.outcome.sequence === input.sequence &&
|
|
3486
|
-
|
|
3679
|
+
!sameHubOutcome(remote.outcome, input))
|
|
3487
3680
|
)
|
|
3488
3681
|
throw new Error(
|
|
3489
3682
|
'Hub outcome was not confirmed by authenticated status',
|
|
@@ -3732,13 +3925,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3732
3925
|
input.evidenceRefs,
|
|
3733
3926
|
);
|
|
3734
3927
|
if (prior) return prior;
|
|
3735
|
-
// The caps before the authority read it would waste (create checks
|
|
3736
|
-
// them again, atomically).
|
|
3737
|
-
await topics.assertCapacity(
|
|
3738
|
-
this.userDid,
|
|
3739
|
-
input.origin.sessionId,
|
|
3740
|
-
input.origin.runId,
|
|
3741
|
-
);
|
|
3742
3928
|
const rpc = {
|
|
3743
3929
|
devnet: this.env.ACTION_PROPOSALS_RPC_DEVNET,
|
|
3744
3930
|
testnet: this.env.ACTION_PROPOSALS_RPC_TESTNET,
|