create-safest-tools 0.2.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 (240) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +71 -0
  3. package/bin/create-safest-tools.mjs +12 -0
  4. package/package.json +34 -0
  5. package/src/cli.mjs +128 -0
  6. package/src/config.mjs +151 -0
  7. package/src/scaffold.mjs +30 -0
  8. package/template/LICENSE +201 -0
  9. package/template/README.md +35 -0
  10. package/template/console/README.md +28 -0
  11. package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
  12. package/template/console/analytics/controller.ts +133 -0
  13. package/template/console/analytics/events.ts +33 -0
  14. package/template/console/analytics/store.ts +33 -0
  15. package/template/console/analytics/types.ts +38 -0
  16. package/template/console/appeals/AppealWorkspace.tsx +73 -0
  17. package/template/console/appeals/controller.ts +125 -0
  18. package/template/console/appeals/events.ts +33 -0
  19. package/template/console/appeals/store.ts +35 -0
  20. package/template/console/appeals/types.ts +37 -0
  21. package/template/console/assistant/AssistantWorkspace.tsx +133 -0
  22. package/template/console/assistant/types.ts +34 -0
  23. package/template/console/auth/AccountJourney.tsx +69 -0
  24. package/template/console/auth/InvitationAcceptance.tsx +145 -0
  25. package/template/console/auth/OAuthButtons.tsx +78 -0
  26. package/template/console/auth/OperatorLogin.tsx +110 -0
  27. package/template/console/auth/PasswordRecovery.tsx +132 -0
  28. package/template/console/auth/account-route.ts +15 -0
  29. package/template/console/auth/browser-navigation.ts +7 -0
  30. package/template/console/auth/events.ts +15 -0
  31. package/template/console/command/CommandCentre.tsx +119 -0
  32. package/template/console/command/controller.ts +267 -0
  33. package/template/console/command/events.ts +19 -0
  34. package/template/console/command/store.ts +36 -0
  35. package/template/console/command/types.ts +61 -0
  36. package/template/console/components/AnalystIdentity.tsx +25 -0
  37. package/template/console/components/PageHeader.tsx +27 -0
  38. package/template/console/configuration/ConfigurationDialog.tsx +200 -0
  39. package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
  40. package/template/console/configuration/FormBuilder.tsx +143 -0
  41. package/template/console/configuration/api.ts +40 -0
  42. package/template/console/configuration/events.ts +14 -0
  43. package/template/console/configuration/types.ts +79 -0
  44. package/template/console/lib/format.ts +49 -0
  45. package/template/console/lib/http.ts +96 -0
  46. package/template/console/main.tsx +225 -0
  47. package/template/console/operations/OperationsWorkspace.tsx +126 -0
  48. package/template/console/operations/controller.ts +237 -0
  49. package/template/console/operations/events.ts +37 -0
  50. package/template/console/operations/store.ts +32 -0
  51. package/template/console/operations/types.ts +107 -0
  52. package/template/console/people/PeopleWorkspace.tsx +81 -0
  53. package/template/console/people/types.ts +25 -0
  54. package/template/console/profile/ProfileWorkspace.tsx +136 -0
  55. package/template/console/profile/events.ts +9 -0
  56. package/template/console/profile/types.ts +6 -0
  57. package/template/console/quality/QualityWorkspace.tsx +87 -0
  58. package/template/console/quality/controller.ts +154 -0
  59. package/template/console/quality/events.ts +34 -0
  60. package/template/console/quality/store.ts +36 -0
  61. package/template/console/quality/types.ts +70 -0
  62. package/template/console/queues/QueueEditor.tsx +173 -0
  63. package/template/console/queues/QueueWorkspace.tsx +109 -0
  64. package/template/console/queues/controller.ts +194 -0
  65. package/template/console/queues/events.ts +34 -0
  66. package/template/console/queues/store.ts +37 -0
  67. package/template/console/queues/types.ts +102 -0
  68. package/template/console/registry/RegistryDialog.tsx +198 -0
  69. package/template/console/registry/RegistryWorkspace.tsx +108 -0
  70. package/template/console/registry/events.ts +14 -0
  71. package/template/console/registry/types.ts +52 -0
  72. package/template/console/reports/ReportDrawer.tsx +159 -0
  73. package/template/console/reports/ReportWorkspace.tsx +90 -0
  74. package/template/console/reports/controller.ts +517 -0
  75. package/template/console/reports/events.ts +42 -0
  76. package/template/console/reports/store.ts +39 -0
  77. package/template/console/reports/types.ts +230 -0
  78. package/template/console/settings/BrandEditor.tsx +126 -0
  79. package/template/console/settings/ChannelDialog.tsx +241 -0
  80. package/template/console/settings/SettingsWorkspace.tsx +155 -0
  81. package/template/console/settings/events.ts +19 -0
  82. package/template/console/settings/handoff.ts +22 -0
  83. package/template/console/settings/types.ts +93 -0
  84. package/template/console/shell/WorkspaceShell.tsx +160 -0
  85. package/template/console/shell/controller.ts +182 -0
  86. package/template/console/shell/events.ts +25 -0
  87. package/template/console/shell/navigation.ts +61 -0
  88. package/template/console/shell/store.ts +56 -0
  89. package/template/console/shell/types.ts +86 -0
  90. package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
  91. package/template/console/workflows/WorkflowCanvas.tsx +42 -0
  92. package/template/console/workflows/WorkflowDialogs.tsx +6 -0
  93. package/template/console/workflows/WorkflowStudio.tsx +331 -0
  94. package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
  95. package/template/console/workflows/events.ts +17 -0
  96. package/template/console/workflows/graph.ts +156 -0
  97. package/template/console/workflows/templates.ts +70 -0
  98. package/template/console/workflows/types.ts +201 -0
  99. package/template/gitignore.template +18 -0
  100. package/template/migrations/0001_reports_foundation.sql +444 -0
  101. package/template/migrations/0002_human_report_loop.sql +65 -0
  102. package/template/migrations/0003_delivery_reliability.sql +18 -0
  103. package/template/migrations/0004_public_intake.sql +14 -0
  104. package/template/migrations/0005_operations_visibility.sql +24 -0
  105. package/template/migrations/0006_ai_governance.sql +198 -0
  106. package/template/migrations/0007_ai_release_gates.sql +6 -0
  107. package/template/migrations/0008_retention_analytics_exports.sql +52 -0
  108. package/template/migrations/0009_retention_derived_copies.sql +14 -0
  109. package/template/migrations/0010_ai_quality_controls.sql +26 -0
  110. package/template/migrations/0011_analyst_presence.sql +28 -0
  111. package/template/migrations/0012_queue_policies.sql +87 -0
  112. package/template/migrations/0013_routing_agents.sql +63 -0
  113. package/template/migrations/0014_webhook_enrichments.sql +122 -0
  114. package/template/migrations/0015_queue_owned_ai.sql +55 -0
  115. package/template/migrations/0016_operator_accounts.sql +67 -0
  116. package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
  117. package/template/migrations/0018_platform_configuration.sql +382 -0
  118. package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
  119. package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
  120. package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
  121. package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
  122. package/template/migrations/0023_component_connection_execution.sql +75 -0
  123. package/template/migrations/0024_access_runtime_integrity.sql +72 -0
  124. package/template/migrations/0025_installation_timezone.sql +11 -0
  125. package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
  126. package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
  127. package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
  128. package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
  129. package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
  130. package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
  131. package/template/migrations/0032_action_delivery_outbox.sql +55 -0
  132. package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
  133. package/template/migrations/0034_installation_integrations.sql +31 -0
  134. package/template/migrations/0035_workspace_governance.sql +21 -0
  135. package/template/migrations/0036_published_routing_baseline.sql +15 -0
  136. package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
  137. package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
  138. package/template/migrations/0039_secure_reporting_channels.sql +45 -0
  139. package/template/migrations/0040_notification_only_reporting.sql +24 -0
  140. package/template/migrations/0041_better_auth_credentials.sql +17 -0
  141. package/template/package.json +47 -0
  142. package/template/public/_headers +27 -0
  143. package/template/public/app-icon-192.png +0 -0
  144. package/template/public/app-icon-512.png +0 -0
  145. package/template/public/brand-icon.svg +7 -0
  146. package/template/public/brand-tokens.css +80 -0
  147. package/template/public/console/auth-shell.js +19340 -0
  148. package/template/public/customer-brand.js +58 -0
  149. package/template/public/embed/embed.css +139 -0
  150. package/template/public/embed/embed.js +408 -0
  151. package/template/public/embed/index.html +91 -0
  152. package/template/public/favicon.svg +7 -0
  153. package/template/public/fonts/Manrope-Variable.ttf +0 -0
  154. package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
  155. package/template/public/fonts/Newsreader-Variable.ttf +0 -0
  156. package/template/public/index.html +123 -0
  157. package/template/public/logo-primary.svg +7 -0
  158. package/template/public/logo-reversed.svg +7 -0
  159. package/template/public/manifest.webmanifest +21 -0
  160. package/template/public/public-report.js +184 -0
  161. package/template/public/report/index.html +39 -0
  162. package/template/public/report/public-report.css +20 -0
  163. package/template/public/social-card.png +0 -0
  164. package/template/public/styles.css +1521 -0
  165. package/template/public/widget.css +80 -0
  166. package/template/public/widget.js +220 -0
  167. package/template/reports.config.example.json +38 -0
  168. package/template/reports.schema.json +89 -0
  169. package/template/scripts/reports-auth-onboarding.mjs +180 -0
  170. package/template/scripts/reports-backup.mjs +276 -0
  171. package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
  172. package/template/scripts/reports-deploy.mjs +173 -0
  173. package/template/scripts/reports-plan.mjs +226 -0
  174. package/template/scripts/reports-restore.mjs +180 -0
  175. package/template/scripts/reports-secrets.mjs +73 -0
  176. package/template/scripts/reports-uninstall-plan.mjs +32 -0
  177. package/template/src/agent-executor.ts +330 -0
  178. package/template/src/ai-observability.ts +163 -0
  179. package/template/src/ai-registry-validation.ts +244 -0
  180. package/template/src/ai-registry.ts +292 -0
  181. package/template/src/api-cursor.ts +84 -0
  182. package/template/src/assistant.ts +554 -0
  183. package/template/src/audit.ts +39 -0
  184. package/template/src/backup-service.ts +269 -0
  185. package/template/src/budget-control.ts +127 -0
  186. package/template/src/component-executor.ts +845 -0
  187. package/template/src/configuration-registry.ts +550 -0
  188. package/template/src/connection-egress.ts +430 -0
  189. package/template/src/connection-oauth.ts +368 -0
  190. package/template/src/evidence-service.ts +335 -0
  191. package/template/src/human-tasks.ts +316 -0
  192. package/template/src/index.ts +4114 -0
  193. package/template/src/installation-admin.ts +301 -0
  194. package/template/src/intake-abuse.ts +156 -0
  195. package/template/src/platform-registry-validation.ts +367 -0
  196. package/template/src/platform-registry.ts +753 -0
  197. package/template/src/report-admin.ts +342 -0
  198. package/template/src/report-ai-quality.ts +390 -0
  199. package/template/src/report-ai-validation.ts +80 -0
  200. package/template/src/report-ai.ts +857 -0
  201. package/template/src/report-auth.ts +292 -0
  202. package/template/src/report-better-auth.ts +583 -0
  203. package/template/src/report-context-schema.ts +132 -0
  204. package/template/src/report-context.ts +126 -0
  205. package/template/src/report-crypto.ts +142 -0
  206. package/template/src/report-delivery.ts +579 -0
  207. package/template/src/report-form-validation.ts +51 -0
  208. package/template/src/report-governance.ts +510 -0
  209. package/template/src/report-http.ts +90 -0
  210. package/template/src/report-operations.ts +685 -0
  211. package/template/src/report-operator-accounts.ts +810 -0
  212. package/template/src/report-presence.ts +398 -0
  213. package/template/src/report-queue-validation.ts +309 -0
  214. package/template/src/report-queues.ts +478 -0
  215. package/template/src/report-repository.ts +972 -0
  216. package/template/src/report-router-agent.ts +328 -0
  217. package/template/src/report-router-validation.ts +81 -0
  218. package/template/src/report-routing.ts +178 -0
  219. package/template/src/report-turnstile.ts +142 -0
  220. package/template/src/report-types.ts +224 -0
  221. package/template/src/report-validation.ts +279 -0
  222. package/template/src/report-workflow-validation.ts +119 -0
  223. package/template/src/report-workflow.ts +886 -0
  224. package/template/src/shadow-quality.ts +278 -0
  225. package/template/src/workflow-actions.ts +782 -0
  226. package/template/src/workflow-compiler.ts +230 -0
  227. package/template/src/workflow-dynamic-runtime.ts +613 -0
  228. package/template/src/workflow-effects.ts +316 -0
  229. package/template/src/workflow-expressions.ts +191 -0
  230. package/template/src/workflow-platform-types.ts +121 -0
  231. package/template/src/workflow-platform-validation.ts +540 -0
  232. package/template/src/workflow-repository.ts +916 -0
  233. package/template/src/workflow-runs.ts +686 -0
  234. package/template/src/workflow-simulator.ts +211 -0
  235. package/template/src/workspace-branding.ts +128 -0
  236. package/template/src/workspace-governance.ts +279 -0
  237. package/template/tsconfig.console.json +23 -0
  238. package/template/tsconfig.json +22 -0
  239. package/template/vite.console.config.ts +20 -0
  240. package/template/worker-configuration.d.ts +65 -0
