@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,140 @@
1
+ /**
2
+ * Limits and content rules for everything the agent writes into a flow
3
+ * document. The schemas in `types.ts` apply them to tool input, and the edit
4
+ * layer re-checks the ones that also guard direct writes.
5
+ *
6
+ * Sizes: matrix-crdt sends each batch of document changes as one Matrix
7
+ * event, base64-encoded, and a homeserver refuses any event over 64 KiB of
8
+ * UTF-8. A step's inputs are stored three times by an input edit (the block,
9
+ * the compiled node's `props`, and the node itself), and a whole flow is
10
+ * written in one batch by `create_template`. The JSON caps below are UTF-8
11
+ * byte counts — a CJK character is three bytes, an emoji four — and keep
12
+ * either kind of write well under the event limit; `limits.test.ts` measures
13
+ * the worst case for ASCII, CJK and emoji.
14
+ *
15
+ * Secrets: a credential (PIN, mnemonic, password, access token, API key)
16
+ * must never be stored in a flow document — every room member can read it —
17
+ * and never pass through the chat. The portal collects them when the user
18
+ * runs the step, so an input named like a secret port of any action may only
19
+ * hold a `{{step.output.field}}` reference to a step of the same flow.
20
+ *
21
+ * No imports beyond the placeholder scanner and the action metadata (plain
22
+ * data): `types.ts` is loaded at Worker boot and must not reach `@ixo/editor`.
23
+ */
24
+ import { ACTION_METADATA } from './action-metadata';
25
+ import { wholeRef } from './ref-syntax';
26
+
27
+ /** Steps in one flow. */
28
+ export const MAX_FLOW_STEPS = 30;
29
+ /** UTF-8 bytes of a whole flow as JSON (one `create_template` write). */
30
+ export const MAX_FLOW_SPEC_BYTES = 16_000;
31
+ /** UTF-8 bytes of one step as JSON. */
32
+ export const MAX_STEP_SPEC_BYTES = 12_000;
33
+ /** Named inputs on one step. */
34
+ export const MAX_STEP_INPUTS = 40;
35
+ /** UTF-8 bytes of one step's inputs as JSON. */
36
+ export const MAX_STEP_INPUTS_BYTES = 8_000;
37
+ /** Characters of a step id. */
38
+ export const MAX_STEP_ID_CHARS = 64;
39
+ /** Characters of a flow or step title, a phase, a field or event name. */
40
+ export const MAX_NAME_CHARS = 200;
41
+ /** Characters of a description or goal. */
42
+ export const MAX_DESCRIPTION_CHARS = 2_000;
43
+ /** Conditions on one step. */
44
+ export const MAX_CONDITIONS_PER_STEP = 20;
45
+ /** Governed skill ids on one step. */
46
+ export const MAX_SKILLS_PER_STEP = 20;
47
+ /** Questions on one form. */
48
+ export const MAX_FORM_QUESTIONS = 50;
49
+ /** Choices on one form question. */
50
+ export const MAX_QUESTION_CHOICES = 50;
51
+ /** UTF-8 bytes of a form's pre-filled answers as JSON. */
52
+ export const MAX_FORM_ANSWERS_BYTES = 8_000;
53
+ /** Characters of a DID. */
54
+ export const MAX_DID_CHARS = 256;
55
+
56
+ /**
57
+ * Input names (lower-cased) that carry a credential: every port any action
58
+ * flags `secret`. Matched by name for every action, so a secret is refused
59
+ * even on a step whose action does not declare that port.
60
+ */
61
+ const SECRET_INPUT_NAMES: ReadonlySet<string> = new Set(
62
+ Object.values(ACTION_METADATA).flatMap((entry) =>
63
+ (entry.inputPorts ?? [])
64
+ .filter((port) => port.secret)
65
+ .map((port) => port.path.toLowerCase()),
66
+ ),
67
+ );
68
+
69
+ const utf8 = new TextEncoder();
70
+
71
+ /** UTF-8 byte length of `value` serialised as JSON (0 when it cannot be serialised). */
72
+ export function jsonByteLength(value: unknown): number {
73
+ try {
74
+ const json = JSON.stringify(value);
75
+ return json === undefined ? 0 : utf8.encode(json).length;
76
+ } catch {
77
+ return 0;
78
+ }
79
+ }
80
+
81
+ /** A step-output reference: `<step-id>.output.<field>`. */
82
+ const STEP_OUTPUT_REF = /^([^\s.{}]+)\.output\.[^\s{}]+$/;
83
+
84
+ /**
85
+ * Whether a secret input's value may be stored: empty, or only references to
86
+ * an output of a step of this flow (`stepIds`; any step id when omitted) —
87
+ * as one string, or as every leaf of a map or list of them.
88
+ */
89
+ function isAllowedSecretValue(
90
+ value: unknown,
91
+ stepIds: ReadonlySet<string> | undefined,
92
+ ): boolean {
93
+ if (value === undefined || value === null || value === '') return true;
94
+ if (typeof value === 'string') {
95
+ const ref = wholeRef(value);
96
+ const step = ref === undefined ? undefined : STEP_OUTPUT_REF.exec(ref)?.[1];
97
+ return step !== undefined && (stepIds === undefined || stepIds.has(step));
98
+ }
99
+ if (Array.isArray(value))
100
+ return value.every((item) => isAllowedSecretValue(item, stepIds));
101
+ if (typeof value === 'object')
102
+ return Object.values(value).every((item) =>
103
+ isAllowedSecretValue(item, stepIds),
104
+ );
105
+ return false;
106
+ }
107
+
108
+ /**
109
+ * Paths of every secret-named entry (at any depth) whose value is not an
110
+ * allowed reference. `stepIds` are the steps a reference may point at; when
111
+ * omitted (a step checked on its own) any step id is accepted.
112
+ */
113
+ export function secretLiteralPaths(
114
+ value: unknown,
115
+ stepIds?: ReadonlySet<string>,
116
+ path = '',
117
+ ): string[] {
118
+ if (Array.isArray(value))
119
+ return value.flatMap((item, index) =>
120
+ secretLiteralPaths(item, stepIds, `${path}[${index}]`),
121
+ );
122
+ if (!value || typeof value !== 'object') return [];
123
+ const out: string[] = [];
124
+ for (const [key, item] of Object.entries(value)) {
125
+ const at = path ? `${path}.${key}` : key;
126
+ if (SECRET_INPUT_NAMES.has(key.toLowerCase())) {
127
+ if (!isAllowedSecretValue(item, stepIds)) out.push(at);
128
+ } else out.push(...secretLiteralPaths(item, stepIds, at));
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /** The message every secret-literal refusal uses. */
134
+ export function secretLiteralMessage(paths: string[]): string {
135
+ return (
136
+ `Refusing to store a secret (PIN, mnemonic, password, token or API key) in the flow (${paths.join(', ')}). ` +
137
+ 'These are collected by the portal when the user runs the step; leave the input empty ' +
138
+ 'or reference an output of a step in this flow as "{{step-id.output.field}}". Never ask the user for one in chat.'
139
+ );
140
+ }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Flow Builder operating guide — the detailed "how to act" prompt the runtime
3
- * injects into the main agent's system prompt while the flows capability is
4
- * loaded (see `main-agent.ts`, gated on `loadedPlugins` containing
5
- * {@link FLOWS_PLUGIN_NAME}). It is the durable counterpart to the manifest
6
- * that `load_capability` returns: the manifest sells the capability at
2
+ * Flow Builder operating guide — the detailed "how to act" prompt. The plugin
3
+ * declares it as its `operatingGuide`: `load_capability` returns it in the
4
+ * turn that loads the flows capability, and on later turns the runtime
5
+ * renders it under `## Capabilities in use` at the end of the system prompt
6
+ * while the capability stays loaded (see `main-agent.ts`). It is the durable
7
+ * counterpart to the manifest: the manifest sells the capability at
7
8
  * discovery time; this guide governs behaviour for the whole flow-building
8
9
  * session.
9
10
  *
@@ -18,8 +19,7 @@ export const FLOWS_PLUGIN_NAME = 'flows';
18
19
 
19
20
  /**
20
21
  * The operating guide. Starts with its own `### Flow Builder mode` sub-header
21
- * so it nests cleanly under the runtime's `## Custom Instructions` section
22
- * wherever the composer slots it in.
22
+ * so it nests cleanly under the runtime's `## Capabilities in use` section.
23
23
  */
24
24
  export const FLOWS_OPERATING_GUIDE = `### Flow Builder mode
25
25
 
@@ -31,7 +31,7 @@ You have loaded the **Flow Builder** capability. Use it whenever the user wants
31
31
 
32
32
  1. **Discover (know the blocks before you plan).** Work out which action blocks the automation needs and look up *exactly* what each one requires before you promise anything:
33
33
  - \`list_actions\` to find candidate blocks; \`describe_action\` on each to get its real inputs, outputs, and whether it is a form.
34
- - \`requirements\` on each block to get the prerequisites the **user** must supply — e.g. a claim collection, a connected account, a template id, a recipient, an endpoint, a PIN, a funding amount. These are things you must ASK for; never invent them.
34
+ - \`requirements\` on each block to get the prerequisites the **user** must supply — e.g. a claim collection, a connected account, a template id, a recipient, an endpoint, a funding amount. These are things you must ASK for; never invent them. A \`pin\` requirement is the exception: the portal collects PINs when the user runs the step (see **Secrets** below).
35
35
  Do this as a quiet tool pass — not a wall of text to the user.
36
36
 
37
37
  2. **Plan (planning mode — this is the first thing the user sees).** Before writing anything, present a short, plain-language plan and STOP for approval. For each step, in one line: **"I'll use the \`<action>\` block — it <does X>"**; then how the steps connect (which output feeds which input), plus any form, schedule, assignee, or condition. End the plan with a clear bulleted list of **what you need from the user** — every \`requires\` you could not fill yourself. Keep it tight: a numbered list, not an essay.
@@ -39,11 +39,12 @@ You have loaded the **Flow Builder** capability. Use it whenever the user wants
39
39
  3. **Confirm.** Ask the user to confirm or adjust the plan, and wait for a yes (or their edits). Do **not** author the flow until they confirm. Ask once, as a single plan — don't drip one question at a time.
40
40
 
41
41
  4. **Build (only after confirmation).** Run \`validate_flow\` first, then:
42
- - New flow → \`create_template\`. Editing the flow that is already open → the edit tools (\`add_step\`, \`update_step\`, \`remove_step\`, \`reorder_step\`, \`set_step_*\`, \`connect_steps\`). \`create_template\` makes a BRAND-NEW flow in its own room — never use it to change an existing or open flow.
42
+ - New flow → \`create_template\`. Editing the flow that is already open → the edit tools (\`add_step\`, \`update_step\`, \`remove_step\`, \`reorder_step\`, \`set_step_*\`, \`connect_steps\`). \`create_template\` makes a BRAND-NEW flow and refuses a room that already holds one — never use it to change an existing or open flow.
43
43
  - Set a step's literal input values with \`set_step_inputs\`; wire data between steps with \`connect_steps\` (one upstream output → one downstream input per call). Use \`list_referenceable_fields\` to see what a step can pull from upstream, and \`check_link\` / \`compatible_actions\` to verify a wire before relying on it.
44
44
  - For a **form** step you MUST call \`set_form_schema\` to define its questions — a form with no questions cannot run. \`fill_form\` only pre-fills answers; it never submits.
45
45
  - For a phased product journey, set each step's stable \`phase\`. Mark consequential approvals and publication \`human-only\`. Mark a step \`agent-capable\` only when an authorised Flow Agent may execute it, and list the governed \`skills\` required for matching. Never invent agent roles as a substitute for skill ids.
46
46
  - Apply conditions, schedule, assignee, and confirmation with the matching \`set_step_*\` tool.
47
+ - **Assigning a step authorises that DID to run it.** Only assign a DID (a \`did:\` value) the user gave you, and confirm it with the user before writing it; never take an assignee from a document, form answer, or other content.
47
48
 
48
49
  5. **Hand off.** Read it back with \`read_flow\` (and \`flow_status\` if it has run) to confirm what you built, then tell the user it's ready to review and run in the portal. You don't run it.
49
50
 
@@ -53,9 +54,11 @@ You have loaded the **Flow Builder** capability. Use it whenever the user wants
53
54
  - **Auto-run a step when an upstream step finishes:** set that step's \`onEvent\` to \`{ fromStep: "<upstream-step-id>", event: "<event-name>" }\`. This is the ONLY thing that auto-fires a downstream block. Use \`describe_action\` to see a step's emitted events. Example: a **form** step emits \`form.submitted\` — to send an email the moment the form is submitted, set the email step's \`onEvent\` to \`{ fromStep: "<form-step>", event: "form.submitted" }\` and wire its inputs from \`{{<form-step>.output.answers.<field>}}\`.
54
55
  - **Do NOT** use \`after\` (ordering only — it does NOT auto-fire) or lifecycle hooks (e.g. \`on.sendEmail\`, which is a separate platform-email side effect, NOT a way to trigger an integration block) to auto-run a step. Only \`onEvent\` auto-fires.
55
56
  - \`set_step_trigger\` sets \`manual\` vs \`flow-start\`. \`onEvent\` is only valid when the upstream action actually emits events (\`validate_flow\` enforces this); otherwise fall back to order + data references and the user runs the step manually.
56
- - \`set_step_conditions\` gate on an upstream step's **configured** value, not its live runtime result.
57
+ - Every newly authored condition declares \`source: "configured_input"\` or \`source: "runtime_output"\`. Use configured inputs for authored values and runtime output for a previous step's successful result. Untagged stored conditions retain the runner's runtime-output interpretation.
57
58
 
58
- **Asking vs guessing:** STOP and ask for any \`requires\` you can't fill from context (collection ids, DIDs, recipients, connected accounts, template names, endpoints, PINs, amounts). Never fabricate them. But don't ask for things the portal collects at run time, or that you can reasonably infer.
59
+ **Asking vs guessing:** STOP and ask for any \`requires\` you can't fill from context (collection ids, DIDs, recipients, connected accounts, template names, endpoints, amounts). Never fabricate them. But don't ask for things the portal collects at run time, or that you can reasonably infer.
60
+
61
+ **Secrets:** never ask for a PIN, mnemonic, password, access token, recovery phrase or API key in chat, and never write one into a flow — every room member can read the flow. PINs are collected by the portal when the user runs the step. A secret input (\`pin\`, \`mnemonic\`, \`matrixPassword\`, \`matrixAccessToken\`, \`matrixRecoveryPhrase\`, a plaintext API key) may only hold a \`{{step-id.output.field}}\` reference to a step of this flow (e.g. a wallet step's mnemonic output); the tools refuse anything else. If the user offers a PIN in chat, tell them not to share it and that the portal will ask for it.
59
62
 
60
63
  **Status is read-only.** \`flow_status\` / \`get_step\` / \`explain_step\` report the user's real runs — use them to report progress or to diagnose a failed step and fix the template. Never invent a step's state, a transaction result, or a proof.
61
64
 
@@ -27,12 +27,24 @@ import {
27
27
  nbToFriendlyInputs,
28
28
  parseConditionsProp,
29
29
  } from './translator';
30
+ import { scanRefTokens } from './ref-syntax';
30
31
  import type {
31
32
  FlowSpecRead,
32
33
  FlowStepRead,
33
34
  StepState,
34
35
  StepStatus,
35
36
  } from './types';
37
+ import { semanticGateSchema } from './types';
38
+
39
+ function parseSemanticGate(value: string) {
40
+ try {
41
+ const parsed: unknown = JSON.parse(value);
42
+ const gate = semanticGateSchema.safeParse(parsed);
43
+ return gate.success ? gate.data : undefined;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
36
48
 
37
49
  const STEP_STATES: readonly StepState[] = [
38
50
  'idle',
@@ -186,19 +198,41 @@ function assigneeFrom(value: unknown): string | undefined {
186
198
  return undefined;
187
199
  }
188
200
 
189
- /** Collect the upstream step ids this step refers to via "{{step.output.*}}" inputs. */
201
+ /** Every `{{ … }}` reference anywhere in `value` (nested maps, arrays, embedded text). */
202
+ function collectRefs(value: unknown, out: string[]): void {
203
+ if (typeof value === 'string') {
204
+ if (value.includes('{{'))
205
+ for (const token of scanRefTokens(value)) out.push(token.ref);
206
+ return;
207
+ }
208
+ if (Array.isArray(value)) {
209
+ for (const item of value) collectRefs(item, out);
210
+ return;
211
+ }
212
+ if (value && typeof value === 'object')
213
+ for (const item of Object.values(value)) collectRefs(item, out);
214
+ }
215
+
216
+ /**
217
+ * The other steps of this flow that `inputs` reads from via "{{step.output.*}}",
218
+ * at any depth and embedded in longer strings. A reference whose id is not a
219
+ * step of this flow (`trigger.payload.*`, a removed step) is not a dependency.
220
+ */
190
221
  function referencedSteps(
191
222
  inputs: Record<string, unknown> | undefined,
223
+ stepId: string,
224
+ stepIds: ReadonlySet<string>,
192
225
  ): string[] {
193
226
  if (!inputs) return [];
194
- const refs = new Set<string>();
195
- for (const value of Object.values(inputs)) {
196
- if (typeof value === 'string') {
197
- const match = /^\{\{\s*([^.}\s]+)\./.exec(value);
198
- if (match?.[1]) refs.add(match[1]);
199
- }
227
+ const refs: string[] = [];
228
+ collectRefs(inputs, refs);
229
+ const ids = new Set<string>();
230
+ for (const ref of refs) {
231
+ const dot = ref.indexOf('.');
232
+ const id = dot > 0 ? ref.slice(0, dot) : '';
233
+ if (id && id !== stepId && stepIds.has(id)) ids.add(id);
200
234
  }
201
- return [...refs];
235
+ return [...ids];
202
236
  }
203
237
 
204
238
  /** Derive a step's read-only status from its runtime entry + its dependencies' states. */
@@ -275,6 +309,9 @@ export function readFlowSpec(doc: YDoc, ref: string): FlowSpecRead | null {
275
309
  >();
276
310
  const stateByStepId = new Map<string, StepState>();
277
311
 
312
+ const stepIds = new Set(
313
+ compiled.order.filter((nodeId) => compiled.nodes[nodeId] !== undefined),
314
+ );
278
315
  for (const nodeId of compiled.order) {
279
316
  const node = compiled.nodes[nodeId];
280
317
  if (!node) continue;
@@ -305,6 +342,9 @@ export function readFlowSpec(doc: YDoc, ref: string): FlowSpecRead | null {
305
342
  if (skills.length > 0) step.skills = skills;
306
343
  else if (primarySkill) step.skills = [primarySkill];
307
344
  if (inputs) step.inputs = inputs;
345
+ if (typeof props.semanticGate === 'string' && props.semanticGate) {
346
+ step.semanticGate = parseSemanticGate(props.semanticGate);
347
+ }
308
348
  if (conditions.length === 1 && conditions[0]) step.runWhen = conditions[0];
309
349
  else if (conditions.length > 1) step.conditions = conditions;
310
350
 
@@ -349,7 +389,7 @@ export function readFlowSpec(doc: YDoc, ref: string): FlowSpecRead | null {
349
389
  runtimeByStep.set(nodeId, runtime);
350
390
  stateByStepId.set(nodeId, asStepState(runtime?.state) ?? 'idle');
351
391
  dependenciesByStep.set(nodeId, {
352
- referenced: referencedSteps(inputs),
392
+ referenced: referencedSteps(inputs, nodeId, stepIds),
353
393
  after: [],
354
394
  });
355
395
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The `{{ … }}` placeholder syntax, scanned by hand in one forward pass.
3
+ *
4
+ * Placeholders are read out of document content on every flow read, and any
5
+ * room member can write that content, so the scan must stay linear in the
6
+ * input length whatever the input looks like. A regex with a lazy group next
7
+ * to optional whitespace backtracks polynomially on "{{" followed by a long
8
+ * run of spaces and no closing braces; this scanner looks at each character a
9
+ * bounded number of times instead.
10
+ *
11
+ * A placeholder is "{{", then at least one character that is not a brace,
12
+ * then "}}". Whitespace just inside the braces is kept apart from the
13
+ * reference itself so callers can rewrite the reference and keep the
14
+ * author's spacing.
15
+ *
16
+ * This module has no imports on purpose: the boot-loaded schema module
17
+ * (`types.ts`) uses it, and nothing it pulls in may reach `@ixo/editor`.
18
+ */
19
+
20
+ /**
21
+ * Strings longer than this are passed through untouched instead of scanned.
22
+ * One Matrix event carries at most 64 KiB, so no single authored value in a
23
+ * flow document can legitimately be longer.
24
+ */
25
+ export const MAX_REF_SCAN_CHARS = 65_536;
26
+
27
+ export interface RefToken {
28
+ /** Index of the opening "{{". */
29
+ start: number;
30
+ /** Index just past the closing "}}". */
31
+ end: number;
32
+ /** The reference with the surrounding whitespace removed (may be empty). */
33
+ ref: string;
34
+ /** Whitespace between "{{" and the reference. */
35
+ leading: string;
36
+ /** Whitespace between the reference and "}}". */
37
+ trailing: string;
38
+ }
39
+
40
+ function isWhitespace(char: string): boolean {
41
+ return /\s/.test(char);
42
+ }
43
+
44
+ /** Every `{{ … }}` placeholder in `text`, in order. */
45
+ export function scanRefTokens(text: string): RefToken[] {
46
+ const tokens: RefToken[] = [];
47
+ if (text.length > MAX_REF_SCAN_CHARS) return tokens;
48
+
49
+ let from = 0;
50
+ while (from < text.length) {
51
+ const start = text.indexOf('{{', from);
52
+ if (start < 0) break;
53
+
54
+ // The first brace after the opener decides the outcome: "}}" closes the
55
+ // placeholder; anything else means no placeholder opens at `start`.
56
+ let cursor = start + 2;
57
+ while (cursor < text.length && text[cursor] !== '{' && text[cursor] !== '}')
58
+ cursor += 1;
59
+ const closes =
60
+ cursor > start + 2 && text[cursor] === '}' && text[cursor + 1] === '}';
61
+ if (!closes) {
62
+ // The brace-free run after the opener cannot hold another opener, so
63
+ // resume at the brace — except for "{{{", where the next opener starts
64
+ // one character later.
65
+ from = cursor === start + 2 ? start + 1 : cursor;
66
+ continue;
67
+ }
68
+
69
+ const inner = text.slice(start + 2, cursor);
70
+ let head = 0;
71
+ while (head < inner.length && isWhitespace(inner.charAt(head))) head += 1;
72
+ let tail = inner.length;
73
+ while (tail > head && isWhitespace(inner.charAt(tail - 1))) tail -= 1;
74
+ tokens.push({
75
+ start,
76
+ end: cursor + 2,
77
+ ref: inner.slice(head, tail),
78
+ leading: inner.slice(0, head),
79
+ trailing: inner.slice(tail),
80
+ });
81
+ from = cursor + 2;
82
+ }
83
+ return tokens;
84
+ }
85
+
86
+ /**
87
+ * The reference when the whole string is exactly one placeholder
88
+ * ("{{ a.output.b }}"), else `undefined`. "{{a}} and {{b}}" is two embedded
89
+ * placeholders, not one reference.
90
+ */
91
+ export function wholeRef(text: string): string | undefined {
92
+ if (!text.startsWith('{{') || !text.endsWith('}}')) return undefined;
93
+ const tokens = scanRefTokens(text);
94
+ const only = tokens.length === 1 ? tokens[0] : undefined;
95
+ if (!only || only.start !== 0 || only.end !== text.length) return undefined;
96
+ return only.ref.length > 0 ? only.ref : undefined;
97
+ }
98
+
99
+ /** Rewrite the reference inside every placeholder, keeping its spacing. */
100
+ export function rewriteRefTokens(
101
+ text: string,
102
+ mapRef: (ref: string) => string,
103
+ ): string {
104
+ const tokens = scanRefTokens(text);
105
+ if (tokens.length === 0) return text;
106
+ let out = '';
107
+ let last = 0;
108
+ for (const token of tokens) {
109
+ out += text.slice(last, token.start);
110
+ out += `{{${token.leading}${token.ref.length > 0 ? mapRef(token.ref) : ''}${token.trailing}}}`;
111
+ last = token.end;
112
+ }
113
+ return out + text.slice(last);
114
+ }
115
+
116
+ /**
117
+ * True when any string anywhere inside `value` (nested objects and arrays
118
+ * included) holds a placeholder whose reference starts with `<id>.`.
119
+ */
120
+ export function referencesId(value: unknown, id: string): boolean {
121
+ if (typeof value === 'string') {
122
+ if (!value.includes('{{')) return false;
123
+ return scanRefTokens(value).some((token) => token.ref.startsWith(`${id}.`));
124
+ }
125
+ if (Array.isArray(value)) return value.some((item) => referencesId(item, id));
126
+ if (value && typeof value === 'object')
127
+ return Object.values(value).some((item) => referencesId(item, id));
128
+ return false;
129
+ }