@ixo/oracle-runtime-workers 0.13.0 → 0.16.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 (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +13 -8
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +310 -0
  18. package/src/channels/contract.ts +246 -0
  19. package/src/channels/turns.ts +280 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +170 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +2003 -274
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +504 -108
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +562 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. package/src/tasks/topic-deliverables.ts +70 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * The ixo-transaction tools. Three read tools resolve and validate a draft;
3
+ * `sign_ixo_transaction` dispatches the validated transaction to the user's
4
+ * Portal wallet as the hidden `sign_transaction` AG-UI action over the
5
+ * realtime channel (`ctx.frontend.callAgAction`) and maps the answer to a
6
+ * status. The oracle never signs, broadcasts or holds a key: the wallet does,
7
+ * in the user's browser.
8
+ *
9
+ * Every request names its chain id; the Portal refuses any other chain. A
10
+ * signed testnet transaction is recorded as a receipt (`receipts.ts`), and a
11
+ * mainnet draft is dispatched only when it cites the receipt of the same
12
+ * message.
13
+ */
14
+ import {
15
+ DEFERRED_MODULES,
16
+ MESSAGE_CATALOG,
17
+ QUERY_ONLY_MODULES,
18
+ SIGN_TRANSACTION_ACTION_NAME,
19
+ TransactionDraftInputSchema,
20
+ TransactionDraftSchema,
21
+ buildSignTransactionActionArgs,
22
+ classifyIntent,
23
+ describeValidationError,
24
+ validateTransactionDraft,
25
+ type Network,
26
+ type SingleSignTransactionActionArgs,
27
+ type ITrxMsg,
28
+ type TestnetReceipt,
29
+ } from '@ixo/ixo-transaction';
30
+ import { z } from 'zod';
31
+ import { tool } from '../../plugin-api/tool-helper';
32
+ import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
33
+ import { logActionToMatrix } from '../portal/action-log';
34
+ import { recordTestnetReceipt, testnetReceiptProblem } from './receipts';
35
+ import {
36
+ requestWalletSignature,
37
+ walletUnavailableReason,
38
+ type WalletSignatureOutcome,
39
+ } from './wallet-signing';
40
+
41
+ export interface IxoTransactionToolOptions {
42
+ /** How long the wallet has to answer a signing request. */
43
+ signTimeoutMs: number;
44
+ /** Whether mainnet drafts may pass validation at all. */
45
+ allowMainnet: boolean;
46
+ /** The chain id each network's requests are for. */
47
+ chainIds: Readonly<Record<Network, string>>;
48
+ }
49
+
50
+ /** What `sign_ixo_transaction` returns to the model. */
51
+ export type SignIxoTransactionResult =
52
+ | {
53
+ status: 'signed';
54
+ network: Network;
55
+ chainId: string;
56
+ typeUrl: string;
57
+ transactionHash?: string;
58
+ code?: number;
59
+ height?: string | number;
60
+ /** Testnet only: cite it in the mainnet draft of the same message. */
61
+ testnetReceipt?: TestnetReceipt & { expiresInHours: number };
62
+ /** Testnet only: why no receipt could be recorded. */
63
+ receiptError?: string;
64
+ }
65
+ | {
66
+ /** Included in a block but failed: the hash and code are real. */
67
+ status: 'failed';
68
+ network: Network;
69
+ chainId: string;
70
+ typeUrl: string;
71
+ code: number;
72
+ transactionHash?: string;
73
+ height?: string | number;
74
+ error: string;
75
+ }
76
+ | {
77
+ status: 'rejected' | 'error';
78
+ network: Network;
79
+ typeUrl: string;
80
+ error: string;
81
+ }
82
+ | {
83
+ status: 'timeout';
84
+ /** The wallet may still sign after the oracle stopped waiting. */
85
+ outcome: 'unknown';
86
+ network: Network;
87
+ typeUrl: string;
88
+ error: string;
89
+ }
90
+ | { status: 'validation_error' | 'unavailable'; error: string };
91
+
92
+ const RouteListToolSchema = z.object({
93
+ messageType: z
94
+ .string()
95
+ .optional()
96
+ .describe(
97
+ 'Module to list (entity, iid, claims, token, smart-account); all when omitted',
98
+ ),
99
+ });
100
+
101
+ const IntentInputToolSchema = z.object({
102
+ input: z
103
+ .string()
104
+ .min(1)
105
+ .describe('A slash command, Msg name, typeUrl, or the user request'),
106
+ });
107
+
108
+ function errorText(error: unknown): string {
109
+ return error instanceof Error ? error.message : String(error);
110
+ }
111
+
112
+ function listRoutes(messageType: string | undefined) {
113
+ const module = messageType?.trim().toLowerCase();
114
+ return {
115
+ slashCommandFormat: '/ixo {message-type} {message-action}',
116
+ queryOnlyModules: QUERY_ONLY_MODULES,
117
+ deferredModules: DEFERRED_MODULES,
118
+ routes: MESSAGE_CATALOG.filter(
119
+ (entry) => !module || entry.module === module,
120
+ ).map((entry) => ({
121
+ command: `/ixo ${entry.module} ${entry.action}`,
122
+ messageName: entry.messageName,
123
+ typeUrl: entry.typeUrl,
124
+ fields: entry.fields,
125
+ riskLevel: entry.riskLevel,
126
+ risks: entry.risks,
127
+ })),
128
+ };
129
+ }
130
+
131
+ /** The tool's status for a signing outcome: which network, chain and message. */
132
+ function signResult(
133
+ args: SingleSignTransactionActionArgs,
134
+ outcome: WalletSignatureOutcome,
135
+ ): SignIxoTransactionResult {
136
+ const network = args.network;
137
+ const typeUrl = args.intent.typeUrl;
138
+ switch (outcome.status) {
139
+ case 'signed':
140
+ case 'failed':
141
+ return { ...outcome, network, chainId: args.chainId, typeUrl };
142
+ case 'rejected':
143
+ case 'error':
144
+ case 'timeout':
145
+ return { ...outcome, network, typeUrl };
146
+ case 'unavailable':
147
+ return outcome;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * For a mainnet draft: why its `testnetReceipt` does not prove a testnet
153
+ * signing of the same message by this user, or null. Null for other networks.
154
+ */
155
+ async function mainnetReceiptProblem(
156
+ ctx: RuntimeContext,
157
+ options: IxoTransactionToolOptions,
158
+ network: Network,
159
+ receipt: TestnetReceipt | undefined,
160
+ message: ITrxMsg | undefined,
161
+ ): Promise<string | null> {
162
+ if (network !== 'mainnet') return null;
163
+ if (!receipt || !message) {
164
+ return 'Mainnet draft blocked: sign the same transaction on testnet first and pass the testnetReceipt that signing returned';
165
+ }
166
+ return testnetReceiptProblem(ctx, receipt, message, options.chainIds.testnet);
167
+ }
168
+
169
+ /** The draft's `testnetReceipt`, when it has a well-formed one. */
170
+ function testnetReceiptOf(input: unknown): TestnetReceipt | undefined {
171
+ const parsed = TransactionDraftSchema.safeParse(input);
172
+ return parsed.success ? parsed.data.testnetReceipt : undefined;
173
+ }
174
+
175
+ /** A signed testnet transaction gets a receipt the mainnet draft can cite. */
176
+ async function withTestnetReceipt(
177
+ ctx: RuntimeContext,
178
+ args: SingleSignTransactionActionArgs,
179
+ result: SignIxoTransactionResult,
180
+ ): Promise<SignIxoTransactionResult> {
181
+ const message = args.messages[0];
182
+ if (
183
+ result.status !== 'signed' ||
184
+ args.network !== 'testnet' ||
185
+ result.transactionHash === undefined ||
186
+ !message
187
+ ) {
188
+ return result;
189
+ }
190
+ try {
191
+ const receipt = await recordTestnetReceipt(ctx, {
192
+ transactionHash: result.transactionHash,
193
+ message,
194
+ chainId: args.chainId,
195
+ });
196
+ return { ...result, testnetReceipt: { ...receipt, expiresInHours: 24 } };
197
+ } catch (error) {
198
+ return {
199
+ ...result,
200
+ receiptError: `The testnet signing could not be recorded (${errorText(error)}); a mainnet run of this transaction will be refused until a testnet signing is recorded.`,
201
+ };
202
+ }
203
+ }
204
+
205
+ async function signIxoTransaction(
206
+ input: unknown,
207
+ ctx: RuntimeContext,
208
+ options: IxoTransactionToolOptions,
209
+ ): Promise<SignIxoTransactionResult> {
210
+ let args: SingleSignTransactionActionArgs;
211
+ try {
212
+ args = buildSignTransactionActionArgs(input, {
213
+ allowMainnet: options.allowMainnet,
214
+ chainIds: options.chainIds,
215
+ });
216
+ } catch (error) {
217
+ return {
218
+ status: 'validation_error',
219
+ error: describeValidationError(error),
220
+ };
221
+ }
222
+ const receiptProblem = await mainnetReceiptProblem(
223
+ ctx,
224
+ options,
225
+ args.network,
226
+ args.testnetReceipt,
227
+ args.messages[0],
228
+ );
229
+ if (receiptProblem !== null) {
230
+ return { status: 'validation_error', error: receiptProblem };
231
+ }
232
+ // Nothing to log in the room when the request cannot be sent at all.
233
+ const unavailable = walletUnavailableReason(ctx);
234
+ if (unavailable !== null) {
235
+ return { status: 'unavailable', error: unavailable };
236
+ }
237
+
238
+ const outcome = await requestWalletSignature(ctx, {
239
+ args,
240
+ callIdPrefix: 'ixo_tx',
241
+ timeoutMs: options.signTimeoutMs,
242
+ });
243
+ const result = await withTestnetReceipt(ctx, args, signResult(args, outcome));
244
+ logActionToMatrix(ctx, {
245
+ name: SIGN_TRANSACTION_ACTION_NAME,
246
+ args,
247
+ result,
248
+ success: result.status === 'signed',
249
+ ...('error' in result ? { error: result.error } : {}),
250
+ });
251
+ return result;
252
+ }
253
+
254
+ export function createIxoTransactionTools(
255
+ options: IxoTransactionToolOptions,
256
+ ): PluginTool[] {
257
+ return [
258
+ tool(
259
+ async (input) => {
260
+ const { messageType } = RouteListToolSchema.parse(input);
261
+ return listRoutes(messageType);
262
+ },
263
+ {
264
+ name: 'list_ixo_transaction_routes',
265
+ description:
266
+ 'List the supported IXO transaction routes with their fields, risk level and risks. Read-only.',
267
+ schema: RouteListToolSchema,
268
+ effect: 'read',
269
+ },
270
+ ),
271
+ tool(
272
+ async (input) => {
273
+ const { input: text } = IntentInputToolSchema.parse(input);
274
+ try {
275
+ return { status: 'resolved', intent: classifyIntent(text) };
276
+ } catch (error) {
277
+ return { status: 'unresolved', error: errorText(error) };
278
+ }
279
+ },
280
+ {
281
+ name: 'classify_ixo_transaction_intent',
282
+ description:
283
+ 'Resolve an IXO transaction route from a slash command, Msg name, typeUrl, or a natural-language request. Read-only.',
284
+ schema: IntentInputToolSchema,
285
+ effect: 'read',
286
+ },
287
+ ),
288
+ tool(
289
+ async (input, ctx) => {
290
+ let validated: ReturnType<typeof validateTransactionDraft>;
291
+ try {
292
+ validated = validateTransactionDraft(input, {
293
+ allowMainnet: options.allowMainnet,
294
+ });
295
+ } catch (error) {
296
+ return { status: 'invalid', error: describeValidationError(error) };
297
+ }
298
+ const receiptProblem = await mainnetReceiptProblem(
299
+ ctx,
300
+ options,
301
+ validated.network,
302
+ testnetReceiptOf(input),
303
+ validated.message,
304
+ );
305
+ if (receiptProblem !== null) {
306
+ return { status: 'invalid', error: receiptProblem };
307
+ }
308
+ return {
309
+ status: 'valid',
310
+ ...validated,
311
+ chainId: options.chainIds[validated.network],
312
+ };
313
+ },
314
+ {
315
+ name: 'validate_ixo_transaction_draft',
316
+ description:
317
+ 'Strictly validate an IXO transaction draft WITHOUT signing: returns the canonical message, its risks and whether the user must confirm them, or what is wrong. Read-only.',
318
+ schema: TransactionDraftInputSchema,
319
+ effect: 'read',
320
+ },
321
+ ),
322
+ tool((input, ctx) => signIxoTransaction(input, ctx, options), {
323
+ name: 'sign_ixo_transaction',
324
+ description:
325
+ "Send a validated IXO transaction to the user's Portal wallet for them to sign, and return the outcome: signed (with the tx hash; on testnet also a testnetReceipt), failed (on chain, with hash and code), rejected, timeout (outcome unknown), error, validation_error or unavailable. Only after the user accepted every listed risk; quote each one exactly in riskConfirmation.acceptedRisks. Network defaults to testnet. Mainnet (when the oracle allows it) needs the testnetReceipt of the same transaction signed on testnet. The oracle never signs or broadcasts; the user does, in their wallet.",
326
+ schema: TransactionDraftInputSchema,
327
+ }),
328
+ ];
329
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The oracle half of the `sign_transaction` round trip, shared by every
3
+ * plugin that asks the user's Portal wallet to sign (`sign_ixo_transaction`,
4
+ * the POD creator's `request_pod_signature`): send the validated action args
5
+ * over the realtime channel and read the answer through the action's result
6
+ * contract. The oracle never signs or broadcasts; the wallet does.
7
+ */
8
+ import {
9
+ SIGN_TRANSACTION_ACTION_NAME,
10
+ SignTransactionActionResultSchema,
11
+ normalizeWalletSignResult,
12
+ type SignTransactionActionArgs,
13
+ } from '@ixo/ixo-transaction';
14
+ import { reportsUnknownOutcome } from '@ixo/common/ai/frontend-bridge';
15
+ import type {
16
+ FrontendCallSurface,
17
+ RuntimeContext,
18
+ } from '../../plugin-api/types';
19
+
20
+ /** How a signing request ended, before a plugin adds its own context. */
21
+ export type WalletSignatureOutcome =
22
+ | {
23
+ status: 'signed';
24
+ transactionHash?: string;
25
+ code?: number;
26
+ height?: string | number;
27
+ }
28
+ | {
29
+ /** Included in a block but failed: the hash and code are real. */
30
+ status: 'failed';
31
+ code: number;
32
+ transactionHash?: string;
33
+ height?: string | number;
34
+ error: string;
35
+ }
36
+ | { status: 'rejected' | 'error'; error: string }
37
+ | {
38
+ status: 'timeout';
39
+ /**
40
+ * The wallet may still sign after the oracle stopped waiting. The
41
+ * tool-execution middleware reads `outcome: 'unknown'`: the write claim
42
+ * stays and the same request is not dispatched again in the thread.
43
+ */
44
+ outcome: 'unknown';
45
+ error: string;
46
+ }
47
+ | { status: 'unavailable'; error: string };
48
+
49
+ /** The client SDK's answer to an `action_call` for an action it has no handler for. */
50
+ const MISSING_HANDLER = `Action tool ${SIGN_TRANSACTION_ACTION_NAME} not found`;
51
+
52
+ /**
53
+ * A wallet's refusal as its error text words it. SignX reports no structured
54
+ * reason, so a refusal is recognised from the message.
55
+ */
56
+ const WALLET_REFUSAL = /\b(?:reject|denied|declin|cancel)/i;
57
+
58
+ function errorText(error: unknown): string {
59
+ return error instanceof Error ? error.message : String(error);
60
+ }
61
+
62
+ type WalletChannel =
63
+ | { ok: true; frontend: FrontendCallSurface; sessionId: string }
64
+ | { ok: false; reason: string };
65
+
66
+ /**
67
+ * The turn's way to the wallet: a session with a realtime channel and a
68
+ * Portal browser connected to it.
69
+ */
70
+ function walletChannel(ctx: RuntimeContext): WalletChannel {
71
+ const sessionId = ctx.session.id;
72
+ const frontend = ctx.frontend;
73
+ if (!sessionId || !frontend) {
74
+ return {
75
+ ok: false,
76
+ reason:
77
+ 'Wallet signing needs a Portal session with a realtime connection, which this conversation does not have.',
78
+ };
79
+ }
80
+ if (!frontend.hasClient(sessionId)) {
81
+ return {
82
+ ok: false,
83
+ reason:
84
+ "No Portal browser is connected to this session, so the transaction cannot reach the user's wallet. Ask the user to open the chat in the Portal.",
85
+ };
86
+ }
87
+ return { ok: true, frontend, sessionId };
88
+ }
89
+
90
+ /**
91
+ * Why a signing request cannot reach the user's wallet from this turn, or
92
+ * null when it can. Nothing is sent either way.
93
+ */
94
+ export function walletUnavailableReason(ctx: RuntimeContext): string | null {
95
+ const channel = walletChannel(ctx);
96
+ return channel.ok ? null : channel.reason;
97
+ }
98
+
99
+ /** Map a settled bridge call (or its rejection) to an outcome. */
100
+ function settledOutcome(
101
+ outcome: { ok: true; value: unknown } | { ok: false; error: string },
102
+ timeoutMs: number,
103
+ ): WalletSignatureOutcome {
104
+ if (outcome.ok && reportsUnknownOutcome(outcome.value)) {
105
+ // The bridge's own answer when none arrived: the deadline passed, the
106
+ // socket the request went to is gone, or the turn ended after the
107
+ // request was sent. The wallet may still have signed.
108
+ return {
109
+ status: 'timeout',
110
+ outcome: 'unknown',
111
+ error: `The Portal wallet did not answer within ${timeoutMs / 1000} s, or its tab disconnected, and the request timed out. The outcome is unknown: the user may still sign it in their wallet. Do not send it again; ask the user whether it went through.`,
112
+ };
113
+ }
114
+ if (outcome.ok) {
115
+ // The Portal handler answers with the result contract; anything else is
116
+ // a raw wallet response from a Portal with its own handler.
117
+ const contract = SignTransactionActionResultSchema.safeParse(outcome.value);
118
+ const summary = contract.success
119
+ ? contract.data
120
+ : normalizeWalletSignResult(outcome.value);
121
+ if (summary.delivered) {
122
+ return {
123
+ status: 'failed',
124
+ code: summary.delivered.code,
125
+ ...(summary.delivered.transactionHash !== undefined
126
+ ? { transactionHash: summary.delivered.transactionHash }
127
+ : {}),
128
+ ...(summary.delivered.height !== undefined
129
+ ? { height: summary.delivered.height }
130
+ : {}),
131
+ error:
132
+ summary.error ??
133
+ `The transaction failed on chain with code ${summary.delivered.code}`,
134
+ };
135
+ }
136
+ if (!summary.success) {
137
+ return {
138
+ status: 'error',
139
+ error: summary.error ?? 'The wallet reported a failed transaction',
140
+ };
141
+ }
142
+ return {
143
+ status: 'signed',
144
+ ...(summary.transactionHash !== undefined
145
+ ? { transactionHash: summary.transactionHash }
146
+ : {}),
147
+ ...(summary.code !== undefined ? { code: summary.code } : {}),
148
+ ...(summary.height !== undefined ? { height: summary.height } : {}),
149
+ };
150
+ }
151
+ if (outcome.error === MISSING_HANDLER) {
152
+ return {
153
+ status: 'unavailable',
154
+ error:
155
+ 'This Portal does not handle wallet signing (no sign_transaction handler is registered), so nothing was sent to a wallet.',
156
+ };
157
+ }
158
+ // A rejected call carries only the Portal's error text (a delivered
159
+ // transaction comes back as `failed` above, with its code and hash), so a
160
+ // refusal is read from that text — never from a chain log.
161
+ return {
162
+ status: WALLET_REFUSAL.test(outcome.error) ? 'rejected' : 'error',
163
+ error: outcome.error,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Send `sign_transaction` to the session's Portal tab and wait for the
169
+ * wallet. The call is bound to the one socket it was sent to and ends as an
170
+ * unknown outcome on the deadline, a lost socket or the turn's abort after
171
+ * it was sent. Without a way to the wallet it is `unavailable` and nothing
172
+ * is sent.
173
+ */
174
+ export async function requestWalletSignature(
175
+ ctx: RuntimeContext,
176
+ request: {
177
+ args: SignTransactionActionArgs;
178
+ /** Readable prefix of the call id, e.g. `ixo_tx`. */
179
+ callIdPrefix: string;
180
+ timeoutMs: number;
181
+ },
182
+ ): Promise<WalletSignatureOutcome> {
183
+ const channel = walletChannel(ctx);
184
+ if (!channel.ok) return { status: 'unavailable', error: channel.reason };
185
+ const toolCallId = `${request.callIdPrefix}_${ctx.session.requestId || 'noreq'}_${crypto
186
+ .randomUUID()
187
+ .slice(0, 8)}`;
188
+ const outcome = await channel.frontend
189
+ .callAgAction({
190
+ sessionId: channel.sessionId,
191
+ toolCallId,
192
+ toolName: SIGN_TRANSACTION_ACTION_NAME,
193
+ args: request.args,
194
+ timeoutMs: request.timeoutMs,
195
+ signal: ctx.abortSignal,
196
+ })
197
+ .then(
198
+ (value) => ({ ok: true as const, value }),
199
+ (error: unknown) => ({ ok: false as const, error: errorText(error) }),
200
+ );
201
+ return settledOutcome(outcome, request.timeoutMs);
202
+ }
@@ -24,7 +24,22 @@ export interface RawMcpClientTool {
24
24
  name: string;
25
25
  description: string;
26
26
  schema?: unknown;
27
- invoke(input: unknown): Promise<unknown>;
27
+ invoke(
28
+ input: unknown,
29
+ config?: { metadata?: Record<string, unknown> },
30
+ ): Promise<unknown>;
31
+ }
32
+
33
+ export interface AdaptMcpClientToolsOptions {
34
+ /**
35
+ * Timeout of each tool call's MCP request. `@langchain/mcp-adapters` reads
36
+ * `metadata.timeoutMs` and hands it to the MCP SDK as the request's
37
+ * `timeout`, a timer the SDK clears when the response arrives; without it
38
+ * the SDK's 60 s default applies. Never pass a numeric `timeout` in the
39
+ * call config instead: LangChain turns that into `AbortSignal.timeout`,
40
+ * whose timer outlives the call.
41
+ */
42
+ requestTimeoutMs?: number;
28
43
  }
29
44
 
30
45
  /** An upstream MCP tool with its schema normalised to Zod. */
@@ -77,11 +92,17 @@ export function mcpToolZodSchema(
77
92
  export function adaptMcpClientTools(
78
93
  tools: RawMcpClientTool[],
79
94
  logger: Logger = NOOP_LOGGER,
95
+ options: AdaptMcpClientToolsOptions = {},
80
96
  ): AdaptedMcpTool[] {
97
+ const { requestTimeoutMs } = options;
98
+ const config =
99
+ requestTimeoutMs === undefined
100
+ ? undefined
101
+ : { metadata: { timeoutMs: requestTimeoutMs } };
81
102
  return tools.map((t) => ({
82
103
  name: t.name,
83
104
  description: t.description,
84
105
  schema: mcpToolZodSchema(t.schema, t.name, logger),
85
- invoke: (input: unknown) => t.invoke(input),
106
+ invoke: (input: unknown) => t.invoke(input, config),
86
107
  }));
87
108
  }
@@ -98,7 +98,10 @@ export function clearMemoryToolDefsCache(): void {
98
98
  * for the life of the isolate.
99
99
  */
100
100
  const IDLE_CLIENT_CLOSE_MS = 5 * 60 * 1000;
101
- /** Our own per-call limit (never given to the SDK — see `mcp-call-timeout.ts`). */
101
+ /**
102
+ * Per-call limit: the MCP request's own timeout (`metadata.timeoutMs`, see
103
+ * `mcp-tool-adapter.ts`), also enforced by `withCallTimeout`.
104
+ */
102
105
  const MEMORY_TOOL_TIMEOUT_MS = 420_000;
103
106
 
104
107
  function createMemoryMcpClient(
@@ -303,7 +306,9 @@ function buildLazyUpstreamTools(
303
306
  connection = (async () => {
304
307
  const client = createMemoryMcpClient(memoryMcpUrl, headers);
305
308
  try {
306
- const tools = adaptMcpClientTools(await client.getTools(), console);
309
+ const tools = adaptMcpClientTools(await client.getTools(), console, {
310
+ requestTimeoutMs: MEMORY_TOOL_TIMEOUT_MS,
311
+ });
307
312
  return { client, byName: new Map(tools.map((t) => [t.name, t])) };
308
313
  } catch (error) {
309
314
  await client.close().catch(() => undefined);
@@ -1,4 +1,5 @@
1
1
  import type { RuntimeContext } from '../../plugin-api/types';
2
+ import { MEMORY_CAPABILITY } from '../delegated-capabilities';
2
3
  import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
3
4
 
4
5
  /**
@@ -31,11 +32,11 @@ export async function buildMemoryHeaders(
31
32
 
32
33
  const invocation = await mintInvocationSafely(
33
34
  runCtx,
34
- { did: memoryDid, capability: 'ixo:memory' },
35
+ { did: memoryDid, capability: MEMORY_CAPABILITY.resource },
35
36
  'memory',
36
37
  // Claim the ability the user's delegation actually grants. A `'*'` claim is
37
38
  // satisfiable only by a `'*'` grant — `'*'.startsWith('memory/')` is false.
38
- { can: 'memory/*' },
39
+ { can: MEMORY_CAPABILITY.action },
39
40
  );
40
41
  if (!invocation) return null;
41
42
 
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { OraclePlugin, type PluginEnv } from '../../plugin-api/oracle-plugin';
3
+ import { MEMORY_CAPABILITY } from '../delegated-capabilities';
3
4
  import type {
4
5
  MergedConfig,
5
6
  PluginManifest,
@@ -42,6 +43,7 @@ const manifest: PluginManifest = {
42
43
  category: 'memory',
43
44
  visibility: 'always',
44
45
  stability: 'stable',
46
+ requires: [MEMORY_CAPABILITY],
45
47
  };
46
48
 
47
49
  export interface MemoryPluginOptions {