@xdelivered/emberflow 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 +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
package/server/redact.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure value-based secret redaction. Deep-clones a JSON-safe payload,
|
|
3
|
+
* replacing every occurrence of each secret VALUE (raw and its
|
|
4
|
+
* encodeURIComponent form) inside any string leaf with «secret:KEY».
|
|
5
|
+
*/
|
|
6
|
+
export function redactSecrets<T>(payload: T, secrets: Record<string, string>): T {
|
|
7
|
+
const replacements: Array<[string, string]> = [];
|
|
8
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
9
|
+
if (value.length < 6) continue;
|
|
10
|
+
const token = `«secret:${key}»`;
|
|
11
|
+
const encoded = encodeURIComponent(value);
|
|
12
|
+
const base64 = Buffer.from(value).toString('base64');
|
|
13
|
+
replacements.push([value, token]);
|
|
14
|
+
if (encoded !== value) replacements.push([encoded, token]);
|
|
15
|
+
if (base64 !== value && base64 !== encoded) replacements.push([base64, token]);
|
|
16
|
+
}
|
|
17
|
+
// Longest value first, so an encoded superstring is replaced before the raw substring it contains.
|
|
18
|
+
replacements.sort((a, b) => b[0].length - a[0].length);
|
|
19
|
+
|
|
20
|
+
const redactString = (input: string): string =>
|
|
21
|
+
replacements.reduce((acc, [value, token]) => acc.split(value).join(token), input);
|
|
22
|
+
|
|
23
|
+
const walk = (node: unknown): unknown => {
|
|
24
|
+
if (typeof node === 'string') return redactString(node);
|
|
25
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
26
|
+
if (node !== null && typeof node === 'object') {
|
|
27
|
+
const out: Record<string, unknown> = {};
|
|
28
|
+
for (const [k, v] of Object.entries(node)) out[k] = walk(v);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return node;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return walk(payload) as T;
|
|
35
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// Verifies secret values never reach the studio via SSE run events or
|
|
8
|
+
// GET /samples, per Task 2 (egress-choke-point redaction). Boots the actual
|
|
9
|
+
// runner subprocess (server/index.ts) against scratch project dirs, mirroring
|
|
10
|
+
// the harness in runsAuth.test.ts.
|
|
11
|
+
|
|
12
|
+
const PORT = 8130;
|
|
13
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
14
|
+
const SECRET = 'supersecretvalue123';
|
|
15
|
+
|
|
16
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
17
|
+
for (let i = 0; i < tries; i++) {
|
|
18
|
+
try {
|
|
19
|
+
if ((await fetch(url)).ok) return;
|
|
20
|
+
} catch {
|
|
21
|
+
/* not up yet */
|
|
22
|
+
}
|
|
23
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
24
|
+
}
|
|
25
|
+
throw new Error('runner did not become healthy');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
29
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
30
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
31
|
+
stdio: 'ignore',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Single-node flow: an Input node that just echoes whatever input the run
|
|
36
|
+
// was invoked with (including `headers`).
|
|
37
|
+
const echoFlow = {
|
|
38
|
+
id: 'echo-input',
|
|
39
|
+
name: 'Echo Input',
|
|
40
|
+
version: 1,
|
|
41
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
42
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
43
|
+
nodes: [
|
|
44
|
+
{
|
|
45
|
+
id: 'input',
|
|
46
|
+
type: 'Input',
|
|
47
|
+
label: 'Input',
|
|
48
|
+
position: { x: 0, y: 0 },
|
|
49
|
+
config: { fields: [], defaults: {} },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
edges: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Collects every SSE event emitted for a run until it finishes. */
|
|
56
|
+
async function collectSseEvents(runId: string): Promise<Array<Record<string, unknown>>> {
|
|
57
|
+
const res = await fetch(`${base}/api/runs/${runId}/events`);
|
|
58
|
+
if (!res.body) throw new Error('no SSE body');
|
|
59
|
+
const reader = res.body.getReader();
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
let buf = '';
|
|
62
|
+
const events: Array<Record<string, unknown>> = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
const { value, done } = await reader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
buf += decoder.decode(value, { stream: true });
|
|
67
|
+
let idx: number;
|
|
68
|
+
while ((idx = buf.indexOf('\n\n')) !== -1) {
|
|
69
|
+
const chunk = buf.slice(0, idx);
|
|
70
|
+
buf = buf.slice(idx + 2);
|
|
71
|
+
const dataLine = chunk.split('\n').find((l) => l.startsWith('data: '));
|
|
72
|
+
if (dataLine) {
|
|
73
|
+
const parsed = JSON.parse(dataLine.slice('data: '.length));
|
|
74
|
+
events.push(parsed);
|
|
75
|
+
if (parsed.type === 'finished') {
|
|
76
|
+
reader.cancel();
|
|
77
|
+
return events;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return events;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('secret redaction at egress choke points', () => {
|
|
86
|
+
describe('with a configured auth secret', () => {
|
|
87
|
+
let projectDir: string;
|
|
88
|
+
let proc: ChildProcess;
|
|
89
|
+
|
|
90
|
+
beforeAll(async () => {
|
|
91
|
+
projectDir = mkdtempSync(join(tmpdir(), 'redactegress-'));
|
|
92
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
93
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
defaultEnvironment: 'local',
|
|
98
|
+
environments: {
|
|
99
|
+
local: {
|
|
100
|
+
vars: {},
|
|
101
|
+
secrets: { sessionCookie: SECRET },
|
|
102
|
+
auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
108
|
+
await waitHealthy(`${base}/healthz`);
|
|
109
|
+
}, 20_000);
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
proc?.kill();
|
|
113
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('never leaks the raw secret value over SSE, and redacts the attached cookie', async () => {
|
|
117
|
+
const createRes = await fetch(`${base}/api/runs`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ flow: echoFlow, mode: 'run', input: {} }),
|
|
121
|
+
});
|
|
122
|
+
expect(createRes.status).toBe(201);
|
|
123
|
+
const { runId } = await createRes.json();
|
|
124
|
+
|
|
125
|
+
const events = await collectSseEvents(runId);
|
|
126
|
+
expect(events.length).toBeGreaterThan(0);
|
|
127
|
+
|
|
128
|
+
const raw = JSON.stringify(events);
|
|
129
|
+
expect(raw).not.toContain(SECRET);
|
|
130
|
+
|
|
131
|
+
const nodeStateEvents = events.filter(
|
|
132
|
+
(e) => e.type === 'nodeState' && (e as { nodeId?: string }).nodeId === 'input',
|
|
133
|
+
);
|
|
134
|
+
const succeeded = nodeStateEvents.find(
|
|
135
|
+
(e) => (e.state as { status?: string } | undefined)?.status === 'succeeded',
|
|
136
|
+
);
|
|
137
|
+
expect(succeeded).toBeDefined();
|
|
138
|
+
const output = (succeeded!.state as { output?: { headers?: { cookie?: string } } }).output;
|
|
139
|
+
expect(output?.headers?.cookie).toContain('session=«secret:sessionCookie»');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('redacts the sample output returned via GET /samples', async () => {
|
|
143
|
+
const createRes = await fetch(`${base}/api/runs`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ flow: echoFlow, mode: 'step', input: {} }),
|
|
147
|
+
});
|
|
148
|
+
expect(createRes.status).toBe(201);
|
|
149
|
+
const { runId } = await createRes.json();
|
|
150
|
+
|
|
151
|
+
const stepRes = await fetch(`${base}/api/runs/${runId}/step`, { method: 'POST' });
|
|
152
|
+
expect(stepRes.status).toBe(200);
|
|
153
|
+
|
|
154
|
+
const samplesRes = await fetch(`${base}/api/samples?nodeId=input`);
|
|
155
|
+
expect(samplesRes.status).toBe(200);
|
|
156
|
+
const body = await samplesRes.json();
|
|
157
|
+
const raw = JSON.stringify(body);
|
|
158
|
+
expect(raw).not.toContain(SECRET);
|
|
159
|
+
|
|
160
|
+
const sample = body.samples.find((s: { runId: string }) => s.runId === runId);
|
|
161
|
+
expect(sample).toBeDefined();
|
|
162
|
+
expect(sample.output.headers.cookie).toContain('session=«secret:sessionCookie»');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('with two environments sharing a secret key (different values)', () => {
|
|
167
|
+
let projectDir: string;
|
|
168
|
+
let proc: ChildProcess;
|
|
169
|
+
const port = 8141;
|
|
170
|
+
const base3 = `http://127.0.0.1:${port}`;
|
|
171
|
+
const DEV_SECRET = 'devsecretvalue-abc123';
|
|
172
|
+
const PROD_SECRET = 'prodsecretvalue-xyz789';
|
|
173
|
+
|
|
174
|
+
beforeAll(async () => {
|
|
175
|
+
projectDir = mkdtempSync(join(tmpdir(), 'redactegress-multienv-'));
|
|
176
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
177
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
178
|
+
writeFileSync(
|
|
179
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
defaultEnvironment: 'dev',
|
|
182
|
+
environments: {
|
|
183
|
+
// Both envs define the SAME key with DIFFERENT values. A merged
|
|
184
|
+
// redaction map keyed by name would keep only prod's value and
|
|
185
|
+
// let dev's pass through /samples raw.
|
|
186
|
+
dev: {
|
|
187
|
+
vars: {},
|
|
188
|
+
secrets: { sessionCookie: DEV_SECRET },
|
|
189
|
+
auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
|
|
190
|
+
},
|
|
191
|
+
prod: {
|
|
192
|
+
vars: {},
|
|
193
|
+
secrets: { sessionCookie: PROD_SECRET },
|
|
194
|
+
auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
proc = bootRunner(port, { EMBERFLOW_PROJECT: projectDir });
|
|
200
|
+
await waitHealthy(`${base3}/healthz`);
|
|
201
|
+
}, 20_000);
|
|
202
|
+
|
|
203
|
+
afterAll(() => {
|
|
204
|
+
proc?.kill();
|
|
205
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('GET /samples leaks NEITHER environment value raw, even on key collision', async () => {
|
|
209
|
+
// Run against dev (the default env): dev's value lands in the sample
|
|
210
|
+
// via the auto-attached cookie.
|
|
211
|
+
const createRes = await fetch(`${base3}/api/runs`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'Content-Type': 'application/json' },
|
|
214
|
+
body: JSON.stringify({ flow: echoFlow, mode: 'step', input: {} }),
|
|
215
|
+
});
|
|
216
|
+
expect(createRes.status).toBe(201);
|
|
217
|
+
const { runId } = await createRes.json();
|
|
218
|
+
|
|
219
|
+
const stepRes = await fetch(`${base3}/api/runs/${runId}/step`, { method: 'POST' });
|
|
220
|
+
expect(stepRes.status).toBe(200);
|
|
221
|
+
|
|
222
|
+
const samplesRes = await fetch(`${base3}/api/samples?nodeId=input`);
|
|
223
|
+
expect(samplesRes.status).toBe(200);
|
|
224
|
+
const body = await samplesRes.json();
|
|
225
|
+
const raw = JSON.stringify(body);
|
|
226
|
+
expect(raw).not.toContain(DEV_SECRET);
|
|
227
|
+
expect(raw).not.toContain(PROD_SECRET);
|
|
228
|
+
|
|
229
|
+
const sample = body.samples.find((s: { runId: string }) => s.runId === runId);
|
|
230
|
+
expect(sample).toBeDefined();
|
|
231
|
+
expect(sample.output.headers.cookie).toContain('session=«secret:sessionCookie»');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('without any secrets configured', () => {
|
|
236
|
+
let projectDir: string;
|
|
237
|
+
let proc: ChildProcess;
|
|
238
|
+
const port = 8140;
|
|
239
|
+
const base2 = `http://127.0.0.1:${port}`;
|
|
240
|
+
|
|
241
|
+
beforeAll(async () => {
|
|
242
|
+
projectDir = mkdtempSync(join(tmpdir(), 'redactegress-nosecrets-'));
|
|
243
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
244
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
247
|
+
JSON.stringify({
|
|
248
|
+
defaultEnvironment: 'local',
|
|
249
|
+
environments: { local: { vars: {}, secrets: {} } },
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
proc = bootRunner(port, { EMBERFLOW_PROJECT: projectDir });
|
|
253
|
+
await waitHealthy(`${base2}/healthz`);
|
|
254
|
+
}, 20_000);
|
|
255
|
+
|
|
256
|
+
afterAll(() => {
|
|
257
|
+
proc?.kill();
|
|
258
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('run output is untouched: no redaction tokens appear anywhere', async () => {
|
|
262
|
+
const createRes = await fetch(`${base2}/api/runs`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ flow: echoFlow, mode: 'step', input: { headers: { 'x-foo': 'bar' } } }),
|
|
266
|
+
});
|
|
267
|
+
expect(createRes.status).toBe(201);
|
|
268
|
+
const { runId } = await createRes.json();
|
|
269
|
+
|
|
270
|
+
const stepRes = await fetch(`${base2}/api/runs/${runId}/step`, { method: 'POST' });
|
|
271
|
+
expect(stepRes.status).toBe(200);
|
|
272
|
+
|
|
273
|
+
const samplesRes = await fetch(`${base2}/api/samples?nodeId=input`);
|
|
274
|
+
const { samples } = await samplesRes.json();
|
|
275
|
+
const sample = samples.find((s: { runId: string }) => s.runId === runId);
|
|
276
|
+
expect(sample).toBeDefined();
|
|
277
|
+
expect(sample.output.headers['x-foo']).toBe('bar');
|
|
278
|
+
expect(JSON.stringify(sample)).not.toContain('«secret:');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LogLine, NodeExecutionSample, NodeRunState,
|
|
3
|
+
WorkflowDefinition, WorkflowRun,
|
|
4
|
+
} from '../src/engine';
|
|
5
|
+
import { InMemoryTraceSink, startRun, type FlowRun, type NodeRegistry } from '../src/engine';
|
|
6
|
+
import { createDefaultRegistry } from '../src/nodes';
|
|
7
|
+
import { redactSecrets } from './redact';
|
|
8
|
+
import { makeSubflowRunner } from './subflowRunner';
|
|
9
|
+
|
|
10
|
+
/** SSE-shaped events, mirroring ExecutorEvents 1:1. */
|
|
11
|
+
export type RunEvent =
|
|
12
|
+
| { type: 'nodeState'; nodeId: string; state: NodeRunState }
|
|
13
|
+
| { type: 'log'; line: LogLine }
|
|
14
|
+
| {
|
|
15
|
+
type: 'finished';
|
|
16
|
+
run: WorkflowRun;
|
|
17
|
+
/** Present only when this run is an error-handler invocation: the
|
|
18
|
+
* workflowId of the run whose failure fired it. */
|
|
19
|
+
errorHandler?: { firedBy: string };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Listener = (event: RunEvent) => void;
|
|
23
|
+
|
|
24
|
+
interface LiveRun {
|
|
25
|
+
handle: FlowRun;
|
|
26
|
+
buffer: RunEvent[];
|
|
27
|
+
listeners: Set<Listener>;
|
|
28
|
+
finished: boolean;
|
|
29
|
+
cleanupTimer?: ReturnType<typeof setTimeout>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const RETAIN_FINISHED_MS = 10 * 60 * 1000; // 10 minutes
|
|
33
|
+
const MAX_LIVE_RUNS = 100;
|
|
34
|
+
|
|
35
|
+
/** Manages live runs: buffers engine events per run and fans them out to SSE subscribers. */
|
|
36
|
+
export class RunRegistry {
|
|
37
|
+
private readonly registry: NodeRegistry;
|
|
38
|
+
private readonly runs = new Map<string, LiveRun>();
|
|
39
|
+
readonly sink = new InMemoryTraceSink();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param loadFlow Resolves a workflow by id for Subflow nodes (server hosts
|
|
43
|
+
* this via flowStore.load). Absent → Subflow nodes fail with a clear message.
|
|
44
|
+
* @param registry Execution registry to run against. Defaults to the
|
|
45
|
+
* built-in node set; project mode passes one preloaded with consumer nodes.
|
|
46
|
+
* @param errorOperation Op id (resolved via `loadFlow`, same as Subflow
|
|
47
|
+
* nodes) to run whenever a run finishes with status 'failed'. Best-effort:
|
|
48
|
+
* never throws, never affects the failed run. Absent → disabled.
|
|
49
|
+
*/
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly loadFlow?: (id: string) => WorkflowDefinition | undefined,
|
|
52
|
+
registry: NodeRegistry = createDefaultRegistry(),
|
|
53
|
+
private readonly errorOperation?: string,
|
|
54
|
+
) {
|
|
55
|
+
this.registry = registry;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Number of registered node types (for boot logging). */
|
|
59
|
+
get nodeCount(): number {
|
|
60
|
+
return this.registry.list().length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The registry runs execute against — used to verify artifact node hashes. */
|
|
64
|
+
get executionRegistry(): NodeRegistry {
|
|
65
|
+
return this.registry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Samples recorded for a node across all runs on this registry (newest first, capped). */
|
|
69
|
+
samplesFor(nodeId: string): NodeExecutionSample[] {
|
|
70
|
+
return this.sink.samplesFor(nodeId).slice(0, 100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Create a run; wires engine events into a per-run buffer + attached subscribers. */
|
|
74
|
+
create(
|
|
75
|
+
flow: WorkflowDefinition,
|
|
76
|
+
opts: {
|
|
77
|
+
secrets: Record<string, string>;
|
|
78
|
+
vars: Record<string, string>;
|
|
79
|
+
environment: string;
|
|
80
|
+
safeMode: boolean;
|
|
81
|
+
pins?: Record<string, unknown>;
|
|
82
|
+
input?: Record<string, unknown>;
|
|
83
|
+
/** When true, runs in Mock mode: mocked nodes return their canned
|
|
84
|
+
* output, unmocked infra nodes fail loud. See `StartRunOptions.mockRun`. */
|
|
85
|
+
mockRun?: boolean;
|
|
86
|
+
/** nodeId -> canned output, consulted only when `mockRun` is true. */
|
|
87
|
+
mocks?: Record<string, unknown>;
|
|
88
|
+
/** Internal: marks this run as an error-workflow invocation so it never
|
|
89
|
+
* itself triggers `errorOperation` (recursion guard — a flag, not an id
|
|
90
|
+
* comparison, since an error op can legitimately fail on its own). */
|
|
91
|
+
isErrorHandler?: boolean;
|
|
92
|
+
/** Internal: the workflowId of the run that failed and fired this
|
|
93
|
+
* error-handler run — stamped onto this run's `finished` event as
|
|
94
|
+
* `errorHandler: { firedBy }`. Only meaningful alongside isErrorHandler. */
|
|
95
|
+
errorHandlerFiredBy?: string;
|
|
96
|
+
},
|
|
97
|
+
): { runId: string; handle: FlowRun } {
|
|
98
|
+
this.evictIfNeeded();
|
|
99
|
+
|
|
100
|
+
// Placeholder entry so the ExecutorEvents closures can reach the live run.
|
|
101
|
+
const entry: LiveRun = { handle: undefined as unknown as FlowRun, buffer: [], listeners: new Set(), finished: false };
|
|
102
|
+
|
|
103
|
+
// Redact secret values from every event before it reaches the buffer
|
|
104
|
+
// (replayed to SSE subscribers) or any listener — internal run state
|
|
105
|
+
// (`entry.handle`, `handle.run.nodeStates`) is never routed through
|
|
106
|
+
// `emit` and stays raw.
|
|
107
|
+
const scrub = <T>(e: T): T => redactSecrets(e, opts.secrets ?? {});
|
|
108
|
+
|
|
109
|
+
const emit = (event: RunEvent): void => {
|
|
110
|
+
const scrubbed = scrub(event);
|
|
111
|
+
entry.buffer.push(scrubbed);
|
|
112
|
+
for (const listener of entry.listeners) listener(scrubbed);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Subflow runner (shared factory — see server/subflowRunner.ts): resolves
|
|
116
|
+
// the child via loadFlow, runs it to completion on the same registry
|
|
117
|
+
// sharing the parent's environment, and forwards its (prefixed) logs into
|
|
118
|
+
// this run's SSE stream via `onLog`.
|
|
119
|
+
const subflowRunner = makeSubflowRunner(
|
|
120
|
+
{
|
|
121
|
+
loadFlow: (id) => this.loadFlow?.(id),
|
|
122
|
+
registry: this.registry,
|
|
123
|
+
secrets: opts.secrets,
|
|
124
|
+
vars: opts.vars,
|
|
125
|
+
environment: opts.environment,
|
|
126
|
+
safeMode: opts.safeMode,
|
|
127
|
+
mockRun: opts.mockRun,
|
|
128
|
+
trace: this.sink,
|
|
129
|
+
onLog: (line) => emit({ type: 'log', line }),
|
|
130
|
+
},
|
|
131
|
+
[flow.id],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handle = startRun({
|
|
135
|
+
flow,
|
|
136
|
+
registry: this.registry,
|
|
137
|
+
secrets: opts.secrets,
|
|
138
|
+
vars: opts.vars,
|
|
139
|
+
environment: opts.environment,
|
|
140
|
+
safeMode: opts.safeMode,
|
|
141
|
+
pins: opts.pins,
|
|
142
|
+
input: opts.input,
|
|
143
|
+
mockRun: opts.mockRun,
|
|
144
|
+
mocks: opts.mocks,
|
|
145
|
+
trace: this.sink,
|
|
146
|
+
subflowRunner,
|
|
147
|
+
events: {
|
|
148
|
+
onNodeStateChange: (nodeId, state) => emit({ type: 'nodeState', nodeId, state }),
|
|
149
|
+
onLog: (line) => emit({ type: 'log', line }),
|
|
150
|
+
onRunFinished: (run) => {
|
|
151
|
+
emit({
|
|
152
|
+
type: 'finished',
|
|
153
|
+
run,
|
|
154
|
+
...(opts.isErrorHandler && opts.errorHandlerFiredBy
|
|
155
|
+
? { errorHandler: { firedBy: opts.errorHandlerFiredBy } }
|
|
156
|
+
: {}),
|
|
157
|
+
});
|
|
158
|
+
entry.finished = true;
|
|
159
|
+
this.scheduleCleanup(run.id);
|
|
160
|
+
if (run.status === 'failed' && !opts.isErrorHandler) this.fireErrorWorkflow(run, flow, opts);
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
entry.handle = handle;
|
|
166
|
+
this.runs.set(handle.run.id, entry);
|
|
167
|
+
return { runId: handle.run.id, handle };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Best-effort: fires `errorOperation` (resolved via the same `loadFlow`
|
|
172
|
+
* callback Subflow nodes use) as a new run when `failedRun` finished with
|
|
173
|
+
* status 'failed'. Never throws and never affects the failed run — any
|
|
174
|
+
* problem (unset config, unknown op id, the error op itself failing to
|
|
175
|
+
* start) is logged and swallowed.
|
|
176
|
+
*/
|
|
177
|
+
private fireErrorWorkflow(
|
|
178
|
+
failedRun: WorkflowRun,
|
|
179
|
+
failedFlow: WorkflowDefinition,
|
|
180
|
+
opts: {
|
|
181
|
+
secrets: Record<string, string>;
|
|
182
|
+
vars: Record<string, string>;
|
|
183
|
+
environment: string;
|
|
184
|
+
safeMode: boolean;
|
|
185
|
+
/** A failed MOCK run's error op must also run mocked — otherwise the
|
|
186
|
+
* most common mock failure (the fail-loud infra boundary) would fire
|
|
187
|
+
* an error op that touches real infrastructure. */
|
|
188
|
+
mockRun?: boolean;
|
|
189
|
+
},
|
|
190
|
+
): void {
|
|
191
|
+
if (!this.errorOperation) return;
|
|
192
|
+
try {
|
|
193
|
+
const errorFlow = this.loadFlow?.(this.errorOperation);
|
|
194
|
+
if (!errorFlow) {
|
|
195
|
+
console.error(`[error-workflow] unknown errorOperation "${this.errorOperation}"`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Report the failure that HALTED the run: an `optional` (fail-soft) node
|
|
199
|
+
// can fail earlier without aborting, so prefer the first failed state
|
|
200
|
+
// whose node is not optional. Fall back to the first failed state only
|
|
201
|
+
// when every failed node is optional.
|
|
202
|
+
const failedEntries = Object.entries(failedRun.nodeStates).filter(([, s]) => s.status === 'failed');
|
|
203
|
+
const failedEntry =
|
|
204
|
+
failedEntries.find(([id]) => !failedFlow.nodes.find((n) => n.id === id)?.optional)
|
|
205
|
+
?? failedEntries[0];
|
|
206
|
+
const failedNodeId = failedEntry?.[0];
|
|
207
|
+
const error = failedEntry?.[1] && 'error' in failedEntry[1] ? failedEntry[1].error : undefined;
|
|
208
|
+
const { handle } = this.create(errorFlow, {
|
|
209
|
+
secrets: opts.secrets,
|
|
210
|
+
vars: opts.vars,
|
|
211
|
+
environment: opts.environment,
|
|
212
|
+
safeMode: opts.safeMode,
|
|
213
|
+
// Mock mode propagates with the ERROR op's own op-level mocks
|
|
214
|
+
// (nodeIds are flow-scoped — the failed run's map doesn't apply).
|
|
215
|
+
mockRun: opts.mockRun,
|
|
216
|
+
mocks: (errorFlow as { mocks?: Record<string, unknown> }).mocks ?? {},
|
|
217
|
+
input: {
|
|
218
|
+
failedRunId: failedRun.id,
|
|
219
|
+
failedWorkflowId: failedRun.workflowId,
|
|
220
|
+
failedNodeId,
|
|
221
|
+
error,
|
|
222
|
+
environment: opts.environment,
|
|
223
|
+
},
|
|
224
|
+
isErrorHandler: true,
|
|
225
|
+
errorHandlerFiredBy: failedRun.workflowId,
|
|
226
|
+
});
|
|
227
|
+
handle.runToEnd().catch((err) => {
|
|
228
|
+
console.error('[error-workflow] error op run failed', err instanceof Error ? err.message : String(err));
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error('[error-workflow] failed to fire error operation', err instanceof Error ? err.message : String(err));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get(runId: string): FlowRun | undefined {
|
|
236
|
+
return this.runs.get(runId)?.handle;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
|
|
240
|
+
subscribe(runId: string, listener: Listener): (() => void) | undefined {
|
|
241
|
+
const entry = this.runs.get(runId);
|
|
242
|
+
if (!entry) return undefined;
|
|
243
|
+
for (const event of entry.buffer) listener(event);
|
|
244
|
+
// A run that already finished has its full buffer replayed above; no live events follow.
|
|
245
|
+
entry.listeners.add(listener);
|
|
246
|
+
return () => entry.listeners.delete(listener);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private scheduleCleanup(runId: string): void {
|
|
250
|
+
const entry = this.runs.get(runId);
|
|
251
|
+
if (!entry) return;
|
|
252
|
+
if (entry.cleanupTimer) clearTimeout(entry.cleanupTimer);
|
|
253
|
+
entry.cleanupTimer = setTimeout(() => this.runs.delete(runId), RETAIN_FINISHED_MS);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Cap live entries: drop the oldest finished run, or the oldest run outright. */
|
|
257
|
+
private evictIfNeeded(): void {
|
|
258
|
+
if (this.runs.size < MAX_LIVE_RUNS) return;
|
|
259
|
+
let victim: string | undefined;
|
|
260
|
+
for (const [id, entry] of this.runs) {
|
|
261
|
+
if (entry.finished) { victim = id; break; }
|
|
262
|
+
}
|
|
263
|
+
if (!victim) victim = this.runs.keys().next().value;
|
|
264
|
+
if (victim) {
|
|
265
|
+
const entry = this.runs.get(victim);
|
|
266
|
+
if (entry?.cleanupTimer) clearTimeout(entry.cleanupTimer);
|
|
267
|
+
this.runs.delete(victim);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|