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.
- package/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/create-safest-tools.mjs +12 -0
- package/package.json +34 -0
- package/src/cli.mjs +128 -0
- package/src/config.mjs +151 -0
- package/src/scaffold.mjs +30 -0
- package/template/LICENSE +201 -0
- package/template/README.md +35 -0
- package/template/console/README.md +28 -0
- package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
- package/template/console/analytics/controller.ts +133 -0
- package/template/console/analytics/events.ts +33 -0
- package/template/console/analytics/store.ts +33 -0
- package/template/console/analytics/types.ts +38 -0
- package/template/console/appeals/AppealWorkspace.tsx +73 -0
- package/template/console/appeals/controller.ts +125 -0
- package/template/console/appeals/events.ts +33 -0
- package/template/console/appeals/store.ts +35 -0
- package/template/console/appeals/types.ts +37 -0
- package/template/console/assistant/AssistantWorkspace.tsx +133 -0
- package/template/console/assistant/types.ts +34 -0
- package/template/console/auth/AccountJourney.tsx +69 -0
- package/template/console/auth/InvitationAcceptance.tsx +145 -0
- package/template/console/auth/OAuthButtons.tsx +78 -0
- package/template/console/auth/OperatorLogin.tsx +110 -0
- package/template/console/auth/PasswordRecovery.tsx +132 -0
- package/template/console/auth/account-route.ts +15 -0
- package/template/console/auth/browser-navigation.ts +7 -0
- package/template/console/auth/events.ts +15 -0
- package/template/console/command/CommandCentre.tsx +119 -0
- package/template/console/command/controller.ts +267 -0
- package/template/console/command/events.ts +19 -0
- package/template/console/command/store.ts +36 -0
- package/template/console/command/types.ts +61 -0
- package/template/console/components/AnalystIdentity.tsx +25 -0
- package/template/console/components/PageHeader.tsx +27 -0
- package/template/console/configuration/ConfigurationDialog.tsx +200 -0
- package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
- package/template/console/configuration/FormBuilder.tsx +143 -0
- package/template/console/configuration/api.ts +40 -0
- package/template/console/configuration/events.ts +14 -0
- package/template/console/configuration/types.ts +79 -0
- package/template/console/lib/format.ts +49 -0
- package/template/console/lib/http.ts +96 -0
- package/template/console/main.tsx +225 -0
- package/template/console/operations/OperationsWorkspace.tsx +126 -0
- package/template/console/operations/controller.ts +237 -0
- package/template/console/operations/events.ts +37 -0
- package/template/console/operations/store.ts +32 -0
- package/template/console/operations/types.ts +107 -0
- package/template/console/people/PeopleWorkspace.tsx +81 -0
- package/template/console/people/types.ts +25 -0
- package/template/console/profile/ProfileWorkspace.tsx +136 -0
- package/template/console/profile/events.ts +9 -0
- package/template/console/profile/types.ts +6 -0
- package/template/console/quality/QualityWorkspace.tsx +87 -0
- package/template/console/quality/controller.ts +154 -0
- package/template/console/quality/events.ts +34 -0
- package/template/console/quality/store.ts +36 -0
- package/template/console/quality/types.ts +70 -0
- package/template/console/queues/QueueEditor.tsx +173 -0
- package/template/console/queues/QueueWorkspace.tsx +109 -0
- package/template/console/queues/controller.ts +194 -0
- package/template/console/queues/events.ts +34 -0
- package/template/console/queues/store.ts +37 -0
- package/template/console/queues/types.ts +102 -0
- package/template/console/registry/RegistryDialog.tsx +198 -0
- package/template/console/registry/RegistryWorkspace.tsx +108 -0
- package/template/console/registry/events.ts +14 -0
- package/template/console/registry/types.ts +52 -0
- package/template/console/reports/ReportDrawer.tsx +159 -0
- package/template/console/reports/ReportWorkspace.tsx +90 -0
- package/template/console/reports/controller.ts +517 -0
- package/template/console/reports/events.ts +42 -0
- package/template/console/reports/store.ts +39 -0
- package/template/console/reports/types.ts +230 -0
- package/template/console/settings/BrandEditor.tsx +126 -0
- package/template/console/settings/ChannelDialog.tsx +241 -0
- package/template/console/settings/SettingsWorkspace.tsx +155 -0
- package/template/console/settings/events.ts +19 -0
- package/template/console/settings/handoff.ts +22 -0
- package/template/console/settings/types.ts +93 -0
- package/template/console/shell/WorkspaceShell.tsx +160 -0
- package/template/console/shell/controller.ts +182 -0
- package/template/console/shell/events.ts +25 -0
- package/template/console/shell/navigation.ts +61 -0
- package/template/console/shell/store.ts +56 -0
- package/template/console/shell/types.ts +86 -0
- package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
- package/template/console/workflows/WorkflowCanvas.tsx +42 -0
- package/template/console/workflows/WorkflowDialogs.tsx +6 -0
- package/template/console/workflows/WorkflowStudio.tsx +331 -0
- package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
- package/template/console/workflows/events.ts +17 -0
- package/template/console/workflows/graph.ts +156 -0
- package/template/console/workflows/templates.ts +70 -0
- package/template/console/workflows/types.ts +201 -0
- package/template/gitignore.template +18 -0
- package/template/migrations/0001_reports_foundation.sql +444 -0
- package/template/migrations/0002_human_report_loop.sql +65 -0
- package/template/migrations/0003_delivery_reliability.sql +18 -0
- package/template/migrations/0004_public_intake.sql +14 -0
- package/template/migrations/0005_operations_visibility.sql +24 -0
- package/template/migrations/0006_ai_governance.sql +198 -0
- package/template/migrations/0007_ai_release_gates.sql +6 -0
- package/template/migrations/0008_retention_analytics_exports.sql +52 -0
- package/template/migrations/0009_retention_derived_copies.sql +14 -0
- package/template/migrations/0010_ai_quality_controls.sql +26 -0
- package/template/migrations/0011_analyst_presence.sql +28 -0
- package/template/migrations/0012_queue_policies.sql +87 -0
- package/template/migrations/0013_routing_agents.sql +63 -0
- package/template/migrations/0014_webhook_enrichments.sql +122 -0
- package/template/migrations/0015_queue_owned_ai.sql +55 -0
- package/template/migrations/0016_operator_accounts.sql +67 -0
- package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
- package/template/migrations/0018_platform_configuration.sql +382 -0
- package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
- package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
- package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
- package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
- package/template/migrations/0023_component_connection_execution.sql +75 -0
- package/template/migrations/0024_access_runtime_integrity.sql +72 -0
- package/template/migrations/0025_installation_timezone.sql +11 -0
- package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
- package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
- package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
- package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
- package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
- package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
- package/template/migrations/0032_action_delivery_outbox.sql +55 -0
- package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
- package/template/migrations/0034_installation_integrations.sql +31 -0
- package/template/migrations/0035_workspace_governance.sql +21 -0
- package/template/migrations/0036_published_routing_baseline.sql +15 -0
- package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
- package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
- package/template/migrations/0039_secure_reporting_channels.sql +45 -0
- package/template/migrations/0040_notification_only_reporting.sql +24 -0
- package/template/migrations/0041_better_auth_credentials.sql +17 -0
- package/template/package.json +47 -0
- package/template/public/_headers +27 -0
- package/template/public/app-icon-192.png +0 -0
- package/template/public/app-icon-512.png +0 -0
- package/template/public/brand-icon.svg +7 -0
- package/template/public/brand-tokens.css +80 -0
- package/template/public/console/auth-shell.js +19340 -0
- package/template/public/customer-brand.js +58 -0
- package/template/public/embed/embed.css +139 -0
- package/template/public/embed/embed.js +408 -0
- package/template/public/embed/index.html +91 -0
- package/template/public/favicon.svg +7 -0
- package/template/public/fonts/Manrope-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Variable.ttf +0 -0
- package/template/public/index.html +123 -0
- package/template/public/logo-primary.svg +7 -0
- package/template/public/logo-reversed.svg +7 -0
- package/template/public/manifest.webmanifest +21 -0
- package/template/public/public-report.js +184 -0
- package/template/public/report/index.html +39 -0
- package/template/public/report/public-report.css +20 -0
- package/template/public/social-card.png +0 -0
- package/template/public/styles.css +1521 -0
- package/template/public/widget.css +80 -0
- package/template/public/widget.js +220 -0
- package/template/reports.config.example.json +38 -0
- package/template/reports.schema.json +89 -0
- package/template/scripts/reports-auth-onboarding.mjs +180 -0
- package/template/scripts/reports-backup.mjs +276 -0
- package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
- package/template/scripts/reports-deploy.mjs +173 -0
- package/template/scripts/reports-plan.mjs +226 -0
- package/template/scripts/reports-restore.mjs +180 -0
- package/template/scripts/reports-secrets.mjs +73 -0
- package/template/scripts/reports-uninstall-plan.mjs +32 -0
- package/template/src/agent-executor.ts +330 -0
- package/template/src/ai-observability.ts +163 -0
- package/template/src/ai-registry-validation.ts +244 -0
- package/template/src/ai-registry.ts +292 -0
- package/template/src/api-cursor.ts +84 -0
- package/template/src/assistant.ts +554 -0
- package/template/src/audit.ts +39 -0
- package/template/src/backup-service.ts +269 -0
- package/template/src/budget-control.ts +127 -0
- package/template/src/component-executor.ts +845 -0
- package/template/src/configuration-registry.ts +550 -0
- package/template/src/connection-egress.ts +430 -0
- package/template/src/connection-oauth.ts +368 -0
- package/template/src/evidence-service.ts +335 -0
- package/template/src/human-tasks.ts +316 -0
- package/template/src/index.ts +4114 -0
- package/template/src/installation-admin.ts +301 -0
- package/template/src/intake-abuse.ts +156 -0
- package/template/src/platform-registry-validation.ts +367 -0
- package/template/src/platform-registry.ts +753 -0
- package/template/src/report-admin.ts +342 -0
- package/template/src/report-ai-quality.ts +390 -0
- package/template/src/report-ai-validation.ts +80 -0
- package/template/src/report-ai.ts +857 -0
- package/template/src/report-auth.ts +292 -0
- package/template/src/report-better-auth.ts +583 -0
- package/template/src/report-context-schema.ts +132 -0
- package/template/src/report-context.ts +126 -0
- package/template/src/report-crypto.ts +142 -0
- package/template/src/report-delivery.ts +579 -0
- package/template/src/report-form-validation.ts +51 -0
- package/template/src/report-governance.ts +510 -0
- package/template/src/report-http.ts +90 -0
- package/template/src/report-operations.ts +685 -0
- package/template/src/report-operator-accounts.ts +810 -0
- package/template/src/report-presence.ts +398 -0
- package/template/src/report-queue-validation.ts +309 -0
- package/template/src/report-queues.ts +478 -0
- package/template/src/report-repository.ts +972 -0
- package/template/src/report-router-agent.ts +328 -0
- package/template/src/report-router-validation.ts +81 -0
- package/template/src/report-routing.ts +178 -0
- package/template/src/report-turnstile.ts +142 -0
- package/template/src/report-types.ts +224 -0
- package/template/src/report-validation.ts +279 -0
- package/template/src/report-workflow-validation.ts +119 -0
- package/template/src/report-workflow.ts +886 -0
- package/template/src/shadow-quality.ts +278 -0
- package/template/src/workflow-actions.ts +782 -0
- package/template/src/workflow-compiler.ts +230 -0
- package/template/src/workflow-dynamic-runtime.ts +613 -0
- package/template/src/workflow-effects.ts +316 -0
- package/template/src/workflow-expressions.ts +191 -0
- package/template/src/workflow-platform-types.ts +121 -0
- package/template/src/workflow-platform-validation.ts +540 -0
- package/template/src/workflow-repository.ts +916 -0
- package/template/src/workflow-runs.ts +686 -0
- package/template/src/workflow-simulator.ts +211 -0
- package/template/src/workspace-branding.ts +128 -0
- package/template/src/workspace-governance.ts +279 -0
- package/template/tsconfig.console.json +23 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.console.config.ts +20 -0
- 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
|
+
}
|