@@ -0,0 +1,613 @@
1
+ import {
2
+ createDynamicWorkflowEntrypoint,
3
+ DynamicWorkflowBinding,
4
+ wrapWorkflowBinding,
5
+ type WorkflowRunner,
6
+ } from "@cloudflare/dynamic-workflows";
7
+ import { WorkerEntrypoint } from "cloudflare:workers";
8
+ import { canonicalJson } from "./audit";
9
+ import {
10
+ ComponentExecutionError,
11
+ executeReadOnlyComponent,
12
+ loadWorkflowExecutionContext,
13
+ } from "./component-executor";
14
+ import { executeAgentProposal } from "./agent-executor";
15
+ import { sha256 } from "./report-crypto";
16
+ import { evaluateInputMapping, evaluateWorkflowExpression } from "./workflow-expressions";
17
+ import {
18
+ handoffWorkflowReport,
19
+ recordWorkflowDecision,
20
+ sendWorkflowMessage,
21
+ WorkflowEffectError,
22
+ } from "./workflow-effects";
23
+ import { completeWorkflowAction, prepareWorkflowAction } from "./workflow-actions";
24
+ import { createLinkedWorkflowRun } from "./workflow-runs";
25
+ import { parseWorkflowGraph } from "./workflow-platform-validation";
26
+ import type { JsonObject, JsonValue, WorkflowAuthorityMode, WorkflowGraphV1, WorkflowNodeV1 } from "./workflow-platform-types";
27
+
28
+ export { DynamicWorkflowBinding };
29
+
30
+ interface RuntimeProps {
31
+ artifactId: string;
32
+ workflowVersionId: string;
33
+ }
34
+
35
+ interface RuntimeNodeRequest {
36
+ runId: string;
37
+ nodeId: string;
38
+ nodeKind: string;
39
+ branchKey?: string;
40
+ authorityMode: WorkflowAuthorityMode;
41
+ input?: JsonObject;
42
+ }
43
+
44
+ interface RuntimeWaitRequest {
45
+ runId: string;
46
+ nodeId: string;
47
+ nodeKind: "human_task" | "wait_for_event";
48
+ branchKey?: string;
49
+ authorityMode: WorkflowAuthorityMode;
50
+ }
51
+
52
+ interface RuntimeCompleteWaitRequest {
53
+ runId: string;
54
+ nodeId: string;
55
+ branchKey?: string;
56
+ authorityMode: WorkflowAuthorityMode;
57
+ eventResult: { status: "received"; event: unknown } | { status: "timed_out" };
58
+ }
59
+
60
+ interface RuntimeActionRequest extends RuntimeNodeRequest {
61
+ nodeKind: "action";
62
+ }
63
+
64
+ interface RuntimeCompleteActionRequest extends RuntimeActionRequest {
65
+ proposalId: string;
66
+ eventResult: { status: "received"; event: unknown } | { status: "timed_out" };
67
+ }
68
+
69
+ interface RuntimeFailureRequest extends RuntimeNodeRequest {
70
+ errorCode: string;
71
+ }
72
+
73
+ interface ArtifactRow {
74
+ id: string;
75
+ workflowVersionId: string;
76
+ compatibilityDate: string;
77
+ bundleDigest: string;
78
+ bundleR2Key: string | null;
79
+ inlineSourceText: string | null;
80
+ status: string;
81
+ }
82
+
83
+ interface AuthorizedRun {
84
+ id: string;
85
+ workflowVersionId: string;
86
+ authorityMode: WorkflowAuthorityMode;
87
+ state: string;
88
+ graph: WorkflowGraphV1;
89
+ }
90
+
91
+ function now(): string {
92
+ return new Date().toISOString();
93
+ }
94
+
95
+ function cleanIdentifier(value: unknown, name: string, maximum = 200): string {
96
+ if (typeof value !== "string" || value.length < 1 || value.length > maximum || !/^[A-Za-z0-9._:-]+$/u.test(value)) {
97
+ throw new Error(`runtime_invalid_${name}`);
98
+ }
99
+ return value;
100
+ }
101
+
102
+ async function artifactRow(env: Env, artifactId: string, workflowVersionId: string): Promise<ArtifactRow> {
103
+ const row = await env.DB.prepare(`
104
+ SELECT a.id, v.id AS workflowVersionId, a.compatibility_date AS compatibilityDate,
105
+ a.bundle_digest AS bundleDigest, a.bundle_r2_key AS bundleR2Key,
106
+ a.inline_source_text AS inlineSourceText, a.status
107
+ FROM workflow_artifacts a JOIN workflow_versions v ON v.artifact_id = a.id
108
+ WHERE a.id = ?1 AND v.id = ?2 AND v.retired_at IS NULL LIMIT 1
109
+ `).bind(artifactId, workflowVersionId).first<ArtifactRow>();
110
+ if (!row || row.status !== "ready") throw new Error("workflow_artifact_not_ready");
111
+ return row;
112
+ }
113
+
114
+ async function artifactSource(env: Env, artifact: ArtifactRow): Promise<string> {
115
+ let source = artifact.inlineSourceText;
116
+ if (!source && artifact.bundleR2Key) source = await (await env.WORKFLOW_ARTIFACTS.get(artifact.bundleR2Key))?.text() ?? null;
117
+ if (!source) throw new Error("workflow_artifact_missing");
118
+ if (`sha256:${await sha256(source)}` !== artifact.bundleDigest) throw new Error("workflow_artifact_digest_mismatch");
119
+ return source;
120
+ }
121
+
122
+ function runtimeStub(ctx: ExecutionContext, props: RuntimeProps): Fetcher {
123
+ return ctx.exports.WorkflowRuntimeCapability({ props }) as unknown as Fetcher;
124
+ }
125
+
126
+ async function loadCompiledWorkflow(env: Env, ctx: ExecutionContext, metadata: RuntimeProps) {
127
+ const artifact = await artifactRow(env, metadata.artifactId, metadata.workflowVersionId);
128
+ const stub = env.LOADER.get(`workflow-version:${metadata.workflowVersionId}`, async () => ({
129
+ compatibilityDate: artifact.compatibilityDate,
130
+ mainModule: "workflow.mjs",
131
+ modules: { "workflow.mjs": await artifactSource(env, artifact) },
132
+ env: {
133
+ WORKFLOWS: wrapWorkflowBinding({
134
+ artifactId: metadata.artifactId,
135
+ workflowVersionId: metadata.workflowVersionId,
136
+ }),
137
+ RUNTIME: runtimeStub(ctx, metadata),
138
+ },
139
+ globalOutbound: null,
140
+ limits: { cpuMs: 30_000, subRequests: 50 },
141
+ }));
142
+ return stub;
143
+ }
144
+
145
+ export const DynamicWorkflow = createDynamicWorkflowEntrypoint<Env>(
146
+ async ({ env, ctx, metadata }) => {
147
+ const artifactId = cleanIdentifier(metadata.artifactId, "artifact_id");
148
+ const workflowVersionId = cleanIdentifier(metadata.workflowVersionId, "workflow_version_id");
149
+ const stub = await loadCompiledWorkflow(env, ctx, { artifactId, workflowVersionId });
150
+ return stub.getEntrypoint("TenantWorkflow") as unknown as WorkflowRunner;
151
+ },
152
+ );
153
+
154
+ async function authorizedRun(env: Env, props: RuntimeProps, input: RuntimeNodeRequest | RuntimeWaitRequest | RuntimeCompleteWaitRequest): Promise<AuthorizedRun> {
155
+ const runId = cleanIdentifier(input.runId, "run_id");
156
+ const row = await env.DB.prepare(`
157
+ SELECT r.id, r.workflow_version_id AS workflowVersionId,
158
+ r.authority_mode AS authorityMode, r.state, v.graph_json AS graphJson,
159
+ v.artifact_id AS artifactId
160
+ FROM workflow_runs r JOIN workflow_versions v ON v.id = r.workflow_version_id
161
+ WHERE r.id = ?1 LIMIT 1
162
+ `).bind(runId).first<{
163
+ id: string;
164
+ workflowVersionId: string;
165
+ authorityMode: WorkflowAuthorityMode;
166
+ state: string;
167
+ graphJson: string;
168
+ artifactId: string;
169
+ }>();
170
+ if (!row || row.workflowVersionId !== props.workflowVersionId || row.artifactId !== props.artifactId) throw new Error("runtime_capability_denied");
171
+ if (row.authorityMode !== input.authorityMode) throw new Error("runtime_authority_mismatch");
172
+ if (["succeeded", "failed", "terminated", "superseded"].includes(row.state)) throw new Error("runtime_run_terminal");
173
+ let parsed: unknown;
174
+ try {
175
+ parsed = JSON.parse(row.graphJson);
176
+ } catch {
177
+ throw new Error("runtime_graph_invalid");
178
+ }
179
+ return { id: row.id, workflowVersionId: row.workflowVersionId, authorityMode: row.authorityMode, state: row.state, graph: parseWorkflowGraph(parsed) };
180
+ }
181
+
182
+ function authorizedNode(run: AuthorizedRun, nodeIdValue: unknown, nodeKindValue?: unknown): WorkflowNodeV1 {
183
+ const nodeId = cleanIdentifier(nodeIdValue, "node_id", 100);
184
+ const node = run.graph.nodes.find((candidate) => candidate.id === nodeId);
185
+ if (!node || (nodeKindValue !== undefined && node.kind !== nodeKindValue)) throw new Error("runtime_node_denied");
186
+ return node;
187
+ }
188
+
189
+ function stepRunId(runId: string, nodeId: string, branchKey: string): string {
190
+ return `${runId}:${nodeId}:${branchKey || "main"}:1`;
191
+ }
192
+
193
+ function outputPort(node: WorkflowNodeV1): string {
194
+ switch (node.kind) {
195
+ case "start": return "success";
196
+ case "join": return "success";
197
+ case "delay":
198
+ case "delay_until": return "completed";
199
+ case "end": return "terminal";
200
+ default: throw new Error(`runtime_node_output_port_invalid:${node.kind}`);
201
+ }
202
+ }
203
+
204
+ function shadowSimulation(node: WorkflowNodeV1): Record<string, unknown> | null {
205
+ const ports: Partial<Record<WorkflowNodeV1["kind"], string>> = {
206
+ human_task: "completed",
207
+ send_message: "sent",
208
+ decision: "recorded",
209
+ handoff: "handed_off",
210
+ start_linked_workflow: "started",
211
+ };
212
+ const port = ports[node.kind];
213
+ return port ? {
214
+ port,
215
+ simulated: true,
216
+ effectSuppressed: true,
217
+ simulationCode: `shadow_${node.kind}_suppressed`,
218
+ } : null;
219
+ }
220
+
221
+ function conditionCases(node: WorkflowNodeV1): { port: string; when: JsonValue }[] {
222
+ const configured = node.config.cases;
223
+ if (Array.isArray(configured)) {
224
+ return configured.flatMap((entry) => {
225
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
226
+ const port = typeof entry.port === "string" ? entry.port : null;
227
+ return port && "when" in entry ? [{ port, when: entry.when }] : [];
228
+ });
229
+ }
230
+ if (configured && typeof configured === "object") {
231
+ return Object.entries(configured).map(([port, when]) => ({ port, when }));
232
+ }
233
+ return [];
234
+ }
235
+
236
+ async function executeCondition(
237
+ env: Env,
238
+ run: AuthorizedRun,
239
+ node: WorkflowNodeV1,
240
+ branchKey: string,
241
+ ): Promise<Record<string, unknown>> {
242
+ const loaded = await loadWorkflowExecutionContext(env, run.id, branchKey);
243
+ const nodeInput = evaluateInputMapping(node.inputMapping, loaded.context);
244
+ const context = { ...loaded.context, node_input: nodeInput };
245
+ const matched = conditionCases(node).filter((candidate) => evaluateWorkflowExpression(candidate.when, context) === true);
246
+ if (matched.length > 1) throw new Error("condition_not_mutually_exclusive");
247
+ const port = matched[0]?.port ?? (typeof node.config.default_port === "string" ? node.config.default_port : null);
248
+ const allowed = Array.isArray(node.config.case_ports) ? node.config.case_ports.filter((entry): entry is string => typeof entry === "string") : [];
249
+ if (!port || (!allowed.includes(port) && port !== node.config.default_port)) throw new Error("condition_port_invalid");
250
+ return { port, matched: matched[0]?.port ?? null, input: nodeInput };
251
+ }
252
+
253
+ async function recordSuccessfulStep(
254
+ env: Env,
255
+ run: AuthorizedRun,
256
+ node: WorkflowNodeV1,
257
+ branchKey: string,
258
+ output: Record<string, unknown>,
259
+ ): Promise<void> {
260
+ const timestamp = now();
261
+ const id = stepRunId(run.id, node.id, branchKey);
262
+ const outputJson = canonicalJson(output);
263
+ const outputDigest = `sha256:${await sha256(outputJson)}`;
264
+ await env.DB.batch([
265
+ env.DB.prepare(`
266
+ INSERT INTO workflow_step_runs (
267
+ id, run_id, node_id, node_type, branch_key, occurrence, state,
268
+ output_digest, started_at, finished_at, updated_at
269
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 1, 'succeeded', ?6, ?7, ?7, ?7)
270
+ ON CONFLICT(run_id, node_id, branch_key, occurrence) DO UPDATE SET
271
+ state = 'succeeded', output_digest = excluded.output_digest,
272
+ finished_at = excluded.finished_at, updated_at = excluded.updated_at
273
+ `).bind(id, run.id, node.id, node.kind, branchKey, outputDigest, timestamp),
274
+ env.DB.prepare(`
275
+ INSERT INTO workflow_step_outputs (
276
+ step_run_id, inline_json, digest, size_bytes, data_classification_json, created_at
277
+ ) VALUES (?1, ?2, ?3, ?4, '[]', ?5)
278
+ ON CONFLICT(step_run_id) DO UPDATE SET
279
+ inline_json = excluded.inline_json, digest = excluded.digest,
280
+ size_bytes = excluded.size_bytes
281
+ `).bind(id, outputJson, outputDigest, new TextEncoder().encode(outputJson).byteLength, timestamp),
282
+ env.DB.prepare(`
283
+ UPDATE workflow_runs SET state = CASE WHEN state IN ('created','dispatch_pending','queued','waiting') THEN 'running' ELSE state END,
284
+ current_node_id = ?2, started_at = COALESCE(started_at, ?3), waiting_at = NULL,
285
+ updated_at = ?3, revision = revision + 1 WHERE id = ?1
286
+ `).bind(run.id, node.id, timestamp),
287
+ ]);
288
+ }
289
+
290
+ async function recordDelayPreparation(
291
+ env: Env,
292
+ run: AuthorizedRun,
293
+ node: WorkflowNodeV1,
294
+ branchKey: string,
295
+ timestamp: string,
296
+ ): Promise<void> {
297
+ const updatedAt = now();
298
+ const id = stepRunId(run.id, node.id, branchKey);
299
+ await env.DB.batch([
300
+ env.DB.prepare(`
301
+ INSERT INTO workflow_step_runs (
302
+ id, run_id, node_id, node_type, branch_key, occurrence, state,
303
+ started_at, waiting_at, updated_at
304
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 1, 'waiting', ?6, ?6, ?6)
305
+ ON CONFLICT(run_id, node_id, branch_key, occurrence) DO UPDATE SET
306
+ state = 'waiting', waiting_at = excluded.waiting_at,
307
+ started_at = COALESCE(workflow_step_runs.started_at, excluded.started_at),
308
+ updated_at = excluded.updated_at
309
+ `).bind(id, run.id, node.id, node.kind, branchKey, updatedAt),
310
+ env.DB.prepare(`
311
+ UPDATE workflow_runs SET state = 'waiting', current_node_id = ?2,
312
+ waiting_at = ?3, started_at = COALESCE(started_at, ?3),
313
+ updated_at = ?3, revision = revision + 1 WHERE id = ?1
314
+ `).bind(run.id, node.id, updatedAt),
315
+ env.DB.prepare(`
316
+ INSERT OR IGNORE INTO audit_events (
317
+ id, idempotency_key, action, actor_type, actor_id,
318
+ target_type, target_id, details_json, created_at
319
+ ) VALUES (?1, ?2, 'workflow.delay_scheduled', 'system', 'workflow-runtime',
320
+ 'workflow_run', ?3, ?4, ?5)
321
+ `).bind(crypto.randomUUID(), `workflow-delay:${id}`, run.id, canonicalJson({ nodeId: node.id, resumeAt: timestamp }), updatedAt),
322
+ ]);
323
+ }
324
+
325
+ export class WorkflowRuntimeCapability extends WorkerEntrypoint<Env, RuntimeProps> {
326
+ async executeNode(input: RuntimeNodeRequest): Promise<Record<string, unknown>> {
327
+ const run = await authorizedRun(this.env, this.ctx.props, input);
328
+ const node = authorizedNode(run, input.nodeId, input.nodeKind);
329
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
330
+ let output: Record<string, unknown>;
331
+ try {
332
+ const simulated = run.authorityMode === "shadow" ? shadowSimulation(node) : null;
333
+ if (simulated) {
334
+ output = simulated;
335
+ } else if (node.kind === "enrichment") {
336
+ output = await executeReadOnlyComponent(this.env, {
337
+ runId: run.id,
338
+ node,
339
+ branchKey,
340
+ stepRunId: stepRunId(run.id, node.id, branchKey),
341
+ });
342
+ } else if (node.kind === "ai_proposal") {
343
+ const executionInput = {
344
+ runId: run.id, node, branchKey,
345
+ stepRunId: stepRunId(run.id, node.id, branchKey),
346
+ };
347
+ const executed = typeof node.config.component_version_id === "string"
348
+ ? await executeReadOnlyComponent(this.env, executionInput)
349
+ : await executeAgentProposal(this.env, executionInput);
350
+ const data = executed.data && typeof executed.data === "object" && !Array.isArray(executed.data)
351
+ ? executed.data as Record<string, unknown> : {};
352
+ const resultCode = [data.result_code, data.outcome, data.conclusion, data.classification]
353
+ .find((value): value is string => typeof value === "string");
354
+ const allowed = Array.isArray(node.config.result_ports)
355
+ ? node.config.result_ports.filter((entry): entry is string => typeof entry === "string") : [];
356
+ if (!resultCode || !allowed.includes(resultCode)) throw new ComponentExecutionError("ai_result_port_invalid", false);
357
+ output = { ...executed, port: resultCode };
358
+ } else if (node.kind === "condition") {
359
+ output = await executeCondition(this.env, run, node, branchKey);
360
+ } else if (node.kind === "send_message") {
361
+ output = await sendWorkflowMessage(this.env, { runId: run.id, node, branchKey });
362
+ } else if (node.kind === "decision") {
363
+ output = await recordWorkflowDecision(this.env, { runId: run.id, node, branchKey });
364
+ } else if (node.kind === "handoff") {
365
+ output = await handoffWorkflowReport(this.env, { runId: run.id, node, branchKey });
366
+ } else if (node.kind === "start_linked_workflow") {
367
+ const workflowVersionId = typeof node.config.workflow_version_id === "string" ? node.config.workflow_version_id : null;
368
+ if (!workflowVersionId) throw new WorkflowEffectError("linked_workflow_version_required");
369
+ const loaded = await loadWorkflowExecutionContext(this.env, run.id, branchKey);
370
+ const payload = evaluateInputMapping(node.inputMapping, loaded.context);
371
+ const linked = await createLinkedWorkflowRun(this.env, {
372
+ parentRunId: run.id,
373
+ nodeId: node.id,
374
+ workflowVersionId,
375
+ payload,
376
+ });
377
+ output = { port: "started", linkedRunId: linked.runId, workflowVersionId: linked.workflowVersionId, idempotentReplay: linked.idempotentReplay };
378
+ } else if (node.kind === "delay_until" && input.input?.prepare === true) {
379
+ const loaded = await loadWorkflowExecutionContext(this.env, run.id, branchKey);
380
+ const mapped = evaluateInputMapping(node.inputMapping, loaded.context);
381
+ if (typeof mapped.timestamp !== "string") throw new Error("delay_timestamp_invalid");
382
+ const timestamp = new Date(mapped.timestamp);
383
+ if (!Number.isFinite(timestamp.getTime()) || timestamp.getTime() < Date.now() - 1_000 || timestamp.getTime() > Date.now() + 31_536_000_000) throw new Error("delay_timestamp_invalid");
384
+ output = { port: "completed", timestamp: timestamp.toISOString(), prepared: true };
385
+ await recordDelayPreparation(this.env, run, node, branchKey, timestamp.toISOString());
386
+ return output;
387
+ } else {
388
+ const port = outputPort(node);
389
+ output = node.kind === "end"
390
+ ? { port, terminalCode: node.config.terminal_code ?? "complete" }
391
+ : { port, ...(input.input ?? {}) };
392
+ }
393
+ } catch (error) {
394
+ if ((error instanceof ComponentExecutionError || error instanceof WorkflowEffectError) && !error.retryable) {
395
+ output = { port: node.errorPort ?? "error", errorCode: error.code, retryable: false };
396
+ } else {
397
+ throw error;
398
+ }
399
+ }
400
+ await recordSuccessfulStep(this.env, run, node, branchKey, output);
401
+ if (node.kind === "end") {
402
+ const timestamp = now();
403
+ await this.env.DB.prepare(`
404
+ UPDATE workflow_runs SET state = 'succeeded', output_json = ?2,
405
+ current_node_id = ?3, finished_at = ?4, updated_at = ?4, revision = revision + 1
406
+ WHERE id = ?1 AND state NOT IN ('succeeded','failed','terminated','superseded')
407
+ `).bind(run.id, canonicalJson(output), node.id, timestamp).run();
408
+ }
409
+ return output;
410
+ }
411
+
412
+ async prepareWait(input: RuntimeWaitRequest): Promise<{ eventType: string; timeoutMs: number; taskId?: string }> {
413
+ const run = await authorizedRun(this.env, this.ctx.props, input);
414
+ const node = authorizedNode(run, input.nodeId, input.nodeKind);
415
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
416
+ if (run.authorityMode === "shadow" && node.kind === "human_task") throw new Error("shadow_human_wait_prohibited");
417
+ const id = stepRunId(run.id, node.id, branchKey);
418
+ const waitId = `${id}:wait`;
419
+ const taskId = node.kind === "human_task" ? `${id}:task` : undefined;
420
+ const eventType = node.kind === "human_task" ? `task.${taskId}.completed` : String(node.config.event_type);
421
+ const timeoutSeconds = Number(node.config.timeout_seconds ?? node.timeoutPolicy?.timeout_seconds ?? 86_400);
422
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 31_536_000) throw new Error("runtime_wait_timeout_invalid");
423
+ const timestamp = now();
424
+ const deadlineAt = new Date(Date.now() + timeoutSeconds * 1_000).toISOString();
425
+ const statements: D1PreparedStatement[] = [
426
+ this.env.DB.prepare(`
427
+ INSERT INTO workflow_step_runs (
428
+ id, run_id, node_id, node_type, branch_key, occurrence, state,
429
+ started_at, waiting_at, updated_at
430
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 1, 'waiting', ?6, ?6, ?6)
431
+ ON CONFLICT(run_id, node_id, branch_key, occurrence) DO UPDATE SET
432
+ state = 'waiting', waiting_at = excluded.waiting_at, updated_at = excluded.updated_at
433
+ `).bind(id, run.id, node.id, node.kind, branchKey, timestamp),
434
+ this.env.DB.prepare(`
435
+ INSERT INTO workflow_waits (
436
+ id, run_id, step_run_id, wait_type, event_type, state, deadline_at, created_at
437
+ ) VALUES (?1, ?2, ?3, ?4, ?5, 'waiting', ?6, ?7)
438
+ ON CONFLICT(step_run_id) DO NOTHING
439
+ `).bind(waitId, run.id, id, node.kind === "human_task" ? "human_task" : "event", eventType, deadlineAt, timestamp),
440
+ this.env.DB.prepare(`
441
+ UPDATE workflow_runs SET state = 'waiting', current_node_id = ?2,
442
+ waiting_at = ?3, started_at = COALESCE(started_at, ?3),
443
+ updated_at = ?3, revision = revision + 1 WHERE id = ?1
444
+ `).bind(run.id, node.id, timestamp),
445
+ ];
446
+ if (taskId) {
447
+ statements.push(this.env.DB.prepare(`
448
+ INSERT INTO human_tasks (
449
+ id, installation_id, report_id, appeal_id, run_id, step_run_id,
450
+ task_type, state, queue_id, context_json, context_digest,
451
+ response_schema_json, due_at, created_at, updated_at
452
+ ) SELECT
453
+ ?1, r.installation_id, r.report_id, r.appeal_id, r.id, ?2,
454
+ ?3, 'open', COALESCE(?4, r.queue_id), ?5, ?6, ?7, ?8, ?9, ?9
455
+ FROM workflow_runs r WHERE r.id = ?10
456
+ ON CONFLICT(step_run_id) DO NOTHING
457
+ `).bind(
458
+ taskId, id, String(node.config.task_type ?? "review"),
459
+ typeof node.config.queue_id === "string" && node.config.queue_id !== "run-queue" ? node.config.queue_id : null,
460
+ canonicalJson({ nodeId: node.id, inputMapping: node.inputMapping }),
461
+ `sha256:${await sha256(canonicalJson({ nodeId: node.id, inputMapping: node.inputMapping }))}`,
462
+ canonicalJson(node.config.response_schema ?? { type: "object" }), deadlineAt, timestamp, run.id,
463
+ ));
464
+ }
465
+ await this.env.DB.batch(statements);
466
+ return { eventType, timeoutMs: timeoutSeconds * 1_000, ...(taskId ? { taskId } : {}) };
467
+ }
468
+
469
+ async prepareAction(input: RuntimeActionRequest): Promise<Record<string, unknown>> {
470
+ const run = await authorizedRun(this.env, this.ctx.props, input);
471
+ const node = authorizedNode(run, input.nodeId, "action");
472
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
473
+ if (run.authorityMode === "off") throw new Error("action_authority_insufficient");
474
+ const id = stepRunId(run.id, node.id, branchKey);
475
+ if (run.authorityMode === "shadow") {
476
+ const result = { port: "applied", status: "simulated", simulated: true, effectSuppressed: true, simulationCode: "shadow_action_suppressed" };
477
+ await recordSuccessfulStep(this.env, run, node, branchKey, result);
478
+ return result;
479
+ }
480
+ const timestamp = now();
481
+ await this.env.DB.prepare(`
482
+ INSERT INTO workflow_step_runs (
483
+ id, run_id, node_id, node_type, branch_key, occurrence,
484
+ state, started_at, updated_at
485
+ ) VALUES (?1, ?2, ?3, 'action', ?4, 1, 'running', ?5, ?5)
486
+ ON CONFLICT(run_id, node_id, branch_key, occurrence) DO UPDATE SET
487
+ state = CASE WHEN workflow_step_runs.state = 'succeeded' THEN 'succeeded' ELSE 'running' END,
488
+ started_at = COALESCE(workflow_step_runs.started_at, excluded.started_at),
489
+ updated_at = excluded.updated_at
490
+ `).bind(id, run.id, node.id, branchKey, timestamp).run();
491
+ const result = await prepareWorkflowAction(this.env, {
492
+ runId: run.id,
493
+ node,
494
+ branchKey,
495
+ stepRunId: id,
496
+ authorityMode: run.authorityMode,
497
+ });
498
+ if (result.status === "approval_required" || result.status === "delivery_pending") {
499
+ await this.env.DB.batch([
500
+ this.env.DB.prepare(`UPDATE workflow_step_runs SET state = 'waiting', waiting_at = ?2, updated_at = ?2 WHERE id = ?1`).bind(id, timestamp),
501
+ this.env.DB.prepare(`UPDATE workflow_runs SET state = 'waiting', current_node_id = ?2, waiting_at = ?3, started_at = COALESCE(started_at, ?3), updated_at = ?3, revision = revision + 1 WHERE id = ?1`)
502
+ .bind(run.id, node.id, timestamp),
503
+ ]);
504
+ return result;
505
+ }
506
+ await recordSuccessfulStep(this.env, run, node, branchKey, result);
507
+ return result;
508
+ }
509
+
510
+ async completeAction(input: RuntimeCompleteActionRequest): Promise<Record<string, unknown>> {
511
+ const run = await authorizedRun(this.env, this.ctx.props, input);
512
+ const node = authorizedNode(run, input.nodeId, "action");
513
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
514
+ const expectedProposalId = `${stepRunId(run.id, node.id, branchKey)}:proposal`;
515
+ if (cleanIdentifier(input.proposalId, "proposal_id", 500) !== expectedProposalId) throw new Error("action_proposal_denied");
516
+ const result = await completeWorkflowAction(this.env, { proposalId: expectedProposalId, eventResult: input.eventResult });
517
+ if (result.status === "delivery_pending") {
518
+ const timestamp = now();
519
+ await this.env.DB.batch([
520
+ this.env.DB.prepare(`UPDATE workflow_step_runs SET state = 'waiting', waiting_at = ?2, updated_at = ?2 WHERE id = ?1`).bind(expectedProposalId.replace(/:proposal$/u, ""), timestamp),
521
+ this.env.DB.prepare(`UPDATE workflow_runs SET state = 'waiting', current_node_id = ?2, waiting_at = ?3, updated_at = ?3, revision = revision + 1 WHERE id = ?1`)
522
+ .bind(run.id, node.id, timestamp),
523
+ ]);
524
+ return result;
525
+ }
526
+ await recordSuccessfulStep(this.env, run, node, branchKey, result);
527
+ return result;
528
+ }
529
+
530
+ async failNode(input: RuntimeFailureRequest): Promise<Record<string, unknown>> {
531
+ const run = await authorizedRun(this.env, this.ctx.props, input);
532
+ const node = authorizedNode(run, input.nodeId, input.nodeKind);
533
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
534
+ const errorPort = node.errorPort;
535
+ if (!errorPort || !run.graph.edges.some((edge) => edge.from.nodeId === node.id && edge.from.port === errorPort)) {
536
+ const timestamp = now();
537
+ await this.env.DB.prepare(`
538
+ UPDATE workflow_runs SET state = 'repair_required', error_code = 'workflow_node_failed',
539
+ error_summary = ?2, updated_at = ?3, revision = revision + 1
540
+ WHERE id = ?1 AND state NOT IN ('succeeded','failed','terminated','superseded')
541
+ `).bind(run.id, `Node ${node.id} failed without a declared error port.`, timestamp).run();
542
+ throw new Error("workflow_node_failure_unhandled");
543
+ }
544
+ const output = {
545
+ port: errorPort,
546
+ errorCode: cleanIdentifier(input.errorCode, "error_code", 100),
547
+ exhausted: true,
548
+ };
549
+ await recordSuccessfulStep(this.env, run, node, branchKey, output);
550
+ return output;
551
+ }
552
+
553
+ async completeWait(input: RuntimeCompleteWaitRequest): Promise<Record<string, unknown>> {
554
+ const run = await authorizedRun(this.env, this.ctx.props, input);
555
+ const node = authorizedNode(run, input.nodeId);
556
+ if (node.kind !== "human_task" && node.kind !== "wait_for_event") throw new Error("runtime_wait_node_denied");
557
+ const branchKey = input.branchKey ? cleanIdentifier(input.branchKey, "branch_key", 100) : "";
558
+ const id = stepRunId(run.id, node.id, branchKey);
559
+ const timestamp = now();
560
+ const timedOut = input.eventResult.status === "timed_out";
561
+ const output = input.eventResult.status === "timed_out"
562
+ ? { port: "timeout", status: "timed_out" }
563
+ : { port: node.kind === "human_task" ? "completed" : "received", status: "received", event: input.eventResult.event };
564
+ const outputJson = canonicalJson(output);
565
+ const outputDigest = `sha256:${await sha256(outputJson)}`;
566
+ await this.env.DB.batch([
567
+ this.env.DB.prepare(`
568
+ UPDATE workflow_waits SET state = ?2, satisfied_at = ?3
569
+ WHERE step_run_id = ?1 AND state = 'waiting'
570
+ `).bind(id, timedOut ? "timed_out" : "satisfied", timestamp),
571
+ this.env.DB.prepare(`
572
+ UPDATE workflow_step_runs SET state = ?2, output_digest = ?3,
573
+ finished_at = ?4, updated_at = ?4 WHERE id = ?1 AND state = 'waiting'
574
+ `).bind(id, timedOut ? "timed_out" : "succeeded", outputDigest, timestamp),
575
+ this.env.DB.prepare(`
576
+ INSERT INTO workflow_step_outputs (
577
+ step_run_id, inline_json, digest, size_bytes, data_classification_json, created_at
578
+ ) VALUES (?1, ?2, ?3, ?4, '[]', ?5)
579
+ ON CONFLICT(step_run_id) DO UPDATE SET
580
+ inline_json = excluded.inline_json, digest = excluded.digest, size_bytes = excluded.size_bytes
581
+ `).bind(id, outputJson, outputDigest, new TextEncoder().encode(outputJson).byteLength, timestamp),
582
+ this.env.DB.prepare(`
583
+ UPDATE workflow_runs SET state = 'running', waiting_at = NULL,
584
+ updated_at = ?2, revision = revision + 1 WHERE id = ?1 AND state = 'waiting'
585
+ `).bind(run.id, timestamp),
586
+ ...(node.kind === "human_task" ? [this.env.DB.prepare(`
587
+ UPDATE human_tasks SET state = CASE WHEN ?2 = 1 THEN 'expired' ELSE 'completed' END,
588
+ completed_at = ?3, updated_at = ?3 WHERE step_run_id = ?1 AND state IN ('open','claimed')
589
+ `).bind(id, timedOut ? 1 : 0, timestamp)] : []),
590
+ ]);
591
+ return output;
592
+ }
593
+ }
594
+
595
+ export async function startDynamicWorkflow(
596
+ env: Env,
597
+ ctx: ExecutionContext,
598
+ input: { artifactId: string; workflowVersionId: string; instanceId: string; runId: string },
599
+ ): Promise<string> {
600
+ const metadata: RuntimeProps = {
601
+ artifactId: cleanIdentifier(input.artifactId, "artifact_id"),
602
+ workflowVersionId: cleanIdentifier(input.workflowVersionId, "workflow_version_id"),
603
+ };
604
+ const stub = await loadCompiledWorkflow(env, ctx, metadata);
605
+ const starter = stub.getEntrypoint() as unknown as {
606
+ start(params: { instanceId: string; runId: string }): Promise<{ id: string }>;
607
+ };
608
+ const result = await starter.start({
609
+ instanceId: cleanIdentifier(input.instanceId, "instance_id"),
610
+ runId: cleanIdentifier(input.runId, "run_id"),
611
+ });
612
+ return result.id;
613
+ }