@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
@@ -21,6 +21,7 @@ import type {
21
21
  } from '@ixo/editor/core';
22
22
  import { canToType, getActionByCan, typeToCan } from '@ixo/editor/core';
23
23
  import { FlowError } from './errors';
24
+ import { rewriteRefTokens, wholeRef } from './ref-syntax';
24
25
  import type { Condition, FlowSpecInput, FlowStep } from './types';
25
26
 
26
27
  /** The compiler's block-id convention (verified: `generateBlockId`). */
@@ -49,8 +50,6 @@ const EVALUATOR_TO_OP: Record<string, Condition['is']> = Object.fromEntries(
49
50
  ]),
50
51
  );
51
52
 
52
- const REF_PATTERN = /^\{\{\s*(.+?)\s*\}\}$/;
53
-
54
53
  /**
55
54
  * Data refs cross an id boundary. The agent works in STEP ids
56
55
  * ("fill-form.output.answers.did"), but the flow engine resolves a `$ref` by
@@ -84,17 +83,14 @@ function blockRefToStepRef(ref: string): string {
84
83
  * are filled downstream against the same block-id-keyed runtime state; they must
85
84
  * therefore carry the block id, or every "{{...}}" resolves to an empty string.
86
85
  * Non-output tokens (handlebars helpers like `{{#if}}`, `trigger.payload.*`)
87
- * contain no ".output." and pass through untouched.
86
+ * contain no ".output." and pass through untouched. The scan is linear in the
87
+ * string length (ref-syntax.ts), because it runs on document content.
88
88
  */
89
89
  function rewriteEmbeddedRefs(
90
90
  text: string,
91
91
  mapRef: (ref: string) => string,
92
92
  ): string {
93
- return text.replace(
94
- /(\{\{\s*)([^{}]+?)(\s*\}\})/g,
95
- (_full, open: string, ref: string, close: string) =>
96
- `${open}${mapRef(ref)}${close}`,
97
- );
93
+ return rewriteRefTokens(text, mapRef);
98
94
  }
99
95
 
100
96
  /** Resolve a friendly action name (registry `type`) to its `can` ability string. */
