@ixo/oracle-runtime-workers 0.23.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/core/env.ts +4 -1
- package/src/delivery/plan.ts +4 -1
- package/src/do/contracts.ts +0 -1
- package/src/do/user-oracle-do.ts +278 -71
- 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/memory/session-history-indexer.ts +30 -3
- package/src/memory/user-context-fetcher.ts +36 -12
- package/src/plugin-api/types.ts +25 -2
- package/src/plugins/hub-actions/hub-actions.plugin.ts +154 -25
- package/src/plugins/memory/index.ts +4 -0
- package/src/plugins/memory/memory-entity.ts +49 -0
- package/src/plugins/memory/memory-tools.ts +7 -4
- package/src/plugins/memory/memory-ucan.ts +10 -1
- package/src/plugins/memory/memory.plugin.ts +22 -7
- 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/core/env.ts
CHANGED
|
@@ -129,7 +129,10 @@ export const baseEnvSchema = z.object({
|
|
|
129
129
|
ORACLE_DID: z.string().min(1),
|
|
130
130
|
/**
|
|
131
131
|
* On-chain entity DID surfaced to the agent as `identity.entityDid`. Falls
|
|
132
|
-
* back to `ORACLE_DID` when unset (see `createRuntimeCore`)
|
|
132
|
+
* back to `ORACLE_DID` when unset (see `createRuntimeCore`) for rooms and
|
|
133
|
+
* feedback. The memory plugin redeclares it as required
|
|
134
|
+
* (`did:ixo:entity:<32 hex>`): the Memory Engine keys every space by it and
|
|
135
|
+
* never receives the fallback.
|
|
133
136
|
*/
|
|
134
137
|
ORACLE_ENTITY_DID: z.string().min(1).optional(),
|
|
135
138
|
NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).default('mainnet'),
|
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';
|
|
@@ -381,6 +389,10 @@ import {
|
|
|
381
389
|
fetchMemorySchemaDump,
|
|
382
390
|
getMemorySchemaDump,
|
|
383
391
|
} from '../plugins/memory/memory-tools';
|
|
392
|
+
import {
|
|
393
|
+
MEMORY_ENTITY_HEADER,
|
|
394
|
+
memoryEntityDid,
|
|
395
|
+
} from '../plugins/memory/memory-entity';
|
|
384
396
|
import {
|
|
385
397
|
contentToText,
|
|
386
398
|
isSummarizationMessage,
|
|
@@ -595,6 +607,25 @@ const ACTION_TICK_BUDGET_MS = 15_000;
|
|
|
595
607
|
const ACTION_PRUNE_INTERVAL_MS = 60 * 60_000;
|
|
596
608
|
/** How long the Hub's capabilities answer is reused per object. */
|
|
597
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;
|
|
598
629
|
/** How long a `hub_action_options` answer vouches for a Gmail connection. */
|
|
599
630
|
const HUB_OPTIONS_REUSE_MS = 10 * 60_000;
|
|
600
631
|
/** A channel poll waits this long for Auth Hub, once per poll at most. */
|
|
@@ -998,8 +1029,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
998
1029
|
at: number;
|
|
999
1030
|
} | null = null;
|
|
1000
1031
|
/** The last `hub_action_options` answer for this owner (Gmail connection checks). */
|
|
1001
|
-
private lastHubOptions: {
|
|
1002
|
-
|
|
1032
|
+
private lastHubOptions: {
|
|
1033
|
+
value: HubActionOptionsV2 | HubActionOptionsV3;
|
|
1034
|
+
at: number;
|
|
1035
|
+
} | null = null;
|
|
1003
1036
|
private topicProposals: TopicProposalStore | null = null;
|
|
1004
1037
|
/** `assertTopicDependenciesMemo`: card id and digest -> when it passed. */
|
|
1005
1038
|
private topicAuthorityMemo: Map<string, number> | undefined;
|
|
@@ -2183,8 +2216,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2183
2216
|
requestKey: string;
|
|
2184
2217
|
operation: ActionOperationV1;
|
|
2185
2218
|
}) => this.requestHubAction(input),
|
|
2219
|
+
transfer: (input) => this.transferHubAction(input),
|
|
2186
2220
|
status: (ownerDid: string, actionId: string) =>
|
|
2187
|
-
this.
|
|
2221
|
+
this.hubActionStatusForTool(ownerDid, actionId),
|
|
2188
2222
|
},
|
|
2189
2223
|
}
|
|
2190
2224
|
: {}),
|
|
@@ -3203,11 +3237,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3203
3237
|
this.markDirty();
|
|
3204
3238
|
await this.requestHousekeeping(Date.now() + 1000);
|
|
3205
3239
|
}
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
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) }));
|
|
3211
3247
|
return {
|
|
3212
3248
|
...result,
|
|
3213
3249
|
...(actionOutcomes.length ? { actionOutcomes } : {}),
|
|
@@ -3215,14 +3251,64 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3215
3251
|
};
|
|
3216
3252
|
}
|
|
3217
3253
|
|
|
3218
|
-
|
|
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: {
|
|
3219
3307
|
ownerDid: string;
|
|
3220
3308
|
provider: string;
|
|
3221
3309
|
bindingId: string;
|
|
3222
3310
|
bindingRevision: number;
|
|
3223
|
-
}) {
|
|
3224
|
-
if (!this.hubActionClient || input.ownerDid !== this.userDid)
|
|
3225
|
-
throw new Error('Hub action options are unavailable for this owner');
|
|
3311
|
+
}): Promise<void> {
|
|
3226
3312
|
await assertActiveChannelBinding(
|
|
3227
3313
|
{
|
|
3228
3314
|
userDid: input.ownerDid,
|
|
@@ -3234,17 +3320,34 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3234
3320
|
},
|
|
3235
3321
|
this.env,
|
|
3236
3322
|
);
|
|
3237
|
-
|
|
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;
|
|
3238
3339
|
if (
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
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)
|
|
3242
3346
|
)
|
|
3243
|
-
throw new
|
|
3244
|
-
'
|
|
3347
|
+
throw new HubActionRefused(
|
|
3348
|
+
'limit',
|
|
3349
|
+
'Reviews are prepared only in reply to a message from the user.',
|
|
3245
3350
|
);
|
|
3246
|
-
this.lastHubOptions = { value: options, at: Date.now() };
|
|
3247
|
-
return options;
|
|
3248
3351
|
}
|
|
3249
3352
|
|
|
3250
3353
|
/**
|
|
@@ -3276,7 +3379,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3276
3379
|
cached.value.bindingId === input.bindingId &&
|
|
3277
3380
|
cached.value.bindingRevision === input.bindingRevision
|
|
3278
3381
|
? cached.value
|
|
3279
|
-
: await this.
|
|
3382
|
+
: await this.readHubOptions(input, true, (binding) =>
|
|
3383
|
+
this.hubActionClient!.connectionOptions(binding),
|
|
3384
|
+
);
|
|
3280
3385
|
if (
|
|
3281
3386
|
!options.connections.some(
|
|
3282
3387
|
(c) =>
|
|
@@ -3316,50 +3421,32 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3316
3421
|
return value;
|
|
3317
3422
|
}
|
|
3318
3423
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
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
|
+
) {
|
|
3330
3442
|
const store = this.hubActionStore;
|
|
3331
3443
|
const client = this.hubActionClient;
|
|
3332
3444
|
if (!store || !client || input.ownerDid !== this.userDid)
|
|
3333
3445
|
throw new Error('Hub actions are unavailable for this channel');
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
? (JSON.parse(record.request) as StoredRunRequest).turn
|
|
3339
|
-
: null;
|
|
3340
|
-
if (
|
|
3341
|
-
!turn ||
|
|
3342
|
-
turn.client !== 'channel' ||
|
|
3343
|
-
!turn.channel?.remoteMessageRef ||
|
|
3344
|
-
turn.identity.userDid !== input.ownerDid ||
|
|
3345
|
-
turn.sessionId !== input.sessionId ||
|
|
3346
|
-
turn.sessionId.startsWith(SYNTHETIC_SESSION_PREFIX)
|
|
3347
|
-
)
|
|
3348
|
-
throw new HubActionRefused(
|
|
3349
|
-
'limit',
|
|
3350
|
-
'Reviews are prepared only in reply to a message from the user.',
|
|
3351
|
-
);
|
|
3352
|
-
await assertActiveChannelBinding(
|
|
3353
|
-
{
|
|
3354
|
-
userDid: input.ownerDid,
|
|
3355
|
-
channel: {
|
|
3356
|
-
provider: input.provider,
|
|
3357
|
-
bindingId: input.bindingId,
|
|
3358
|
-
bindingRevision: input.bindingRevision,
|
|
3359
|
-
},
|
|
3360
|
-
},
|
|
3361
|
-
this.env,
|
|
3362
|
-
);
|
|
3446
|
+
if (!checked) {
|
|
3447
|
+
await this.assertHubTurnOrigin(input);
|
|
3448
|
+
await this.assertHubBinding(input);
|
|
3449
|
+
}
|
|
3363
3450
|
await this.assertGmailOperationAllowed(input);
|
|
3364
3451
|
const reference = await prepareHubAction(
|
|
3365
3452
|
{
|
|
@@ -3381,6 +3468,107 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3381
3468
|
return reference;
|
|
3382
3469
|
}
|
|
3383
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
|
+
|
|
3384
3572
|
private async statusHubAction(
|
|
3385
3573
|
ownerDid: string,
|
|
3386
3574
|
actionId: string,
|
|
@@ -3471,15 +3659,24 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3471
3659
|
input.reference.id,
|
|
3472
3660
|
);
|
|
3473
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;
|
|
3474
3672
|
const remote = await this.hubActionClient.status(input.reference.id);
|
|
3475
3673
|
if (
|
|
3476
3674
|
!remote.outcome ||
|
|
3477
3675
|
remote.ownerDid !== input.ownerDid ||
|
|
3478
3676
|
canonicalJson(remote.reference) !== canonicalJson(held.reference) ||
|
|
3479
|
-
canonicalJson(input.reference) !== canonicalJson(held.reference) ||
|
|
3480
3677
|
remote.outcome.sequence < input.sequence ||
|
|
3481
3678
|
(remote.outcome.sequence === input.sequence &&
|
|
3482
|
-
|
|
3679
|
+
!sameHubOutcome(remote.outcome, input))
|
|
3483
3680
|
)
|
|
3484
3681
|
throw new Error(
|
|
3485
3682
|
'Hub outcome was not confirmed by authenticated status',
|
|
@@ -3728,13 +3925,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3728
3925
|
input.evidenceRefs,
|
|
3729
3926
|
);
|
|
3730
3927
|
if (prior) return prior;
|
|
3731
|
-
// The caps before the authority read it would waste (create checks
|
|
3732
|
-
// them again, atomically).
|
|
3733
|
-
await topics.assertCapacity(
|
|
3734
|
-
this.userDid,
|
|
3735
|
-
input.origin.sessionId,
|
|
3736
|
-
input.origin.runId,
|
|
3737
|
-
);
|
|
3738
3928
|
const rpc = {
|
|
3739
3929
|
devnet: this.env.ACTION_PROPOSALS_RPC_DEVNET,
|
|
3740
3930
|
testnet: this.env.ACTION_PROPOSALS_RPC_TESTNET,
|
|
@@ -4341,6 +4531,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4341
4531
|
const v = this.core.validatedEnv['MEMORY_ENGINE_URL'];
|
|
4342
4532
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
4343
4533
|
})(),
|
|
4534
|
+
// The configured entity only: `identity.entityDid` falls back to
|
|
4535
|
+
// `ORACLE_DID`, which the engine refuses as a space key.
|
|
4536
|
+
entityDid: memoryEntityDid(this.core.validatedEnv['ORACLE_ENTITY_DID']),
|
|
4344
4537
|
getSession: async (id) => {
|
|
4345
4538
|
const snapshot = this.historySnapshots.get(id);
|
|
4346
4539
|
const row =
|
|
@@ -4429,7 +4622,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4429
4622
|
this.userContextFetcher ??= new UserContextFetcher(
|
|
4430
4623
|
{
|
|
4431
4624
|
memoryEngineUrl: settings.engineUrl,
|
|
4432
|
-
|
|
4625
|
+
// The configured entity only: `identity.entityDid` falls back to
|
|
4626
|
+
// `ORACLE_DID`, which the engine refuses as a space key.
|
|
4627
|
+
entityDid: memoryEntityDid(
|
|
4628
|
+
this.core.validatedEnv['ORACLE_ENTITY_DID'],
|
|
4629
|
+
),
|
|
4433
4630
|
// Looked up per call, not captured: a working-copy reset replaces
|
|
4434
4631
|
// the ambient services while a gather may still be running.
|
|
4435
4632
|
ucan: {
|
|
@@ -5383,6 +5580,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5383
5580
|
if (!ambient || typeof url !== 'string' || url.length === 0) {
|
|
5384
5581
|
return { error: 'MEMORY_MCP_URL is not configured' };
|
|
5385
5582
|
}
|
|
5583
|
+
const entityDid = memoryEntityDid(
|
|
5584
|
+
this.core.validatedEnv['ORACLE_ENTITY_DID'],
|
|
5585
|
+
);
|
|
5586
|
+
if (!entityDid) {
|
|
5587
|
+
return {
|
|
5588
|
+
error:
|
|
5589
|
+
'ORACLE_ENTITY_DID is not a domain entity DID (did:ixo:entity:<32 hex>)',
|
|
5590
|
+
};
|
|
5591
|
+
}
|
|
5386
5592
|
const memoryDid = await ambient.ucan.resolveServiceDid(url);
|
|
5387
5593
|
if (!memoryDid)
|
|
5388
5594
|
return { error: 'memory engine DID could not be resolved' };
|
|
@@ -5400,6 +5606,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5400
5606
|
Authorization: `Bearer ${invocation}`,
|
|
5401
5607
|
'X-Auth-Type': 'ucan',
|
|
5402
5608
|
'User-Agent': 'LangChain-MCP-Client/1.0',
|
|
5609
|
+
[MEMORY_ENTITY_HEADER]: entityDid,
|
|
5403
5610
|
...(room?.roomId ? { 'x-room-id': room.roomId } : {}),
|
|
5404
5611
|
};
|
|
5405
5612
|
try {
|