@ixo/oracle-runtime-workers 0.13.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.
- package/README.md +32 -10
- package/package.json +13 -8
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +170 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +504 -108
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
|
@@ -17,7 +17,14 @@ import * as Y from 'yjs';
|
|
|
17
17
|
import { deleteBlock, editBlock, findParentOf } from './ydoc-helpers';
|
|
18
18
|
import { getActionDef, isEventCapable } from './actions';
|
|
19
19
|
import { FlowError } from './errors';
|
|
20
|
+
import {
|
|
21
|
+
MAX_STEP_INPUTS_BYTES,
|
|
22
|
+
jsonByteLength,
|
|
23
|
+
secretLiteralMessage,
|
|
24
|
+
secretLiteralPaths,
|
|
25
|
+
} from './input-policy';
|
|
20
26
|
import { readFlowSpec } from './read';
|
|
27
|
+
import { referencesId } from './ref-syntax';
|
|
21
28
|
import {
|
|
22
29
|
BLOCK_ID_PREFIX,
|
|
23
30
|
blockIdToStepId,
|
|
@@ -26,17 +33,116 @@ import {
|
|
|
26
33
|
friendlyInputsToNb,
|
|
27
34
|
stepIdToBlockId,
|
|
28
35
|
} from './translator';
|
|
29
|
-
import type
|
|
36
|
+
import { didSchema, type Condition, type FlowStep } from './types';
|
|
30
37
|
|
|
31
38
|
const DOC = 'document';
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
/** The step's block id; throws `step_not_found` when the flow has no such step. */
|
|
41
|
+
export function requireStep(doc: YDoc, stepId: string): string {
|
|
34
42
|
if (!doc.getMap('qi.flow.nodes').has(stepId)) {
|
|
35
43
|
throw new FlowError('step_not_found', `No step "${stepId}" in this flow.`);
|
|
36
44
|
}
|
|
37
45
|
return stepIdToBlockId(stepId);
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
/** The ids of the flow's steps, plus `extra` (steps about to be added). */
|
|
49
|
+
function stepIdsOf(doc: YDoc, extra: readonly string[]): Set<string> {
|
|
50
|
+
return new Set([...doc.getMap('qi.flow.nodes').keys(), ...extra]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Checks. Each throws the error its setter would, without writing, so a tool
|
|
55
|
+
* that makes several edits can validate all of them before the first change
|
|
56
|
+
* (a change already made would still reach the room).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Throw unless `inputs` may be stored: no secret outside a reference to a
|
|
61
|
+
* step of this flow (or of `extraStepIds`), and within the size cap. Returns
|
|
62
|
+
* the serialised value to store.
|
|
63
|
+
*/
|
|
64
|
+
export function checkStepInputs(
|
|
65
|
+
doc: YDoc,
|
|
66
|
+
inputs: Record<string, unknown>,
|
|
67
|
+
extraStepIds: readonly string[] = [],
|
|
68
|
+
): string {
|
|
69
|
+
const secrets = secretLiteralPaths(inputs, stepIdsOf(doc, extraStepIds));
|
|
70
|
+
if (secrets.length > 0)
|
|
71
|
+
throw new FlowError('validation_failed', secretLiteralMessage(secrets));
|
|
72
|
+
const nb = friendlyInputsToNb(inputs) ?? {};
|
|
73
|
+
const bytes = jsonByteLength(nb);
|
|
74
|
+
if (bytes > MAX_STEP_INPUTS_BYTES)
|
|
75
|
+
throw new FlowError(
|
|
76
|
+
'validation_failed',
|
|
77
|
+
`A step's inputs may be at most ${MAX_STEP_INPUTS_BYTES} bytes of JSON (got ${bytes}).`,
|
|
78
|
+
);
|
|
79
|
+
return JSON.stringify(nb);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Throw unless every condition reads a step of this flow (or of `extraStepIds`). */
|
|
83
|
+
export function checkConditions(
|
|
84
|
+
doc: YDoc,
|
|
85
|
+
conditions: readonly Condition[],
|
|
86
|
+
extraStepIds: readonly string[] = [],
|
|
87
|
+
): void {
|
|
88
|
+
// A condition on a step that is not in the flow can never be evaluated.
|
|
89
|
+
const ids = stepIdsOf(doc, extraStepIds);
|
|
90
|
+
for (const condition of conditions)
|
|
91
|
+
if (!ids.has(condition.fromStep))
|
|
92
|
+
throw new FlowError(
|
|
93
|
+
'step_not_found',
|
|
94
|
+
`No step "${condition.fromStep}" in this flow.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Throw unless `assignTo` is a DID (assigning grants run authority). */
|
|
99
|
+
export function checkAssignee(assignTo: string | undefined): void {
|
|
100
|
+
if (assignTo !== undefined && !didSchema.safeParse(assignTo).success)
|
|
101
|
+
throw new FlowError(
|
|
102
|
+
'validation_failed',
|
|
103
|
+
`"${assignTo}" is not a DID. Assigning a step authorises that DID to run it, so it must be a did: value.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Throw unless `stepId` may auto-trigger on `onEvent.fromStep`: a step of
|
|
109
|
+
* this flow whose action can emit events. Returns the source's block id.
|
|
110
|
+
*/
|
|
111
|
+
export function checkEventTrigger(
|
|
112
|
+
doc: YDoc,
|
|
113
|
+
stepId: string,
|
|
114
|
+
onEvent: { fromStep: string; event: string },
|
|
115
|
+
): string {
|
|
116
|
+
if (onEvent.fromStep.startsWith(BLOCK_ID_PREFIX)) {
|
|
117
|
+
throw new FlowError(
|
|
118
|
+
'validation_failed',
|
|
119
|
+
`"${onEvent.fromStep}" is an internal block id — use the short step id ("${blockIdToStepId(onEvent.fromStep)}").`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const sourceBlockId = requireStep(doc, onEvent.fromStep);
|
|
123
|
+
|
|
124
|
+
// Same event-capability rule validate_flow enforces at plan level.
|
|
125
|
+
const source = doc.getMap('qi.flow.nodes').get(onEvent.fromStep);
|
|
126
|
+
const registryType =
|
|
127
|
+
source instanceof Y.Map ? source.get('registryType') : undefined;
|
|
128
|
+
const can = source instanceof Y.Map ? source.get('can') : undefined;
|
|
129
|
+
const action =
|
|
130
|
+
typeof registryType === 'string'
|
|
131
|
+
? registryType
|
|
132
|
+
: typeof can === 'string'
|
|
133
|
+
? canToAction(can)
|
|
134
|
+
: undefined;
|
|
135
|
+
const def = action ? getActionDef(action) : undefined;
|
|
136
|
+
if (def && !isEventCapable(def)) {
|
|
137
|
+
throw new FlowError(
|
|
138
|
+
'validation_failed',
|
|
139
|
+
`Step "${stepId}" can't auto-trigger on "${onEvent.fromStep}" — its action "${action}" cannot emit events. ` +
|
|
140
|
+
'Use ordering (after) + an input reference instead.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return sourceBlockId;
|
|
144
|
+
}
|
|
145
|
+
|
|
40
146
|
/** Write a set of value props onto a step's block (per-block, lossless). */
|
|
41
147
|
export function setStepProps(
|
|
42
148
|
doc: YDoc,
|
|
@@ -47,14 +153,18 @@ export function setStepProps(
|
|
|
47
153
|
editBlock(doc, { blockId, attributes: { props }, docName: DOC });
|
|
48
154
|
}
|
|
49
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Replace a step's inputs. Every path that writes inputs ends here, so the
|
|
158
|
+
* secret and size rules are enforced here as well as in the tool schemas.
|
|
159
|
+
*/
|
|
50
160
|
export function setStepInputs(
|
|
51
161
|
doc: YDoc,
|
|
52
162
|
stepId: string,
|
|
53
163
|
inputs: Record<string, unknown>,
|
|
54
164
|
): void {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
165
|
+
requireStep(doc, stepId);
|
|
166
|
+
const serialized = checkStepInputs(doc, inputs);
|
|
167
|
+
setStepExecutionProps(doc, stepId, { inputs: serialized });
|
|
58
168
|
}
|
|
59
169
|
|
|
60
170
|
export function setStepConditions(
|
|
@@ -62,11 +172,54 @@ export function setStepConditions(
|
|
|
62
172
|
stepId: string,
|
|
63
173
|
conditions: Condition[],
|
|
64
174
|
): void {
|
|
65
|
-
|
|
175
|
+
requireStep(doc, stepId);
|
|
176
|
+
checkConditions(doc, conditions);
|
|
177
|
+
setStepExecutionProps(doc, stepId, {
|
|
66
178
|
conditions: conditions.length > 0 ? buildConditionsProp(conditions) : '',
|
|
67
179
|
});
|
|
68
180
|
}
|
|
69
181
|
|
|
182
|
+
export function setStepSemanticGate(
|
|
183
|
+
doc: YDoc,
|
|
184
|
+
stepId: string,
|
|
185
|
+
gate: FlowStep['semanticGate'] | null,
|
|
186
|
+
): void {
|
|
187
|
+
setStepExecutionProps(doc, stepId, {
|
|
188
|
+
semanticGate: gate ? JSON.stringify(gate) : '',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function setStepExecutionProps(
|
|
193
|
+
doc: YDoc,
|
|
194
|
+
stepId: string,
|
|
195
|
+
props: Record<string, string>,
|
|
196
|
+
): void {
|
|
197
|
+
requireStep(doc, stepId);
|
|
198
|
+
const nodes = doc.getMap('qi.flow.nodes');
|
|
199
|
+
const node = nodes.get(stepId);
|
|
200
|
+
if (!(node instanceof Y.Map))
|
|
201
|
+
throw new FlowError(
|
|
202
|
+
'step_not_found',
|
|
203
|
+
`No compiled step "${stepId}" in this flow.`,
|
|
204
|
+
);
|
|
205
|
+
doc.transact(() => {
|
|
206
|
+
setStepProps(doc, stepId, props);
|
|
207
|
+
const existing: unknown = node.get('props');
|
|
208
|
+
if (existing instanceof Y.Map) {
|
|
209
|
+
for (const [key, value] of Object.entries(props))
|
|
210
|
+
existing.set(key, value);
|
|
211
|
+
} else {
|
|
212
|
+
node.set('props', {
|
|
213
|
+
...(existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
214
|
+
? existing
|
|
215
|
+
: {}),
|
|
216
|
+
...props,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const [key, value] of Object.entries(props)) node.set(key, value);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
70
223
|
export function setStepSchedule(
|
|
71
224
|
doc: YDoc,
|
|
72
225
|
stepId: string,
|
|
@@ -88,6 +241,8 @@ export function setStepAssignment(
|
|
|
88
241
|
// The portal shows + nudges the assignee from `props.assignment`; the
|
|
89
242
|
// `authorisedActors` whitelist also lets that actor run the step. Write both
|
|
90
243
|
// so the assignee is visible AND authorized; clear both when unassigned.
|
|
244
|
+
// Because this grants run authority, only a well-formed DID is accepted.
|
|
245
|
+
checkAssignee(assignTo);
|
|
91
246
|
setStepProps(doc, stepId, {
|
|
92
247
|
assignment: assignTo
|
|
93
248
|
? JSON.stringify({ assignedActor: { did: assignTo } })
|
|
@@ -176,34 +331,7 @@ export function setStepEventTrigger(
|
|
|
176
331
|
stepId: string,
|
|
177
332
|
onEvent: { fromStep: string; event: string },
|
|
178
333
|
): void {
|
|
179
|
-
|
|
180
|
-
throw new FlowError(
|
|
181
|
-
'validation_failed',
|
|
182
|
-
`"${onEvent.fromStep}" is an internal block id — use the short step id ("${blockIdToStepId(onEvent.fromStep)}").`,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
const sourceBlockId = requireStep(doc, onEvent.fromStep);
|
|
186
|
-
|
|
187
|
-
// Same event-capability rule validate_flow enforces at plan level (spec §2.3).
|
|
188
|
-
const source = doc.getMap('qi.flow.nodes').get(onEvent.fromStep);
|
|
189
|
-
const registryType =
|
|
190
|
-
source instanceof Y.Map ? source.get('registryType') : undefined;
|
|
191
|
-
const can = source instanceof Y.Map ? source.get('can') : undefined;
|
|
192
|
-
const action =
|
|
193
|
-
typeof registryType === 'string'
|
|
194
|
-
? registryType
|
|
195
|
-
: typeof can === 'string'
|
|
196
|
-
? canToAction(can)
|
|
197
|
-
: undefined;
|
|
198
|
-
const def = action ? getActionDef(action) : undefined;
|
|
199
|
-
if (def && !isEventCapable(def)) {
|
|
200
|
-
throw new FlowError(
|
|
201
|
-
'validation_failed',
|
|
202
|
-
`Step "${stepId}" can't auto-trigger on "${onEvent.fromStep}" — its action "${action}" cannot emit events. ` +
|
|
203
|
-
'Use ordering (after) + an input reference instead.',
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
334
|
+
const sourceBlockId = checkEventTrigger(doc, stepId, onEvent);
|
|
207
335
|
setStepProps(doc, stepId, {
|
|
208
336
|
trigger: JSON.stringify({
|
|
209
337
|
type: 'block.event',
|
|
@@ -221,10 +349,9 @@ function referrersOf(doc: YDoc, ref: string, stepId: string): string[] {
|
|
|
221
349
|
const referrers: string[] = [];
|
|
222
350
|
for (const step of flow.steps) {
|
|
223
351
|
if (step.id === stepId) continue;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
352
|
+
// Inputs nest (an email's `variables` map, arrays of recipients), and a
|
|
353
|
+
// reference may sit inside a longer template string.
|
|
354
|
+
const refsInput = referencesId(step.inputs ?? {}, stepId);
|
|
228
355
|
const conds = [
|
|
229
356
|
...(step.runWhen ? [step.runWhen] : []),
|
|
230
357
|
...(step.conditions ?? []),
|
|
@@ -259,8 +386,14 @@ export function removeStep(doc: YDoc, ref: string, stepId: string): void {
|
|
|
259
386
|
const edges = doc.getMap('qi.flow.edges');
|
|
260
387
|
const toDelete: string[] = [];
|
|
261
388
|
edges.forEach((value, edgeId) => {
|
|
262
|
-
|
|
263
|
-
|
|
389
|
+
// The compiler stores each edge as a Y.Map; plain objects are tolerated.
|
|
390
|
+
const endpoint = (key: 'source' | 'target'): unknown =>
|
|
391
|
+
value instanceof Y.Map
|
|
392
|
+
? value.get(key)
|
|
393
|
+
: value && typeof value === 'object'
|
|
394
|
+
? Object.getOwnPropertyDescriptor(value, key)?.value
|
|
395
|
+
: undefined;
|
|
396
|
+
if (endpoint('source') === stepId || endpoint('target') === stepId)
|
|
264
397
|
toDelete.push(edgeId);
|
|
265
398
|
});
|
|
266
399
|
for (const edgeId of toDelete) edges.delete(edgeId);
|
|
@@ -269,9 +402,57 @@ export function removeStep(doc: YDoc, ref: string, stepId: string): void {
|
|
|
269
402
|
deleteBlock(doc, { blockId, docName: DOC });
|
|
270
403
|
}
|
|
271
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Move a step's block in the document so it sits next to its neighbours in
|
|
407
|
+
* `order`: right before the next step's block, or right after the previous
|
|
408
|
+
* one's. The document also holds headings and notes between step blocks, so
|
|
409
|
+
* an index taken from `order` would land in the wrong place.
|
|
410
|
+
*
|
|
411
|
+
* Yjs has no move operation: the block is cloned and the original deleted, so
|
|
412
|
+
* an edit another client makes to the original at the same moment is lost.
|
|
413
|
+
* The block is left alone when it already sits between the right neighbours.
|
|
414
|
+
*/
|
|
415
|
+
export function placeStepBlock(
|
|
416
|
+
doc: YDoc,
|
|
417
|
+
stepId: string,
|
|
418
|
+
order: readonly string[],
|
|
419
|
+
): void {
|
|
420
|
+
const fragment = doc.getXmlFragment(DOC);
|
|
421
|
+
const found = findParentOf(fragment, stepIdToBlockId(stepId));
|
|
422
|
+
if (!found) return;
|
|
423
|
+
const { parent } = found;
|
|
424
|
+
const siblings = parent.toArray();
|
|
425
|
+
// A neighbour's index among the same siblings, or -1 when it has no block
|
|
426
|
+
// or its block lives under a different parent.
|
|
427
|
+
const indexIn = (id: string | undefined): number => {
|
|
428
|
+
if (id === undefined) return -1;
|
|
429
|
+
const located = findParentOf(fragment, stepIdToBlockId(id));
|
|
430
|
+
return located && located.parent === parent ? located.index : -1;
|
|
431
|
+
};
|
|
432
|
+
const at = order.indexOf(stepId);
|
|
433
|
+
const before = indexIn(order[at - 1]);
|
|
434
|
+
const after = indexIn(order[at + 1]);
|
|
435
|
+
const current = found.index;
|
|
436
|
+
if ((before < 0 || before < current) && (after < 0 || current < after))
|
|
437
|
+
return;
|
|
438
|
+
|
|
439
|
+
const element = siblings[current];
|
|
440
|
+
if (!(element instanceof Y.XmlElement)) return;
|
|
441
|
+
// Position in the sibling list once the original is removed.
|
|
442
|
+
const shift = (index: number): number =>
|
|
443
|
+
index > current ? index - 1 : index;
|
|
444
|
+
const target =
|
|
445
|
+
after >= 0 ? shift(after) : before >= 0 ? shift(before) + 1 : current;
|
|
446
|
+
doc.transact(() => {
|
|
447
|
+
const clone = element.clone();
|
|
448
|
+
parent.delete(current, 1);
|
|
449
|
+
parent.insert(target, [clone]);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
272
453
|
/** Reorder a step to a new 0-based index. Sequence is display-only; runtime/edges untouched. */
|
|
273
454
|
export function reorderStep(doc: YDoc, stepId: string, toIndex: number): void {
|
|
274
|
-
|
|
455
|
+
requireStep(doc, stepId);
|
|
275
456
|
const orderArr = doc.getArray<string>('qi.flow.order');
|
|
276
457
|
const current = orderArr.toArray();
|
|
277
458
|
const from = current.indexOf(stepId);
|
|
@@ -285,19 +466,9 @@ export function reorderStep(doc: YDoc, stepId: string, toIndex: number): void {
|
|
|
285
466
|
without.splice(target, 0, stepId);
|
|
286
467
|
|
|
287
468
|
doc.transact(() => {
|
|
288
|
-
// Move the fragment block by CLONE (yjs can't reinsert a deleted element).
|
|
289
|
-
const fragment = doc.getXmlFragment(DOC);
|
|
290
|
-
const found = findParentOf(fragment, blockId);
|
|
291
|
-
if (found) {
|
|
292
|
-
const element = found.parent.toArray()[found.index];
|
|
293
|
-
if (element instanceof Y.XmlElement) {
|
|
294
|
-
const clone = element.clone();
|
|
295
|
-
found.parent.delete(found.index, 1);
|
|
296
|
-
found.parent.insert(Math.min(target, found.parent.length), [clone]);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
469
|
orderArr.delete(0, orderArr.length);
|
|
300
470
|
orderArr.push(without);
|
|
471
|
+
placeStepBlock(doc, stepId, without);
|
|
301
472
|
});
|
|
302
473
|
}
|
|
303
474
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Uniform error model (spec §6.1). Tools never throw to the agent; expected
|
|
3
3
|
* failures come back as `{ ok: false, error: { code, message } }` with
|
|
4
|
-
* friendly, leak-safe messages.
|
|
4
|
+
* friendly, leak-safe messages. Invalid tool input (a zod parse failure,
|
|
5
|
+
* including a size limit) is `validation_failed` with the schema's messages.
|
|
5
6
|
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
6
9
|
export type FlowErrorCode =
|
|
7
10
|
| 'no_flow_ref'
|
|
8
11
|
| 'not_in_room'
|
|
@@ -11,6 +14,14 @@ export type FlowErrorCode =
|
|
|
11
14
|
| 'step_not_found'
|
|
12
15
|
| 'referenced'
|
|
13
16
|
| 'unknown_action'
|
|
17
|
+
| 'needs_access'
|
|
18
|
+
/**
|
|
19
|
+
* A write the homeserver never acknowledged (server errors, a network
|
|
20
|
+
* failure, or the wait ran out): it may or may not have been saved. The
|
|
21
|
+
* same code the editor uses, so the runtime keeps the write's claim and
|
|
22
|
+
* does not blindly run the identical write again.
|
|
23
|
+
*/
|
|
24
|
+
| 'write_not_saved'
|
|
14
25
|
| 'error';
|
|
15
26
|
|
|
16
27
|
export class FlowError extends Error {
|
|
@@ -33,6 +44,16 @@ export function toToolError(err: unknown): ToolErrorResult {
|
|
|
33
44
|
if (err instanceof FlowError) {
|
|
34
45
|
return { ok: false, error: { code: err.code, message: err.message } };
|
|
35
46
|
}
|
|
47
|
+
if (err instanceof z.ZodError) {
|
|
48
|
+
const message = err.issues
|
|
49
|
+
.map((issue) =>
|
|
50
|
+
issue.path.length > 0
|
|
51
|
+
? `${issue.path.join('.')}: ${issue.message}`
|
|
52
|
+
: issue.message,
|
|
53
|
+
)
|
|
54
|
+
.join(' ');
|
|
55
|
+
return { ok: false, error: { code: 'validation_failed', message } };
|
|
56
|
+
}
|
|
36
57
|
const message = err instanceof Error ? err.message : String(err);
|
|
37
58
|
return { ok: false, error: { code: 'error', message } };
|
|
38
59
|
}
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Doc as YDoc } from 'yjs';
|
|
12
12
|
import type { MatrixClient } from 'matrix-js-sdk';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DocWriteGuard,
|
|
15
|
+
MatrixProviderManager,
|
|
16
|
+
type AppConfig,
|
|
17
|
+
type DocWriteFailure,
|
|
18
|
+
} from '../editor/provider';
|
|
14
19
|
import { buildBlocknoteToolsConfig } from '../editor/editor-config';
|
|
15
20
|
import { resolveEditorMatrixClient } from '../editor/editor-mx';
|
|
16
21
|
import { isUserInRoom } from '../editor/room-membership';
|
|
@@ -58,17 +63,83 @@ export async function requireRoomMembership(
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
66
|
+
* The tool error for a write that did not reach the room:
|
|
67
|
+
* - refused as forbidden → `needs_access` (nothing was written);
|
|
68
|
+
* - refused with a client error (e.g. too large) → `error` (nothing was written);
|
|
69
|
+
* - not acknowledged (server errors, network, timeout) → `write_not_saved`:
|
|
70
|
+
* the change may have landed, so the flow must be re-read before any retry.
|
|
63
71
|
*/
|
|
64
|
-
|
|
72
|
+
function writeFailureError(failure: DocWriteFailure): FlowError {
|
|
73
|
+
if (failure.kind === 'forbidden')
|
|
74
|
+
return new FlowError(
|
|
75
|
+
'needs_access',
|
|
76
|
+
'The flow was not saved: this assistant is not allowed to edit this flow. ' +
|
|
77
|
+
'Ask a room admin to give it edit rights, then try again.',
|
|
78
|
+
);
|
|
79
|
+
if (!failure.mayHaveLanded)
|
|
80
|
+
return new FlowError(
|
|
81
|
+
'error',
|
|
82
|
+
`The flow was not saved: the homeserver refused the change (${failure.detail}).`,
|
|
83
|
+
);
|
|
84
|
+
return new FlowError(
|
|
85
|
+
'write_not_saved',
|
|
86
|
+
`The change may or may not have been saved: the homeserver did not acknowledge it (${failure.detail}). ` +
|
|
87
|
+
'Re-read the flow (read_flow or get_step) before retrying, so the change is not applied twice.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run compile-driven authoring (`setupFlowFromBaseUcan`), which opens and
|
|
93
|
+
* disposes its own provider, against a Matrix client whose document writes
|
|
94
|
+
* are bounded ({@link DocWriteGuard}, `drain` mode: the editor's own writer
|
|
95
|
+
* is configured to retry rejected writes for minutes). A write the
|
|
96
|
+
* homeserver refused or that could not be delivered becomes a tool error.
|
|
97
|
+
*/
|
|
98
|
+
export async function withFlowsCompileClient<T>(
|
|
65
99
|
rtCtx: RuntimeContext,
|
|
66
100
|
injectedClient: MatrixClient | undefined,
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
fn: (client: MatrixClient) => Promise<T>,
|
|
102
|
+
): Promise<T> {
|
|
103
|
+
const client = await resolveEditorMatrixClient({
|
|
69
104
|
...(await readMatrixCreds(rtCtx)),
|
|
70
105
|
matrixClient: injectedClient,
|
|
71
106
|
});
|
|
107
|
+
const guard = new DocWriteGuard(client, { onGiveUp: 'drain' });
|
|
108
|
+
try {
|
|
109
|
+
const result = await raceAbort(rtCtx, () => fn(guard.client));
|
|
110
|
+
if (guard.failure) throw writeFailureError(guard.failure);
|
|
111
|
+
return result;
|
|
112
|
+
} finally {
|
|
113
|
+
guard.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Settle with `start()`'s result, or reject as soon as the request is
|
|
119
|
+
* aborted. `start` runs only when the request is still live: a promise
|
|
120
|
+
* created before an abort check that then throws would be left with no
|
|
121
|
+
* handler, and its later rejection would go unhandled. Once started, the
|
|
122
|
+
* work stays attached to the race, so a rejection it produces after the
|
|
123
|
+
* abort is handled (the caller already has the abort as its error).
|
|
124
|
+
*/
|
|
125
|
+
function raceAbort<T>(
|
|
126
|
+
rtCtx: RuntimeContext,
|
|
127
|
+
start: () => Promise<T>,
|
|
128
|
+
): Promise<T> {
|
|
129
|
+
const signal = rtCtx.abortSignal;
|
|
130
|
+
signal.throwIfAborted();
|
|
131
|
+
const work = start();
|
|
132
|
+
let onAbort: (() => void) | undefined;
|
|
133
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
134
|
+
onAbort = () => {
|
|
135
|
+
const reason: unknown = signal.reason;
|
|
136
|
+
reject(reason instanceof Error ? reason : new Error('Request aborted'));
|
|
137
|
+
};
|
|
138
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
});
|
|
140
|
+
return Promise.race([work, aborted]).finally(() => {
|
|
141
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
142
|
+
});
|
|
72
143
|
}
|
|
73
144
|
|
|
74
145
|
function buildAppConfig(creds: MatrixCreds, roomId: string): AppConfig {
|
|
@@ -82,7 +153,15 @@ function buildAppConfig(creds: MatrixCreds, roomId: string): AppConfig {
|
|
|
82
153
|
|
|
83
154
|
/**
|
|
84
155
|
* Resolve the flow ref, enforce the room-membership guard (fail closed), open
|
|
85
|
-
* the room's Y.Doc, run `fn`,
|
|
156
|
+
* the room's Y.Doc, run `fn`, wait until every change `fn` made has reached
|
|
157
|
+
* the room, and always dispose the provider — on success, on a tool error,
|
|
158
|
+
* on abort and on a timeout.
|
|
159
|
+
*
|
|
160
|
+
* `fn` returning is not success on its own: Y.Doc edits apply locally at
|
|
161
|
+
* once and reach Matrix later. A forbidden write is `needs_access`; one
|
|
162
|
+
* refused with a client error is an `error`; one the homeserver never
|
|
163
|
+
* acknowledged (server errors, network, or the flush timeout ran out) is
|
|
164
|
+
* `write_not_saved` — see {@link writeFailureError}.
|
|
86
165
|
*/
|
|
87
166
|
export async function withFlowDoc<T>(
|
|
88
167
|
rtCtx: RuntimeContext,
|
|
@@ -90,6 +169,7 @@ export async function withFlowDoc<T>(
|
|
|
90
169
|
injectedClient: MatrixClient | undefined,
|
|
91
170
|
fn: (doc: YDoc, roomId: string) => Promise<T>,
|
|
92
171
|
): Promise<T> {
|
|
172
|
+
rtCtx.abortSignal.throwIfAborted();
|
|
93
173
|
const roomId = resolveFlowRef(rtCtx, ref);
|
|
94
174
|
await requireRoomMembership(rtCtx, roomId);
|
|
95
175
|
|
|
@@ -103,8 +183,11 @@ export async function withFlowDoc<T>(
|
|
|
103
183
|
buildAppConfig(creds, roomId),
|
|
104
184
|
);
|
|
105
185
|
try {
|
|
106
|
-
const { doc } = await manager.init();
|
|
107
|
-
|
|
186
|
+
const { doc } = await raceAbort(rtCtx, () => manager.init());
|
|
187
|
+
const result = await raceAbort(rtCtx, () => fn(doc, roomId));
|
|
188
|
+
const failure = await raceAbort(rtCtx, () => manager.flush());
|
|
189
|
+
if (failure) throw writeFailureError(failure);
|
|
190
|
+
return result;
|
|
108
191
|
} finally {
|
|
109
192
|
await manager.dispose();
|
|
110
193
|
}
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
PluginTool,
|
|
26
26
|
RuntimeContext,
|
|
27
27
|
} from '../../plugin-api/types';
|
|
28
|
-
import { FLOWS_PLUGIN_NAME } from './prompts';
|
|
28
|
+
import { FLOWS_OPERATING_GUIDE, FLOWS_PLUGIN_NAME } from './prompts';
|
|
29
29
|
|
|
30
30
|
export interface FlowsPluginOptions {
|
|
31
31
|
/** A long-lived Matrix client owned by the host (or a test), shared across rooms. */
|
|
@@ -35,10 +35,13 @@ export interface FlowsPluginOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* Discovery surface (returned by `list_capabilities` / `load_capability`). The
|
|
37
37
|
* `summary` is the one-line teaser; `whenToUse`/`whenNotToUse`/`examples` teach
|
|
38
|
-
* the agent when and how to load it. The full operating contract
|
|
39
|
-
* `FLOWS_OPERATING_GUIDE` (prompts.ts),
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* the agent when and how to load it. The full operating contract,
|
|
39
|
+
* `FLOWS_OPERATING_GUIDE` (prompts.ts), is the plugin's `operatingGuide`, not
|
|
40
|
+
* part of the manifest: `load_capability` returns it when it loads flows (also
|
|
41
|
+
* when the capability router had only preloaded it for the turn), and the
|
|
42
|
+
* system prompt carries it on every later turn that starts with flows loaded
|
|
43
|
+
* on the thread. A one-turn router preload leaves the system prompt alone, and
|
|
44
|
+
* turns that never use flows pay no tokens for it.
|
|
42
45
|
*/
|
|
43
46
|
const manifest: PluginManifest = {
|
|
44
47
|
title: 'Flow Builder',
|
|
@@ -54,6 +57,7 @@ const manifest: PluginManifest = {
|
|
|
54
57
|
whenNotToUse: [
|
|
55
58
|
'Editing prose/pages/documents (use the editor).',
|
|
56
59
|
'Actually executing/running/signing a step — that happens in the portal, by the user.',
|
|
60
|
+
'Collecting a PIN, mnemonic or other secret — the portal asks for it when the user runs the step; never ask for one in chat or write one into a flow.',
|
|
57
61
|
],
|
|
58
62
|
examples: [
|
|
59
63
|
{
|
|
@@ -83,6 +87,8 @@ export class FlowsPlugin extends OraclePlugin {
|
|
|
83
87
|
readonly name = FLOWS_PLUGIN_NAME;
|
|
84
88
|
readonly version = '0.1.0';
|
|
85
89
|
readonly manifest = manifest;
|
|
90
|
+
/** The operating contract — see the manifest comment above. */
|
|
91
|
+
override readonly operatingGuide = FLOWS_OPERATING_GUIDE;
|
|
86
92
|
|
|
87
93
|
private readonly matrixClient?: MatrixClient;
|
|
88
94
|
|
|
@@ -19,10 +19,23 @@ import {
|
|
|
19
19
|
} from './ydoc-helpers';
|
|
20
20
|
import { setStepInputs } from './edit';
|
|
21
21
|
import { FlowError } from './errors';
|
|
22
|
+
import {
|
|
23
|
+
MAX_FORM_ANSWERS_BYTES,
|
|
24
|
+
jsonByteLength,
|
|
25
|
+
secretLiteralMessage,
|
|
26
|
+
secretLiteralPaths,
|
|
27
|
+
} from './input-policy';
|
|
22
28
|
import { inferPortType } from './port-types';
|
|
23
29
|
import { readStep } from './read';
|
|
24
30
|
import { stepIdToBlockId } from './translator';
|
|
25
31
|
|
|
32
|
+
/** Runtime states in which a form's answers belong to the user's submission. */
|
|
33
|
+
const LOCKED_FORM_STATES: readonly string[] = [
|
|
34
|
+
'running',
|
|
35
|
+
'completed',
|
|
36
|
+
'awaiting_readback',
|
|
37
|
+
];
|
|
38
|
+
|
|
26
39
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
40
|
return typeof value === 'object' && value !== null;
|
|
28
41
|
}
|
|
@@ -197,6 +210,27 @@ export function fillForm(
|
|
|
197
210
|
merge = true,
|
|
198
211
|
): FillFormResult {
|
|
199
212
|
const { blockId, questions } = readSurveySchema(doc, stepId);
|
|
213
|
+
|
|
214
|
+
// Once the user has submitted (or the step is executing), the stored
|
|
215
|
+
// answers are what downstream steps read; a pre-fill must never replace them.
|
|
216
|
+
const state = readRuntimeState(doc, blockId)[blockId]?.state;
|
|
217
|
+
if (typeof state === 'string' && LOCKED_FORM_STATES.includes(state)) {
|
|
218
|
+
throw new FlowError(
|
|
219
|
+
'validation_failed',
|
|
220
|
+
`Step "${stepId}" is ${state}; its answers can no longer be pre-filled.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (jsonByteLength(answers) > MAX_FORM_ANSWERS_BYTES) {
|
|
224
|
+
throw new FlowError(
|
|
225
|
+
'validation_failed',
|
|
226
|
+
`Form answers may be at most ${MAX_FORM_ANSWERS_BYTES} bytes of JSON.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
// Answers are values, not wiring: a secret answer is refused whatever it holds.
|
|
230
|
+
const secrets = secretLiteralPaths(answers, new Set());
|
|
231
|
+
if (secrets.length > 0)
|
|
232
|
+
throw new FlowError('validation_failed', secretLiteralMessage(secrets));
|
|
233
|
+
|
|
200
234
|
const byName = new Map(questions.map((q) => [q.name, q]));
|
|
201
235
|
|
|
202
236
|
const applied: string[] = [];
|