@@ -130,8 +126,8 @@ function isRuntimeRef(value: unknown): value is { $ref: string } {
130
126
  */
131
127
  function friendlyValueToNb(value: unknown): unknown {
132
128
  if (typeof value === 'string') {
133
- const match = REF_PATTERN.exec(value);
134
- if (match?.[1]) return { $ref: stepRefToBlockRef(match[1]) };
129
+ const ref = wholeRef(value);
130
+ if (ref) return { $ref: stepRefToBlockRef(ref) };
135
131
  return value.includes('{{')
136
132
  ? rewriteEmbeddedRefs(value, stepRefToBlockRef)
137
133
  : value;
@@ -204,6 +200,7 @@ export function buildConditionsProp(conditions: Condition[]): string {
204
200
  name: `Condition from ${c.fromStep}`,
205
201
  sourceBlockId: stepIdToBlockId(c.fromStep),
206
202
  sourceBlockType: 'action',
203
+ source: c.source,
207
204
  rule: {
208
205
  type: 'property_value',
209
206
  property: c.field,
@@ -243,6 +240,12 @@ export function parseConditionsProp(
243
240
  const friendlyOp = EVALUATOR_TO_OP[String(rule.operator)];
244
241
  if (!friendlyOp) continue;
245
242
  out.push({
243
+ // An untagged stored condition is evaluated against runtime output, so
244
+ // it reads as one and round-trips through update_step unchanged in meaning.
245
+ source:
246
+ entry.source === 'configured_input'
247
+ ? 'configured_input'
248
+ : 'runtime_output',
246
249
  fromStep: resolveStep(sourceBlockId),
247
250
  field: String(rule.property),
248
251
  is: friendlyOp,
@@ -9,6 +9,22 @@
9
9
  * same shape augmented with a read-only `status`.
10
10
  */
11
11
  import { z } from 'zod';
12
+ import {
13
+ MAX_CONDITIONS_PER_STEP,
14
+ MAX_DESCRIPTION_CHARS,
15
+ MAX_DID_CHARS,
16
+ MAX_FLOW_SPEC_BYTES,
17
+ MAX_FLOW_STEPS,
18
+ MAX_NAME_CHARS,
19
+ MAX_SKILLS_PER_STEP,
20
+ MAX_STEP_ID_CHARS,
21
+ MAX_STEP_INPUTS,
22
+ MAX_STEP_INPUTS_BYTES,
23
+ MAX_STEP_SPEC_BYTES,
24
+ jsonByteLength,
25
+ secretLiteralMessage,
26
+ secretLiteralPaths,
27
+ } from './input-policy';
12
28
 
13
29
  /**
14
30
  * Condition operators, aligned 1:1 with the FE condition evaluator's
@@ -26,11 +42,69 @@ export const CONDITION_OPERATORS = [
26
42
  'isNotEmpty',
27
43
  ] as const;
28
44
 
45
+ const nameSchema = z
46
+ .string()
47
+ .max(MAX_NAME_CHARS, `At most ${MAX_NAME_CHARS} characters.`);
48
+ const descriptionSchema = z
49
+ .string()
50
+ .max(MAX_DESCRIPTION_CHARS, `At most ${MAX_DESCRIPTION_CHARS} characters.`);
51
+
52
+ /**
53
+ * A step id. It is embedded in `{{<id>.output.<field>}}` references and in
54
+ * block ids, so it may not contain whitespace or braces.
55
+ */
56
+ export const stepIdSchema = z
57
+ .string()
58
+ .min(1)
59
+ .max(
60
+ MAX_STEP_ID_CHARS,
61
+ `A step id is at most ${MAX_STEP_ID_CHARS} characters.`,
62
+ )
63
+ .regex(/^[^\s{}]+$/, 'A step id may not contain spaces or braces.');
64
+
65
+ /** A DID (`did:<method>:<id>`). */
66
+ export const didSchema = z
67
+ .string()
68
+ .max(MAX_DID_CHARS, `A DID is at most ${MAX_DID_CHARS} characters.`)
69
+ .regex(/^did:[a-z0-9]+:\S+$/, 'Must be a DID, e.g. "did:ixo:ixo1…".');
70
+
71
+ /**
72
+ * A step's inputs: at most {@link MAX_STEP_INPUTS} names and
73
+ * {@link MAX_STEP_INPUTS_BYTES} UTF-8 bytes of JSON, and no secret (PIN,
74
+ * mnemonic, password, token, API key) other than a step-output reference.
75
+ * Whether that step exists is checked where the flow is known.
76
+ */
77
+ export const stepInputsSchema = z
78
+ .record(z.string().max(MAX_NAME_CHARS), z.unknown())
79
+ .superRefine((inputs, issue) => {
80
+ if (Object.keys(inputs).length > MAX_STEP_INPUTS)
81
+ issue.addIssue({
82
+ code: 'custom',
83
+ message: `A step may have at most ${MAX_STEP_INPUTS} inputs.`,
84
+ });
85
+ if (jsonByteLength(inputs) > MAX_STEP_INPUTS_BYTES)
86
+ issue.addIssue({
87
+ code: 'custom',
88
+ message: `A step's inputs may be at most ${MAX_STEP_INPUTS_BYTES} bytes of JSON.`,
89
+ });
90
+ const secrets = secretLiteralPaths(inputs);
91
+ if (secrets.length > 0)
92
+ issue.addIssue({
93
+ code: 'custom',
94
+ message: secretLiteralMessage(secrets),
95
+ });
96
+ });
97
+
29
98
  export const conditionSchema = z.object({
30
- fromStep: z
31
- .string()
32
- .describe('Id of the upstream step whose value is checked.'),
33
- field: z.string().describe('Field on the upstream step to inspect.'),
99
+ source: z
100
+ .enum(['configured_input', 'runtime_output'])
101
+ .describe(
102
+ 'Read authored inputs or successful runtime output. Required for newly authored conditions.',
103
+ ),
104
+ fromStep: stepIdSchema.describe(
105
+ 'Id of the upstream step whose value is checked.',
106
+ ),
107
+ field: nameSchema.describe('Field on the upstream step to inspect.'),
34
108
  is: z.enum(CONDITION_OPERATORS).describe('Comparison operator.'),
35
109
  value: z
36
110
  .unknown()
@@ -38,28 +112,36 @@ export const conditionSchema = z.object({
38
112
  .describe('Value to compare against (omit for isEmpty/isNotEmpty).'),
39
113
  });
40
114
 
115
+ export const semanticGateSchema = z
116
+ .object({
117
+ version: z.literal(1),
118
+ decision: z.literal('flow.gate.semantic'),
119
+ criterion: z.string().min(1).max(4000),
120
+ rubric: z.string().min(1).max(4000),
121
+ inputFields: z.array(z.string().min(1).max(512)).min(1).max(50),
122
+ })
123
+ .strict();
124
+
41
125
  export const hookSchema = z.object({
42
126
  type: z.enum(['sendEmail', 'addLinkedEntity', 'sendMatrixDM']),
43
- config: z.record(z.string(), z.unknown()),
127
+ config: z.record(z.string().max(MAX_NAME_CHARS), z.unknown()),
44
128
  });
45
129
 
46
130
  export const dueSchema = z.object({
47
- at: z.string().optional().describe('Absolute due date (ISO 8601).'),
48
- within: z
49
- .string()
131
+ at: nameSchema.optional().describe('Absolute due date (ISO 8601).'),
132
+ within: nameSchema
50
133
  .optional()
51
134
  .describe(
52
135
  'Duration from when the step becomes active (ISO 8601 duration).',
53
136
  ),
54
- afterCommitment: z
55
- .string()
137
+ afterCommitment: nameSchema
56
138
  .optional()
57
139
  .describe('Duration from commitment (ISO 8601 duration).'),
58
140
  });
59
141
 
60
142
  export const onEventSchema = z.object({
61
- fromStep: z.string().describe('Upstream step that emits the event.'),
62
- event: z.string().describe('Event name the upstream step emits.'),
143
+ fromStep: stepIdSchema.describe('Upstream step that emits the event.'),
144
+ event: nameSchema.describe('Event name the upstream step emits.'),
63
145
  });
64
146
 
65
147
  /**
@@ -68,18 +150,15 @@ export const onEventSchema = z.object({
68
150
  * is only attached on read (see FlowStepRead).
69
151
  */
70
152
  export const flowStepSchema = z.object({
71
- id: z
72
- .string()
73
- .min(1)
74
- .describe('Stable, human-readable step id, e.g. "load-batches".'),
75
- action: z
76
- .string()
153
+ id: stepIdSchema.describe(
154
+ 'Stable, human-readable step id, e.g. "load-batches".',
155
+ ),
156
+ action: nameSchema
77
157
  .min(1)
78
158
  .describe('Action name from list_actions (e.g. "qi/email.send").'),
79
- title: z.string().optional(),
80
- description: z.string().optional(),
81
- phase: z
82
- .string()
159
+ title: nameSchema.optional(),
160
+ description: descriptionSchema.optional(),
161
+ phase: nameSchema
83
162
  .min(1)
84
163
  .optional()
85
164
  .describe(
@@ -92,11 +171,11 @@ export const flowStepSchema = z.object({
92
171
  'Whether only a person may run this step or a suitably skilled, authorised Flow Agent may run it.',
93
172
  ),
94
173
 
95
- inputs: z
96
- .record(z.string(), z.unknown())
174
+ inputs: stepInputsSchema
97
175
  .optional()
98
176
  .describe(
99
- 'Inputs for the action. A value may be a reference to an upstream output, written as "{{step-id.output.field}}".',
177
+ 'Inputs for the action. A value may be a reference to an upstream output, written as "{{step-id.output.field}}". ' +
178
+ 'Never put a PIN, mnemonic, password, token or API key here: the portal collects those when the user runs the step.',
100
179
  ),
101
180
  form: z
102
181
  .record(z.string(), z.unknown())
@@ -106,7 +185,8 @@ export const flowStepSchema = z.object({
106
185
  ),
107
186
 
108
187
  after: z
109
- .array(z.string())
188
+ .array(stepIdSchema)
189
+ .max(MAX_FLOW_STEPS)
110
190
  .optional()
111
191
  .describe(
112
192
  'Order this step after the named steps. Pair with an input reference for a real data dependency. This is ordering only, not an auto-trigger.',
@@ -114,12 +194,21 @@ export const flowStepSchema = z.object({
114
194
  runWhen: conditionSchema
115
195
  .optional()
116
196
  .describe(
117
- 'Gate this step on an upstream value (static configuration only).',
197
+ 'Gate this step on an upstream configured input or runtime output.',
118
198
  ),
119
199
  conditions: z
120
200
  .array(conditionSchema)
201
+ .max(
202
+ MAX_CONDITIONS_PER_STEP,
203
+ `A step may have at most ${MAX_CONDITIONS_PER_STEP} conditions.`,
204
+ )
121
205
  .optional()
122
206
  .describe('Multiple activation gates (all must pass).'),
207
+ semanticGate: semanticGateSchema
208
+ .optional()
209
+ .describe(
210
+ 'Additional versioned semantic gate; cannot override deterministic conditions or authority.',
211
+ ),
123
212
  onEvent: onEventSchema
124
213
  .optional()
125
214
  .describe(
@@ -132,12 +221,13 @@ export const flowStepSchema = z.object({
132
221
  .describe('When the step runs. Default "manual".'),
133
222
  due: dueSchema.optional(),
134
223
 
135
- assignTo: z
136
- .string()
224
+ assignTo: didSchema
137
225
  .optional()
138
- .describe('Assignee (a DID or known alias) — metadata only.'),
139
- commitTo: z
140
- .string()
226
+ .describe(
227
+ 'The DID assigned to this step. Assigning AUTHORISES that DID to run the step (it is written to the ' +
228
+ "step's authorised actors) — only assign a DID the user named and confirmed.",
229
+ ),
230
+ commitTo: nameSchema
141
231
  .optional()
142
232
  .describe('Commitment window (ISO 8601 duration).'),
143
233
 
@@ -146,7 +236,11 @@ export const flowStepSchema = z.object({
146
236
  .optional()
147
237
  .describe('Lifecycle hooks: event name -> hooks.'),
148
238
  skills: z
149
- .array(z.string())
239
+ .array(nameSchema)
240
+ .max(
241
+ MAX_SKILLS_PER_STEP,
242
+ `A step may require at most ${MAX_SKILLS_PER_STEP} skills.`,
243
+ )
150
244
  .optional()
151
245
  .describe(
152
246
  'Governed skill ids required for agent-capable execution. The first is used for Flow Agent matching.',
@@ -157,19 +251,50 @@ export const flowStepSchema = z.object({
157
251
  .describe('Hint the portal to force a confirmation before this step runs.'),
158
252
  });
159
253
 
254
+ /**
255
+ * {@link flowStepSchema} plus the whole-step size cap. A separate schema so
256
+ * `flowStepSchema` stays a plain object that `.partial()` can derive from.
257
+ */
258
+ export const sizedFlowStepSchema = flowStepSchema.refine(
259
+ (step) => jsonByteLength(step) <= MAX_STEP_SPEC_BYTES,
260
+ `A step may be at most ${MAX_STEP_SPEC_BYTES} bytes of JSON.`,
261
+ );
262
+
160
263
  /**
161
264
  * A whole flow as the agent authors it. `ref` is the opaque flow handle; it is
162
265
  * omitted on create and assigned by the plugin.
163
266
  */
164
- export const flowSpecSchema = z.object({
165
- ref: z
166
- .string()
167
- .optional()
168
- .describe('Opaque flow handle. Omit when creating; the plugin assigns it.'),
169
- title: z.string().min(1),
170
- goal: z.string().optional(),
171
- steps: z.array(flowStepSchema),
172
- });
267
+ export const flowSpecSchema = z
268
+ .object({
269
+ ref: z
270
+ .string()
271
+ .optional()
272
+ .describe(
273
+ 'Opaque flow handle. Omit when creating; the plugin assigns it.',
274
+ ),
275
+ title: nameSchema.min(1),
276
+ goal: descriptionSchema.optional(),
277
+ steps: z
278
+ .array(sizedFlowStepSchema)
279
+ .max(MAX_FLOW_STEPS, `A flow may have at most ${MAX_FLOW_STEPS} steps.`),
280
+ })
281
+ .refine(
282
+ (flow) => jsonByteLength(flow) <= MAX_FLOW_SPEC_BYTES,
283
+ `A flow may be at most ${MAX_FLOW_SPEC_BYTES} bytes of JSON; build a larger flow step by step with add_step.`,
284
+ )
285
+ .superRefine((flow, issue) => {
286
+ // A secret reference must point at a step of this flow.
287
+ const stepIds = new Set(flow.steps.map((step) => step.id));
288
+ flow.steps.forEach((step, index) => {
289
+ const secrets = secretLiteralPaths(step.inputs ?? {}, stepIds);
290
+ if (secrets.length > 0)
291
+ issue.addIssue({
292
+ code: 'custom',
293
+ path: ['steps', index, 'inputs'],
294
+ message: secretLiteralMessage(secrets),
295
+ });
296
+ });
297
+ });
173
298
 
174
299
  export type Condition = z.infer<typeof conditionSchema>;
175
300
  export type FlowStep = z.infer<typeof flowStepSchema>;
@@ -2,11 +2,16 @@
2
2
  * Bundled Workers-runtime plugins — ports of the Node runtime's MCP / fetch
3
3
  * plugins. Each is gated the same way as on Node: `autoDetect` on its env
4
4
  * key where one exists (memory / sandbox / firecrawl / composio), always-on
5
- * otherwise (domain-indexer; vfs self-gates on `ctx.ucan.hasSigningKey()`).
5
+ * otherwise (domain-indexer; vfs self-gates on `ctx.ucan.hasSigningKey()`;
6
+ * pod-creator is on-demand and stays hidden until `load_capability`).
6
7
  *
7
8
  * A host opts in by spreading `BUNDLED_WORKERS_PLUGINS` into
8
9
  * `createOracleWorker({ plugins })` — plugins whose env keys are absent are
9
10
  * skipped by their `autoDetect`.
11
+ *
12
+ * Shipped but opt-in (not in the bundle): `IxoTransactionPlugin`, which asks
13
+ * the user's Portal wallet to sign chain transactions; an oracle adds
14
+ * `new IxoTransactionPlugin()` itself.
10
15
  */
11
16
  import type { OraclePlugin } from '../plugin-api/oracle-plugin';
12
17
  import { ComposioPlugin } from './composio';
@@ -22,6 +27,7 @@ import { PortalPlugin } from './portal';
22
27
  import { AGUIPlugin } from './agui';
23
28
  import { AttachmentsPlugin } from './attachments';
24
29
  import { MatrixGroupChatsPlugin } from './matrix-group-chats';
30
+ import { PodCreatorPlugin } from './pod-creator';
25
31
 
26
32
  export * from './composio';
27
33
  export * from './editor';
@@ -35,6 +41,8 @@ export * from './vfs';
35
41
  export * from './user-preferences';
36
42
  export * from './portal';
37
43
  export * from './agui';
44
+ export * from './ixo-transaction';
45
+ export * from './pod-creator';
38
46
 
39
47
  export const memoryPlugin = new MemoryPlugin();
40
48
  export const sandboxPlugin = new SandboxPlugin();
@@ -48,6 +56,7 @@ export const portalPlugin = new PortalPlugin();
48
56
  export const aguiPlugin = new AGUIPlugin();
49
57
  export const attachmentsPlugin = new AttachmentsPlugin();
50
58
  export const matrixGroupChatsPlugin = new MatrixGroupChatsPlugin();
59
+ export const podCreatorPlugin = new PodCreatorPlugin();
51
60
  /** TasksPlugin is already an object (defineOraclePlugin), not a class. */
52
61
  export const tasksPlugin = TasksPlugin;
53
62
 
@@ -70,4 +79,5 @@ export const BUNDLED_WORKERS_PLUGINS = [
70
79
  aguiPlugin,
71
80
  attachmentsPlugin,
72
81
  matrixGroupChatsPlugin,
82
+ podCreatorPlugin,
73
83
  ] as const satisfies readonly OraclePlugin[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The chain id each network's signing requests name, as plugin config. Every
3
+ * plugin that asks the Portal wallet to sign (`IxoTransactionPlugin`, the POD
4
+ * creator) declares this same schema object, so one set of variables
5
+ * configures them all and the env composition does not report the shared
6
+ * keys as a collision.
7
+ */
8
+ import {
9
+ ChainIdSchema,
10
+ DEFAULT_CHAIN_IDS,
11
+ type Network,
12
+ } from '@ixo/ixo-transaction';
13
+ import { z } from 'zod';
14
+
15
+ /**
16
+ * The defaults are what each network's RPC reports (`DEFAULT_CHAIN_IDS`);
17
+ * override an entry after a chain upgrade changes the id. The Portal refuses
18
+ * a request for another chain than its wallet's.
19
+ */
20
+ export const chainIdConfigSchema = z.object({
21
+ IXO_TRANSACTION_CHAIN_ID_DEVNET: ChainIdSchema.default(
22
+ DEFAULT_CHAIN_IDS.devnet,
23
+ ),
24
+ IXO_TRANSACTION_CHAIN_ID_TESTNET: ChainIdSchema.default(
25
+ DEFAULT_CHAIN_IDS.testnet,
26
+ ),
27
+ IXO_TRANSACTION_CHAIN_ID_MAINNET: ChainIdSchema.default(
28
+ DEFAULT_CHAIN_IDS.mainnet,
29
+ ),
30
+ });
31
+
32
+ export type ChainIdConfig = z.output<typeof chainIdConfigSchema>;
33
+
34
+ /** The chain id per network, from the parsed config. */
35
+ export function chainIdsFromConfig(
36
+ config: ChainIdConfig,
37
+ ): Readonly<Record<Network, string>> {
38
+ return {
39
+ devnet: config.IXO_TRANSACTION_CHAIN_ID_DEVNET,
40
+ testnet: config.IXO_TRANSACTION_CHAIN_ID_TESTNET,
41
+ mainnet: config.IXO_TRANSACTION_CHAIN_ID_MAINNET,
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ export { IxoTransactionPlugin } from './ixo-transaction.plugin';
2
+ export type { SignIxoTransactionResult } from './tools';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * IXO transaction plugin: turns a conversation into a validated IXO chain
3
+ * transaction and asks the user's Portal wallet to sign it. On demand, and
4
+ * opt-in — not part of `BUNDLED_WORKERS_PLUGINS`; an oracle adds
5
+ * `new IxoTransactionPlugin()` to its plugins.
6
+ *
7
+ * The catalog, intent routing and validation come from the runtime-neutral
8
+ * `@ixo/ixo-transaction` package, whose Portal half
9
+ * (`@ixo/ixo-transaction/react`) answers the `sign_transaction` action.
10
+ */
11
+ import { z } from 'zod';
12
+ import { OraclePlugin } from '../../plugin-api/oracle-plugin';
13
+ import type {
14
+ PluginContext,
15
+ PluginManifest,
16
+ PluginTool,
17
+ } from '../../plugin-api/types';
18
+ import { chainIdConfigSchema, chainIdsFromConfig } from './chain-ids';
19
+ import { createIxoTransactionTools } from './tools';
20
+
21
+ /** Default time the user has to sign in their wallet. */
22
+ export const DEFAULT_SIGN_TIMEOUT_MS = 120_000;
23
+ /** Upper bound: well inside the turn's own deadline (`TURN_TIMEOUT_MS`, 10 min by default). */
24
+ export const MAX_SIGN_TIMEOUT_MS = 300_000;
25
+
26
+ const configSchema = z.object({
27
+ /**
28
+ * `'true'` lets mainnet drafts through validation, each still needing a
29
+ * successful testnet receipt or a recorded override. Off by default: the
30
+ * oracle prepares testnet (and devnet) transactions only.
31
+ */
32
+ IXO_TRANSACTION_ALLOW_MAINNET: z.enum(['true', 'false']).default('false'),
33
+ /** The chain id each network's requests name (shared with the POD creator). */
34
+ ...chainIdConfigSchema.shape,
35
+ /** How long `sign_ixo_transaction` waits for the wallet, in ms. */
36
+ IXO_TRANSACTION_SIGN_TIMEOUT_MS: z.coerce
37
+ .number()
38
+ .int()
39
+ .positive()
40
+ .max(MAX_SIGN_TIMEOUT_MS)
41
+ .default(DEFAULT_SIGN_TIMEOUT_MS),
42
+ });
43
+
44
+ const manifest: PluginManifest = {
45
+ title: 'IXO Transaction',
46
+ summary:
47
+ 'Prepares and validates IXO chain transactions; the user signs them in their Portal wallet.',
48
+ whenToUse: [
49
+ 'The user wants to create, update, transfer, submit, evaluate, mint or retire on the IXO chain.',
50
+ 'The user types a slash command such as /ixo entity create or /ixo token retire.',
51
+ 'Classify, collect fields, validate, disclose risks, then sign only after explicit consent.',
52
+ ],
53
+ whenNotToUse: [
54
+ 'Reading chain state or balances: this plugin only prepares writes.',
55
+ 'Asking the oracle to sign itself: only the user signs, in their wallet.',
56
+ 'Bonds, liquid staking or names: not supported yet.',
57
+ ],
58
+ examples: [
59
+ {
60
+ user: 'I want to create a new domain',
61
+ thought:
62
+ 'Resolve the request to a Msg first, then collect its required fields.',
63
+ tool: 'classify_ixo_transaction_intent',
64
+ args: { input: 'I want to create a new domain' },
65
+ },
66
+ {
67
+ user: '/ixo token retire',
68
+ thought:
69
+ 'List the route to see its fields and risks before asking for values.',
70
+ tool: 'list_ixo_transaction_routes',
71
+ args: { messageType: 'token' },
72
+ },
73
+ {
74
+ user: 'Yes, I accept that retiring is permanent. Sign it.',
75
+ thought:
76
+ "Risks disclosed and accepted in the user's words: send the testnet draft to the wallet.",
77
+ tool: 'sign_ixo_transaction',
78
+ args: {
79
+ command: '/ixo token retire',
80
+ network: 'testnet',
81
+ value: {
82
+ owner: 'ixo1qwertyuiopasdfghjklzxcvbnmqwerty12345',
83
+ tokens: [{ id: 'CARBON-1', amount: '10' }],
84
+ jurisdiction: 'ZA',
85
+ reason: 'Offsetting 2026 travel',
86
+ },
87
+ riskConfirmation: {
88
+ confirmed: true,
89
+ acceptedRisks: [
90
+ 'Permanently retires (burns) impact credits. Irreversible.',
91
+ ],
92
+ },
93
+ },
94
+ },
95
+ ],
96
+ tags: ['ixo', 'transaction', 'wallet', 'portal', 'cosmos'],
97
+ category: 'integration',
98
+ visibility: 'on-demand',
99
+ stability: 'experimental',
100
+ };
101
+
102
+ export class IxoTransactionPlugin extends OraclePlugin {
103
+ readonly name = 'ixo-transaction';
104
+
105
+ readonly version = '1.0.0';
106
+
107
+ readonly manifest = manifest;
108
+
109
+ override readonly configSchema = configSchema;
110
+
111
+ override getTools(ctx: PluginContext): PluginTool[] {
112
+ const config = configSchema.parse(ctx.config);
113
+ return createIxoTransactionTools({
114
+ allowMainnet: config.IXO_TRANSACTION_ALLOW_MAINNET === 'true',
115
+ signTimeoutMs: config.IXO_TRANSACTION_SIGN_TIMEOUT_MS,
116
+ chainIds: chainIdsFromConfig(config),
117
+ });
118
+ }
119
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Testnet receipts: the oracle's own record that a user signed a transaction
3
+ * on testnet, which a mainnet draft of the same message must cite.
4
+ *
5
+ * Kept in the user's blob store (`ctx.blobStore`, Durable Object storage of
6
+ * the user's object, scoped to the user's DID). The store hands out its own
7
+ * random ids and keeps an entry at most 24 hours, so a receipt is cited by
8
+ * that id plus the transaction hash, and expires a day after the testnet
9
+ * signing. The model can only cite an id it was given: it cannot make one up,
10
+ * read another user's, or pass off another plugin's blob (the name is
11
+ * checked). Any store failure refuses the mainnet draft.
12
+ */
13
+ import type { ITrxMsg, TestnetReceipt } from '@ixo/ixo-transaction';
14
+ import { z } from 'zod';
15
+ import { operationKey } from '../../core/middlewares/tool-execution';
16
+ import type { RuntimeContext } from '../../plugin-api/types';
17
+
18
+ export const RECEIPT_NAME_PREFIX = 'ixo-transaction/receipt:';
19
+ /** The blob store's maximum lifetime. */
20
+ export const RECEIPT_TTL_SECONDS = 24 * 60 * 60;
21
+
22
+ const ReceiptRecordSchema = z
23
+ .object({
24
+ transactionHash: z.string(),
25
+ digest: z.string(),
26
+ chainId: z.string(),
27
+ })
28
+ .strict();
29
+
30
+ type ReceiptStoreContext = Pick<RuntimeContext, 'blobStore' | 'user'>;
31
+
32
+ /** SHA-256 of the message's canonical form (typeUrl + canonical value). */
33
+ export function messageDigest(message: ITrxMsg): Promise<string> {
34
+ return operationKey(message.typeUrl, message.value);
35
+ }
36
+
37
+ /** Record a signed testnet transaction; returns the receipt to cite. */
38
+ export async function recordTestnetReceipt(
39
+ ctx: ReceiptStoreContext,
40
+ signed: { transactionHash: string; message: ITrxMsg; chainId: string },
41
+ ): Promise<TestnetReceipt> {
42
+ const receiptId = await ctx.blobStore.put({
43
+ userDid: ctx.user.did,
44
+ name: `${RECEIPT_NAME_PREFIX}${signed.transactionHash}`,
45
+ value: JSON.stringify({
46
+ transactionHash: signed.transactionHash,
47
+ digest: await messageDigest(signed.message),
48
+ chainId: signed.chainId,
49
+ }),
50
+ ttlSeconds: RECEIPT_TTL_SECONDS,
51
+ });
52
+ return { transactionHash: signed.transactionHash, receiptId };
53
+ }
54
+
55
+ /**
56
+ * Why `receipt` does not prove a testnet signing of `message` by this user
57
+ * on `testnetChainId`, or null when it does.
58
+ */
59
+ export async function testnetReceiptProblem(
60
+ ctx: ReceiptStoreContext,
61
+ receipt: TestnetReceipt,
62
+ message: ITrxMsg,
63
+ testnetChainId: string,
64
+ ): Promise<string | null> {
65
+ const unknown = `Testnet receipt ${receipt.receiptId} is not one this oracle recorded for this user (or it is older than 24 hours): sign the same transaction on testnet and use the testnetReceipt that returns`;
66
+ if (!ctx.blobStore.isValidBlobId(receipt.receiptId)) return unknown;
67
+ let entry: { name: string; value: string } | null;
68
+ try {
69
+ entry = await ctx.blobStore.get({
70
+ userDid: ctx.user.did,
71
+ blobId: receipt.receiptId,
72
+ });
73
+ } catch (error) {
74
+ return `Testnet receipts cannot be checked right now (${error instanceof Error ? error.message : String(error)}), so the mainnet transaction was refused`;
75
+ }
76
+ if (!entry) return unknown;
77
+
78
+ let parsed: unknown;
79
+ try {
80
+ parsed = JSON.parse(entry.value);
81
+ } catch {
82
+ return unknown;
83
+ }
84
+ const record = ReceiptRecordSchema.safeParse(parsed);
85
+ if (!record.success) return unknown;
86
+ if (
87
+ entry.name !== `${RECEIPT_NAME_PREFIX}${receipt.transactionHash}` ||
88
+ record.data.transactionHash !== receipt.transactionHash
89
+ ) {
90
+ return `Testnet receipt ${receipt.receiptId} is not for transaction ${receipt.transactionHash}`;
91
+ }
92
+ if (record.data.chainId !== testnetChainId) {
93
+ return `Testnet receipt ${receipt.receiptId} was signed on ${record.data.chainId}, not on the testnet chain ${testnetChainId}`;
94
+ }
95
+ if (record.data.digest !== (await messageDigest(message))) {
96
+ return `Testnet receipt ${receipt.receiptId} is for a different transaction: the mainnet draft must be the same message that was signed on testnet`;
97
+ }
98
+ return null;
99
+ }