@ixo/oracle-runtime-workers 0.12.0 → 0.15.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 +14 -9
  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 +267 -0
  18. package/src/channels/contract.ts +137 -0
  19. package/src/channels/turns.ts +270 -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 +198 -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 +1883 -264
  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 +523 -111
  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 +529 -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
@@ -20,10 +20,17 @@ import { tool } from '../../../plugin-api/tool-helper';
20
20
  import type { PluginTool, RuntimeContext } from '../../../plugin-api/types';
21
21
  import { getActionDef, isEventCapable } from '../actions';
22
22
  import {
23
+ checkAssignee,
24
+ checkConditions,
25
+ checkEventTrigger,
26
+ checkStepInputs,
27
+ placeStepBlock,
28
+ requireStep,
23
29
  removeStep,
24
30
  reorderStep,
25
31
  setStepAssignment,
26
32
  setStepConditions,
33
+ setStepSemanticGate,
27
34
  setStepConfirmation,
28
35
  setStepEventTrigger,
29
36
  setStepExecution,
@@ -35,22 +42,28 @@ import {
35
42
  updateFlowMeta,
36
43
  } from '../edit';
37
44
  import { FlowError, toToolError } from '../errors';
45
+ import { MAX_DESCRIPTION_CHARS, MAX_NAME_CHARS } from '../input-policy';
38
46
  import {
39
- requireRoomMembership,
40
47
  resolveFlowRef,
41
- resolveFlowsMatrixClient,
42
48
  withFlowDoc,
49
+ withFlowsCompileClient,
43
50
  } from '../flow-doc';
44
51
  import { readStep } from '../read';
45
52
  import { flowSpecToBaseUcan } from '../translator';
46
53
  import {
47
54
  flowSpecSchema,
48
55
  flowStepSchema,
56
+ semanticGateSchema,
57
+ sizedFlowStepSchema,
58
+ stepIdSchema,
49
59
  type Condition,
50
60
  type FlowSpecInput,
51
61
  type FlowStep,
52
62
  } from './../types';
53
63
 
64
+ const flowSpecTitleSchema = z.string().min(1).max(MAX_NAME_CHARS);
65
+ const flowSpecGoalSchema = z.string().max(MAX_DESCRIPTION_CHARS);
66
+
54
67
  const base = {
55
68
  flowRef: z
56
69
  .string()
@@ -64,9 +77,22 @@ function conditionsOf(step: FlowStep): Condition[] {
64
77
  return [...(step.runWhen ? [step.runWhen] : []), ...(step.conditions ?? [])];
65
78
  }
66
79
 
67
- /** onEvent must point at an event-capable upstream action. Returns error strings. */
80
+ /**
81
+ * Plan-level checks the compiler does not make: a condition must read a step
82
+ * of this flow, and onEvent must point at an event-capable upstream action.
83
+ * Returns error strings.
84
+ */
68
85
  function eventCapabilityErrors(flow: FlowSpecInput): string[] {
69
86
  const errors: string[] = [];
87
+ const ids = new Set(flow.steps.map((s) => s.id));
88
+ for (const step of flow.steps) {
89
+ for (const condition of conditionsOf(step)) {
90
+ if (!ids.has(condition.fromStep))
91
+ errors.push(
92
+ `Step "${step.id}" has a condition on an unknown step "${condition.fromStep}".`,
93
+ );
94
+ }
95
+ }
70
96
  for (const step of flow.steps) {
71
97
  if (!step.onEvent) continue;
72
98
  const source = flow.steps.find((s) => s.id === step.onEvent?.fromStep);
@@ -121,59 +147,58 @@ function buildValidateFlowTool(): PluginTool {
121
147
  );
122
148
  }
123
149
 
124
- /** Apply each step's conditions to an already-authored flow room (post-pass). */
125
- async function applyConditions(
126
- ctx: RuntimeContext,
127
- flowRef: string,
128
- matrixClient: MatrixClient | undefined,
129
- steps: FlowStep[],
130
- ): Promise<void> {
131
- const withConditions = steps.filter((s) => conditionsOf(s).length > 0);
132
- if (withConditions.length === 0) return;
133
- await withFlowDoc(ctx, flowRef, matrixClient, async (doc) => {
134
- for (const step of withConditions)
135
- setStepConditions(doc, step.id, conditionsOf(step));
136
- });
137
- }
138
-
139
150
  /**
140
- * Apply each step's assignee to an already-authored flow room (post-pass).
141
- * The compiler only writes the authorization whitelist; the assignee the portal
142
- * shows + nudges lives in `props.assignment`, written by setStepAssignment.
151
+ * Write what the compiler does not: conditions and the semantic gate (as
152
+ * `props.conditions`, see the module note), the assignee shown by the portal,
153
+ * and the execution boundary and skills. All of it goes into the one document
154
+ * the caller opened, so a tool call loads the flow once for these passes.
143
155
  */
144
- async function applyAssignments(
145
- ctx: RuntimeContext,
146
- flowRef: string,
147
- matrixClient: MatrixClient | undefined,
148
- steps: FlowStep[],
149
- ): Promise<void> {
150
- const withAssignee = steps.filter((s) => s.assignTo);
151
- if (withAssignee.length === 0) return;
152
- await withFlowDoc(ctx, flowRef, matrixClient, async (doc) => {
153
- for (const step of withAssignee)
154
- setStepAssignment(doc, step.id, step.assignTo);
155
- });
156
- }
157
-
158
- /** Apply Flow Agent execution boundaries and governed skill matching metadata. */
159
- async function applyExecutionPolicy(
160
- ctx: RuntimeContext,
161
- flowRef: string,
162
- matrixClient: MatrixClient | undefined,
163
- steps: FlowStep[],
164
- ): Promise<void> {
165
- const configured = steps.filter(
166
- (step) => step.execution !== undefined || step.skills !== undefined,
167
- );
168
- if (configured.length === 0) return;
169
- await withFlowDoc(ctx, flowRef, matrixClient, async (doc) => {
170
- for (const step of configured) {
156
+ function applyPostCompile(doc: YDoc, steps: FlowStep[]): void {
157
+ doc.transact(() => {
158
+ for (const step of steps) {
159
+ const conditions = conditionsOf(step);
160
+ if (conditions.length > 0) setStepConditions(doc, step.id, conditions);
161
+ if (step.semanticGate)
162
+ setStepSemanticGate(doc, step.id, step.semanticGate);
163
+ // The compiler only writes the authorisation list; the assignee the
164
+ // portal shows and nudges lives in `props.assignment`.
165
+ if (step.assignTo) setStepAssignment(doc, step.id, step.assignTo);
171
166
  if (step.execution) setStepExecution(doc, step.id, step.execution);
172
167
  if (step.skills) setStepSkills(doc, step.id, step.skills);
173
168
  }
174
169
  });
175
170
  }
176
171
 
172
+ /** Whether any step needs {@link applyPostCompile}. */
173
+ function needsPostCompile(steps: FlowStep[]): boolean {
174
+ return steps.some(
175
+ (step) =>
176
+ conditionsOf(step).length > 0 ||
177
+ step.semanticGate !== undefined ||
178
+ step.assignTo !== undefined ||
179
+ step.execution !== undefined ||
180
+ step.skills !== undefined,
181
+ );
182
+ }
183
+
184
+ /** The step ids in `qi.flow.order`. */
185
+ function readOrder(doc: YDoc): string[] {
186
+ return doc
187
+ .getArray('qi.flow.order')
188
+ .toArray()
189
+ .filter((v): v is string => typeof v === 'string');
190
+ }
191
+
192
+ /** Whether the room's document already holds a flow. */
193
+ function holdsFlow(doc: YDoc): boolean {
194
+ return readOrder(doc).length > 0 || doc.getMap('qi.flow.nodes').size > 0;
195
+ }
196
+
197
+ function metaString(doc: YDoc, key: string): string | undefined {
198
+ const value = doc.getMap('qi.flow.meta').get(key);
199
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
200
+ }
201
+
177
202
  /**
178
203
  * Allocate the room to author the template into.
179
204
  *
@@ -218,24 +243,41 @@ function buildCreateTemplateTool(
218
243
  error: { code: 'validation_failed', message: errors.join(' ') },
219
244
  };
220
245
 
221
- const client = await resolveFlowsMatrixClient(ctx, matrixClient);
222
246
  const roomId = allocateTemplateRoom(ctx, flow);
223
- await requireRoomMembership(ctx, roomId);
247
+ // The compiler's full write clears whatever flow the room holds —
248
+ // template, run state and form answers — so an occupied room is
249
+ // refused; changes to an existing flow go through the edit tools.
250
+ // (withFlowDoc also enforces room membership.)
251
+ const occupied = await withFlowDoc(
252
+ ctx,
253
+ roomId,
254
+ matrixClient,
255
+ async (doc) => holdsFlow(doc),
256
+ );
257
+ if (occupied)
258
+ throw new FlowError(
259
+ 'validation_failed',
260
+ 'This room already holds a flow. create_template never replaces one — change it with the edit tools ' +
261
+ '(add_step, update_step, set_step_*, remove_step, reorder_step, connect_steps).',
262
+ );
224
263
 
225
264
  const flowId = `flow-${crypto.randomUUID().slice(0, 8)}`;
226
265
  const plan = flowSpecToBaseUcan(flow, {
227
266
  flowId,
228
267
  ownerDid: ctx.user.did,
229
268
  });
230
- await setupFlowFromBaseUcan({
231
- plan,
232
- roomId,
233
- matrixClient: client,
234
- creatorDid: ctx.user.did,
235
- });
236
- await applyConditions(ctx, roomId, matrixClient, flow.steps);
237
- await applyAssignments(ctx, roomId, matrixClient, flow.steps);
238
- await applyExecutionPolicy(ctx, roomId, matrixClient, flow.steps);
269
+ await withFlowsCompileClient(ctx, matrixClient, (client) =>
270
+ setupFlowFromBaseUcan({
271
+ plan,
272
+ roomId,
273
+ matrixClient: client,
274
+ creatorDid: ctx.user.did,
275
+ }),
276
+ );
277
+ if (needsPostCompile(flow.steps))
278
+ await withFlowDoc(ctx, roomId, matrixClient, async (doc) =>
279
+ applyPostCompile(doc, flow.steps),
280
+ );
239
281
 
240
282
  return { ok: true, flowRef: roomId };
241
283
  } catch (err) {
@@ -246,7 +288,7 @@ function buildCreateTemplateTool(
246
288
  name: 'create_template',
247
289
  description:
248
290
  'Create a BRAND-NEW flow template — a reusable blueprint the user instantiates and runs in ' +
249
- 'the portal. Do NOT use this to change a template that already exists or is currently open — use the editing tools ' +
291
+ 'the portal. Refused when the room already holds a flow. Do NOT use this to change a template that already exists or is currently open — use the editing tools ' +
250
292
  '(add_step, update_step, set_step_*, remove_step, reorder_step, connect_steps), which modify the open template in ' +
251
293
  'place. Validate first with validate_flow.',
252
294
  schema: createTemplateSchema,
@@ -256,12 +298,12 @@ function buildCreateTemplateTool(
256
298
 
257
299
  const addStepSchema = z.object({
258
300
  ...base,
259
- step: flowStepSchema,
301
+ step: sizedFlowStepSchema,
260
302
  position: z
261
303
  .union([
262
304
  z.number().int().nonnegative(),
263
- z.object({ after: z.string() }),
264
- z.object({ before: z.string() }),
305
+ z.object({ after: stepIdSchema }),
306
+ z.object({ before: stepIdSchema }),
265
307
  ])
266
308
  .optional()
267
309
  .describe(
@@ -269,60 +311,101 @@ const addStepSchema = z.object({
269
311
  ),
270
312
  });
271
313
 
314
+ /** Where `position` puts a new step in `order` (which does not yet hold it). */
315
+ function insertionIndex(
316
+ order: readonly string[],
317
+ position: z.infer<typeof addStepSchema>['position'],
318
+ ): number {
319
+ if (position === undefined) return order.length;
320
+ if (typeof position === 'number') return Math.min(position, order.length);
321
+ const anchor = 'after' in position ? position.after : position.before;
322
+ const at = order.indexOf(anchor);
323
+ if (at < 0)
324
+ throw new FlowError('step_not_found', `No step "${anchor}" in this flow.`);
325
+ return 'after' in position ? at + 1 : at;
326
+ }
327
+
272
328
  function buildAddStepTool(matrixClient: MatrixClient | undefined): PluginTool {
273
329
  return tool(
274
330
  async (args, ctx: RuntimeContext) => {
275
331
  try {
276
332
  const { flowRef, step, position } = addStepSchema.parse(args);
277
333
  const roomId = resolveFlowRef(ctx, flowRef);
278
- await requireRoomMembership(ctx, roomId);
279
- const client = await resolveFlowsMatrixClient(ctx, matrixClient);
280
334
 
281
- // Read the existing flow id so the merge targets the same document.
282
- const flowId = await withFlowDoc(
335
+ // What the merge must keep: the compiler's merge rewrites the flow's
336
+ // title and owner from the incoming plan and rebuilds the step order
337
+ // from its node map, so read them first and hand them back.
338
+ const existing = await withFlowDoc(
283
339
  ctx,
284
- flowRef,
340
+ roomId,
285
341
  matrixClient,
286
342
  async (doc) => {
287
- const value = doc.getMap('qi.flow.meta').get('flowId');
288
- return typeof value === 'string' && value.length > 0
289
- ? value
290
- : `flow-${crypto.randomUUID().slice(0, 8)}`;
343
+ const order = readOrder(doc);
344
+ if (!holdsFlow(doc))
345
+ throw new FlowError(
346
+ 'flow_not_found',
347
+ 'There is no flow here yet. Create it with create_template.',
348
+ );
349
+ if (
350
+ doc.getMap('qi.flow.nodes').has(step.id) ||
351
+ order.includes(step.id)
352
+ )
353
+ throw new FlowError(
354
+ 'validation_failed',
355
+ `A step "${step.id}" already exists in this flow. Pick another id, or change that step with update_step.`,
356
+ );
357
+ // Everything the post-compile pass will check, checked now: the
358
+ // compile writes the step, so a later refusal would leave it behind.
359
+ if (step.inputs) checkStepInputs(doc, step.inputs, [step.id]);
360
+ checkConditions(doc, conditionsOf(step), [step.id]);
361
+ if (step.onEvent) checkEventTrigger(doc, step.id, step.onEvent);
362
+ return {
363
+ flowId: metaString(doc, 'flowId'),
364
+ title: metaString(doc, 'title'),
365
+ ownerDid: metaString(doc, 'flowOwnerDid'),
366
+ order,
367
+ };
291
368
  },
292
369
  );
370
+ const order = [...existing.order];
371
+ order.splice(insertionIndex(order, position), 0, step.id);
293
372
 
294
373
  const plan = flowSpecToBaseUcan(
295
- { title: '', steps: [step] },
296
- { flowId, ownerDid: ctx.user.did },
374
+ { title: existing.title ?? '', steps: [step] },
375
+ {
376
+ flowId:
377
+ existing.flowId ?? `flow-${crypto.randomUUID().slice(0, 8)}`,
378
+ ownerDid: existing.ownerDid,
379
+ },
380
+ );
381
+ await withFlowsCompileClient(ctx, matrixClient, (client) =>
382
+ setupFlowFromBaseUcan({
383
+ plan,
384
+ roomId,
385
+ matrixClient: client,
386
+ creatorDid: ctx.user.did,
387
+ strategy: 'merge',
388
+ }),
297
389
  );
298
- await setupFlowFromBaseUcan({
299
- plan,
300
- roomId,
301
- matrixClient: client,
302
- creatorDid: ctx.user.did,
303
- strategy: 'merge',
304
- });
305
- await applyConditions(ctx, roomId, matrixClient, [step]);
306
- await applyAssignments(ctx, roomId, matrixClient, [step]);
307
- await applyExecutionPolicy(ctx, roomId, matrixClient, [step]);
308
390
 
309
- if (typeof position === 'number') {
310
- await withFlowDoc(ctx, flowRef, matrixClient, async (doc) =>
311
- reorderStep(doc, step.id, position),
312
- );
313
- } else if (position) {
314
- await withFlowDoc(ctx, flowRef, matrixClient, async (doc) => {
315
- const order = doc
316
- .getArray('qi.flow.order')
317
- .toArray()
318
- .filter((v): v is string => typeof v === 'string');
319
- const anchor =
320
- 'after' in position ? position.after : position.before;
321
- const idx = order.indexOf(anchor);
322
- if (idx >= 0)
323
- reorderStep(doc, step.id, 'after' in position ? idx + 1 : idx);
391
+ // One load for every post-compile pass: restore the order (with the
392
+ // new step in place), move its block next to its neighbours, and
393
+ // write what the compiler does not.
394
+ await withFlowDoc(ctx, roomId, matrixClient, async (doc) => {
395
+ doc.transact(() => {
396
+ const orderArr = doc.getArray<string>('qi.flow.order');
397
+ const merged = readOrder(doc);
398
+ // Steps another client added meanwhile stay, after the known ones.
399
+ const restored = [
400
+ ...order.filter((id) => merged.includes(id)),
401
+ ...merged.filter((id) => !order.includes(id)),
402
+ ];
403
+ orderArr.delete(0, orderArr.length);
404
+ orderArr.push(restored);
405
+ placeStepBlock(doc, step.id, restored);
406
+ applyPostCompile(doc, [step]);
324
407
  });
325
- }
408
+ });
326
409
  return ok;
327
410
  } catch (err) {
328
411
  return toToolError(err);
@@ -331,41 +414,48 @@ function buildAddStepTool(matrixClient: MatrixClient | undefined): PluginTool {
331
414
  {
332
415
  name: 'add_step',
333
416
  description:
334
- 'Add one step to an existing flow without disturbing the others. Optionally place it at a position.',
417
+ 'Add one step to an existing flow without disturbing the others: the title, owner and step order stay as they are. ' +
418
+ 'Optionally place it at a position. A step id that already exists is refused.',
335
419
  schema: addStepSchema,
336
420
  },
337
421
  );
338
422
  }
339
423
 
340
- const removeSchema = z.object({ ...base, stepId: z.string().min(1) });
424
+ const removeSchema = z.object({ ...base, stepId: stepIdSchema });
341
425
  const reorderSchema = z.object({
342
426
  ...base,
343
- stepId: z.string().min(1),
427
+ stepId: stepIdSchema,
344
428
  toIndex: z.number().int().nonnegative(),
345
429
  });
346
430
  const metaSchema = z.object({
347
431
  ...base,
348
- title: z.string().optional(),
349
- goal: z.string().optional(),
432
+ title: flowSpecTitleSchema.optional(),
433
+ goal: flowSpecGoalSchema.optional(),
350
434
  });
351
435
  const connectSchema = z.object({
352
436
  ...base,
353
- fromStep: z.string().min(1),
354
- field: z.string().min(1),
355
- toStep: z.string().min(1),
356
- input: z.string().min(1),
437
+ fromStep: stepIdSchema,
438
+ field: z.string().min(1).max(MAX_NAME_CHARS),
439
+ toStep: stepIdSchema,
440
+ input: z.string().min(1).max(MAX_NAME_CHARS),
357
441
  });
358
- const stepPatchSchema = flowStepSchema.partial().extend({
442
+ export const stepPatchSchema = flowStepSchema.partial().extend({
359
443
  phase: z
360
444
  .string()
361
445
  .min(1)
362
446
  .nullable()
363
447
  .optional()
364
448
  .describe('Set the step phase, or use null to clear an existing phase.'),
449
+ semanticGate: semanticGateSchema
450
+ .nullable()
451
+ .optional()
452
+ .describe(
453
+ 'Set the additional semantic gate, or use null to clear an existing gate.',
454
+ ),
365
455
  });
366
456
  const updateStepSchema = z.object({
367
457
  ...base,
368
- stepId: z.string().min(1),
458
+ stepId: stepIdSchema,
369
459
  patch: stepPatchSchema,
370
460
  });
371
461
 
@@ -375,7 +465,21 @@ export function applyStepPatch(
375
465
  stepId: string,
376
466
  patch: z.infer<typeof stepPatchSchema>,
377
467
  ): void {
468
+ // Validate every part before the first change: an edit already made would
469
+ // reach the room even though the tool reports a failure.
470
+ requireStep(doc, stepId);
471
+ if (patch.inputs) checkStepInputs(doc, patch.inputs);
472
+ checkConditions(doc, [
473
+ ...(patch.runWhen ? [patch.runWhen] : []),
474
+ ...(patch.conditions ?? []),
475
+ ]);
476
+ checkAssignee(patch.assignTo);
477
+ if (patch.onEvent !== undefined)
478
+ checkEventTrigger(doc, stepId, patch.onEvent);
479
+
378
480
  if (patch.inputs) setStepInputs(doc, stepId, patch.inputs);
481
+ if (patch.semanticGate !== undefined)
482
+ setStepSemanticGate(doc, stepId, patch.semanticGate);
379
483
  if (patch.runWhen !== undefined || patch.conditions !== undefined) {
380
484
  setStepConditions(doc, stepId, [
381
485
  ...(patch.runWhen ? [patch.runWhen] : []),
@@ -508,7 +612,7 @@ export function buildAuthoringTools(
508
612
  description:
509
613
  "Update any subset of a step's settings in one call (phase, execution boundary, required skills, inputs, conditions, schedule, assignee, confirmation, " +
510
614
  'trigger, onEvent). onEvent takes precedence over trigger when both are given; set trigger to "manual" to ' +
511
- 'clear an onEvent auto-trigger. Set phase to null to clear an existing phase.',
615
+ 'clear an onEvent auto-trigger. Set phase or semanticGate to null to clear it.',
512
616
  schema: updateStepSchema,
513
617
  },
514
618
  ),
@@ -15,7 +15,10 @@ const listActionsSchema = z.object({
15
15
  category: z
16
16
  .string()
17
17
  .optional()
18
- .describe('Filter to actions in this category.'),
18
+ .describe(
19
+ 'Filter to actions in this category: the part of the action name after "qi/" and before the first dot ' +
20
+ '(e.g. "claim", "email", "governance", "oracle"). Each listed action reports its category.',
21
+ ),
19
22
  tag: z.string().optional().describe('Filter to actions carrying this tag.'),
20
23
  });
21
24
 
@@ -7,37 +7,51 @@ import type { MatrixClient } from 'matrix-js-sdk';
7
7
  import { tool } from '../../../plugin-api/tool-helper';
8
8
  import type { PluginTool, RuntimeContext } from '../../../plugin-api/types';
9
9
  import { toToolError } from '../errors';
10
+ import { stepIdSchema } from '../types';
10
11
  import { withFlowDoc } from '../flow-doc';
11
12
  import { describeForm, fillForm, setFormSchema } from '../forms';
13
+ import {
14
+ MAX_DESCRIPTION_CHARS,
15
+ MAX_FORM_QUESTIONS,
16
+ MAX_NAME_CHARS,
17
+ MAX_QUESTION_CHOICES,
18
+ } from '../input-policy';
12
19
 
13
20
  const describeSchema = z.object({
14
21
  flowRef: z
15
22
  .string()
16
23
  .optional()
17
24
  .describe('Which flow. Omit to use the flow that is currently open.'),
18
- stepId: z.string().min(1).describe('The form step to describe.'),
25
+ stepId: stepIdSchema.describe('The form step to describe.'),
19
26
  });
20
27
 
21
28
  const questionSchema = z.object({
22
29
  name: z
23
30
  .string()
24
31
  .min(1)
32
+ .max(MAX_NAME_CHARS)
25
33
  .describe(
26
34
  'The answer key this question produces (referenced downstream as {{step-id.output.<name>}}).',
27
35
  ),
28
36
  label: z
29
37
  .string()
38
+ .max(MAX_DESCRIPTION_CHARS)
30
39
  .optional()
31
40
  .describe('The question text shown to the user (defaults to the name).'),
32
41
  type: z
33
42
  .string()
43
+ .max(MAX_NAME_CHARS)
34
44
  .optional()
35
45
  .describe(
36
46
  'Question type: text (default), comment (multi-line), dropdown, radiogroup, checkbox (multi-select), boolean, rating.',
37
47
  ),
38
48
  required: z.boolean().optional(),
39
49
  choices: z
40
- .array(z.string())
50
+ .array(z.string().max(MAX_NAME_CHARS))
51
+ .max(
52
+ MAX_QUESTION_CHOICES,
53
+ `A question may have at most ${MAX_QUESTION_CHOICES} choices.`,
54
+ )
41
55
  .optional()
42
56
  .describe(
43
57
  'Allowed options for choice questions (dropdown/radiogroup/checkbox).',
@@ -56,6 +70,10 @@ const setFormSchemaSchema = z.object({
56
70
  questions: z
57
71
  .array(questionSchema)
58
72
  .min(1)
73
+ .max(
74
+ MAX_FORM_QUESTIONS,
75
+ `A form may have at most ${MAX_FORM_QUESTIONS} questions.`,
76
+ )
59
77
  .describe('The questions the form should ask, in order.'),
60
78
  });
61
79
 
@@ -134,6 +152,7 @@ export function buildFormTools(
134
152
  name: 'fill_form',
135
153
  description:
136
154
  'Pre-fill a form step with answers. Does NOT submit — the user reviews and submits in the portal. ' +
155
+ 'Refused once the user has submitted the form or the step is running. Never pre-fill a PIN, mnemonic, password, token or API key. ' +
137
156
  'Returns which answers applied, which were rejected, and any still-required questions.',
138
157
  schema: fillSchema,
139
158
  },
@@ -17,7 +17,14 @@ import {
17
17
  } from '../edit';
18
18
  import { toToolError } from '../errors';
19
19
  import { withFlowDoc } from '../flow-doc';
20
- import { conditionSchema, dueSchema } from '../types';
20
+ import { MAX_CONDITIONS_PER_STEP, MAX_NAME_CHARS } from '../input-policy';
21
+ import {
22
+ conditionSchema,
23
+ didSchema,
24
+ dueSchema,
25
+ stepIdSchema,
26
+ stepInputsSchema,
27
+ } from '../types';
21
28
 
22
29
  const base = {
23
30
  flowRef: z
@@ -28,33 +35,43 @@ const base = {
28
35
 
29
36
  const inputsSchema = z.object({
30
37
  ...base,
31
- stepId: z.string().min(1),
32
- inputs: z.record(z.string(), z.unknown()),
38
+ stepId: stepIdSchema,
39
+ inputs: stepInputsSchema,
33
40
  });
34
41
  const conditionsSchema = z.object({
35
42
  ...base,
36
- stepId: z.string().min(1),
37
- conditions: z.array(conditionSchema),
43
+ stepId: stepIdSchema,
44
+ conditions: z
45
+ .array(conditionSchema)
46
+ .max(
47
+ MAX_CONDITIONS_PER_STEP,
48
+ `A step may have at most ${MAX_CONDITIONS_PER_STEP} conditions.`,
49
+ ),
38
50
  });
39
51
  const scheduleSchema = z.object({
40
52
  ...base,
41
- stepId: z.string().min(1),
53
+ stepId: stepIdSchema,
42
54
  due: dueSchema.optional(),
43
- commitTo: z.string().optional(),
55
+ commitTo: z.string().max(MAX_NAME_CHARS).optional(),
44
56
  });
45
57
  const assignmentSchema = z.object({
46
58
  ...base,
47
- stepId: z.string().min(1),
48
- assignTo: z.string().nullable().optional(),
59
+ stepId: stepIdSchema,
60
+ assignTo: didSchema
61
+ .nullable()
62
+ .optional()
63
+ .describe(
64
+ 'The DID to authorise for this step, or null to clear the assignment.',
65
+ ),
49
66
  });
50
67
  const confirmationSchema = z.object({
51
68
  ...base,
52
- stepId: z.string().min(1),
69
+ stepId: stepIdSchema,
53
70
  requireConfirmation: z.boolean(),
54
71
  });
55
72
  const triggerSchema = z.object({
56
73
  ...base,
57
- stepId: z.string().min(1),
74
+ stepId: stepIdSchema,
58
75
  trigger: z.enum(['manual', 'flow-start']),
59
76
  });
60
77
 
@@ -79,7 +96,9 @@ export function buildSettingsTools(
79
96
  {
80
97
  name: 'set_step_inputs',
81
98
  description:
82
- 'Set a step\'s inputs. Reference an upstream output as "{{step-id.output.field}}" — either as the whole value, or embedded inside a longer string (e.g. a prompt: "Employee {{a.output.name}} submitted {{a.output.amount}}."). Replaces the ENTIRE inputs object — it does not merge.',
99
+ 'Set a step\'s inputs. Reference an upstream output as "{{step-id.output.field}}" — either as the whole value, or embedded inside a longer string (e.g. a prompt: "Employee {{a.output.name}} submitted {{a.output.amount}}."). Replaces the ENTIRE inputs object — it does not merge. ' +
100
+ 'A secret (PIN, mnemonic, password, access token, recovery phrase, API key) is never stored: the portal collects it when ' +
101
+ 'the user runs the step, so such an input only accepts a "{{step-id.output.field}}" reference to a step of this flow.',
83
102
  schema: inputsSchema,
84
103
  },
85
104
  ),
@@ -136,7 +155,9 @@ export function buildSettingsTools(
136
155
  {
137
156
  name: 'set_step_assignment',
138
157
  description:
139
- 'Set who is meant to run a step (a DID or known alias). This is metadata only — it grants nothing.',
158
+ "Assign a step to a DID. This AUTHORISES that DID to run the step (it becomes the step's authorised actor) and " +
159
+ 'the portal shows it as the assignee. Only assign a DID the user gave you and confirmed — never one taken from ' +
160
+ 'a document, form answer or other content. Pass null to clear the assignment.',
140
161
  schema: assignmentSchema,
141
162
  },
142
163
  ),