@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runOutput } from '../src/engine/index.js';
|
|
2
|
+
/** Map a finished operation run to an HTTP response. A flow can have more than
|
|
3
|
+
* one `Response` node (e.g. a success branch and an error branch) — only one
|
|
4
|
+
* of them actually ran. Among the `Response`-type nodes, pick the one whose
|
|
5
|
+
* run state actually succeeded (first such node by array order if somehow
|
|
6
|
+
* more than one did); its output (already `{ status, body }`) wins.
|
|
7
|
+
* Otherwise — no Response node, or none of them succeeded — fall back to
|
|
8
|
+
* 200 + the run's Result output. */
|
|
9
|
+
export function extractResponse(run, flow) {
|
|
10
|
+
const responseNodes = flow.nodes.filter((n) => n.type === 'Response');
|
|
11
|
+
const succeeded = responseNodes.find((n) => run.nodeStates[n.id]?.status === 'succeeded');
|
|
12
|
+
const out = succeeded ? run.nodeStates[succeeded.id]?.output : undefined;
|
|
13
|
+
if (out && typeof out === 'object') {
|
|
14
|
+
const o = out;
|
|
15
|
+
return { status: typeof o.status === 'number' ? o.status : 200, body: o.body };
|
|
16
|
+
}
|
|
17
|
+
// No Response node succeeded. If the run itself failed, a 200 fallback would
|
|
18
|
+
// silently mask the failure (runOutput returns undefined → 200 + null). Surface
|
|
19
|
+
// it as a 500, preferring the first failed node's error string (a stable shape,
|
|
20
|
+
// no stack traces) so the caller sees why.
|
|
21
|
+
if (run.status === 'failed') {
|
|
22
|
+
const nodeError = Object.values(run.nodeStates).find((s) => s?.status === 'failed')?.error;
|
|
23
|
+
return { status: 500, body: nodeError ? { error: 'run failed', detail: nodeError } : { error: 'run failed' } };
|
|
24
|
+
}
|
|
25
|
+
return { status: 200, body: runOutput(run, flow) };
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isMountablePath(path: string | undefined | null): path is string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// A mountable operation `http.path` must be a real sub-path — never bare
|
|
2
|
+
// root ('/' or '') and never missing. Operation routes are registered
|
|
3
|
+
// before the studio SPA catch-all, so an op mounted at '/' would shadow the
|
|
4
|
+
// studio entirely (GET / would return the op's output, not the studio
|
|
5
|
+
// HTML). Roughly `^/[^/].*`.
|
|
6
|
+
export function isMountablePath(path) {
|
|
7
|
+
if (!path)
|
|
8
|
+
return false;
|
|
9
|
+
if (path.length < 2)
|
|
10
|
+
return false; // '/' or ''
|
|
11
|
+
if (path[0] !== '/')
|
|
12
|
+
return false;
|
|
13
|
+
if (path[1] === '/')
|
|
14
|
+
return false; // '//x'
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `relPath` is a safe POSIX-style relative path that, once joined
|
|
3
|
+
* onto `baseDir`, resolves to somewhere *inside* `baseDir`.
|
|
4
|
+
*
|
|
5
|
+
* Rejects:
|
|
6
|
+
* - empty strings
|
|
7
|
+
* - a leading `/` (absolute)
|
|
8
|
+
* - a backslash anywhere (the Windows separator — letting it through would
|
|
9
|
+
* let a segment like `a\..\b` smuggle a traversal past the POSIX-only
|
|
10
|
+
* `'/'.split` checks below, and still resolve outside `baseDir` via
|
|
11
|
+
* `path.join`'s platform-specific behavior)
|
|
12
|
+
* - any `.`/`..`/empty segment (so `a//b`, `./x`, `../x` are all rejected)
|
|
13
|
+
* - anything that, after resolution, doesn't stay under `baseDir` (defense
|
|
14
|
+
* in depth beyond the segment checks above)
|
|
15
|
+
*
|
|
16
|
+
* Shared by `isSafeApiPath` (server/index.ts, POST /operations and future
|
|
17
|
+
* path-taking endpoints) and `isSafeFlowId` (server/agents/runManager.ts) —
|
|
18
|
+
* one robust implementation instead of two ad hoc string checks.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isPathWithin(baseDir: string, relPath: string): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* True when `relPath` is a safe POSIX-style relative path that, once joined
|
|
4
|
+
* onto `baseDir`, resolves to somewhere *inside* `baseDir`.
|
|
5
|
+
*
|
|
6
|
+
* Rejects:
|
|
7
|
+
* - empty strings
|
|
8
|
+
* - a leading `/` (absolute)
|
|
9
|
+
* - a backslash anywhere (the Windows separator — letting it through would
|
|
10
|
+
* let a segment like `a\..\b` smuggle a traversal past the POSIX-only
|
|
11
|
+
* `'/'.split` checks below, and still resolve outside `baseDir` via
|
|
12
|
+
* `path.join`'s platform-specific behavior)
|
|
13
|
+
* - any `.`/`..`/empty segment (so `a//b`, `./x`, `../x` are all rejected)
|
|
14
|
+
* - anything that, after resolution, doesn't stay under `baseDir` (defense
|
|
15
|
+
* in depth beyond the segment checks above)
|
|
16
|
+
*
|
|
17
|
+
* Shared by `isSafeApiPath` (server/index.ts, POST /operations and future
|
|
18
|
+
* path-taking endpoints) and `isSafeFlowId` (server/agents/runManager.ts) —
|
|
19
|
+
* one robust implementation instead of two ad hoc string checks.
|
|
20
|
+
*/
|
|
21
|
+
export function isPathWithin(baseDir, relPath) {
|
|
22
|
+
if (typeof relPath !== 'string' || relPath.length === 0)
|
|
23
|
+
return false;
|
|
24
|
+
if (relPath.includes('\\'))
|
|
25
|
+
return false;
|
|
26
|
+
if (relPath.startsWith('/'))
|
|
27
|
+
return false;
|
|
28
|
+
const segments = relPath.split('/');
|
|
29
|
+
if (segments.some((seg) => seg === '' || seg === '.' || seg === '..'))
|
|
30
|
+
return false;
|
|
31
|
+
const resolvedBase = resolve(baseDir);
|
|
32
|
+
const resolved = resolve(baseDir, relPath);
|
|
33
|
+
const rel = relative(resolvedBase, resolved);
|
|
34
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { NodeRegistry } from '../src/engine/index.js';
|
|
2
|
+
import type { VerifierRegistry } from './auth/verifiers.js';
|
|
3
|
+
export interface EmberflowUserConfig {
|
|
4
|
+
/** Directory holding <flowId>.json flow files (+ <flowId>.scenarios.json sidecars). Relative to the config file. */
|
|
5
|
+
flowsDir?: string;
|
|
6
|
+
/** Register the project's own node types. */
|
|
7
|
+
registerNodes?: (registry: NodeRegistry) => void;
|
|
8
|
+
/** Register custom auth verifiers (e.g. real JWT) alongside the default bearer/apiKey ones. */
|
|
9
|
+
registerVerifiers?: (registry: VerifierRegistry) => void;
|
|
10
|
+
/** Op id to run when any run finishes with status 'failed' (best-effort; never blocks the failed run). */
|
|
11
|
+
errorOperation?: string;
|
|
12
|
+
/** Authoritative signal for agents/tooling about which language this project's
|
|
13
|
+
* APIs/nodes are authored in. When absent, it's inferred from the config file's
|
|
14
|
+
* own extension (`emberflow.config.ts` → 'typescript', else 'javascript'). */
|
|
15
|
+
language?: 'javascript' | 'typescript';
|
|
16
|
+
}
|
|
17
|
+
/** Identity helper so consumer configs get typing: `export default defineConfig({...})`. */
|
|
18
|
+
export declare function defineConfig(config: EmberflowUserConfig): EmberflowUserConfig;
|
|
19
|
+
export interface ProjectConfig {
|
|
20
|
+
root: string;
|
|
21
|
+
flowsDir: string;
|
|
22
|
+
language: 'javascript' | 'typescript';
|
|
23
|
+
/** Present only when the config's `language` field was EXPLICIT and
|
|
24
|
+
* disagrees with the config file's own extension (e.g. `language:
|
|
25
|
+
* 'javascript'` in an `emberflow.config.ts`). Inference makes language and
|
|
26
|
+
* extension agree by construction, so this is the only detectable drift —
|
|
27
|
+
* see src/engine/diagnostics.ts's `language-drift` check, which callers
|
|
28
|
+
* (server/index.ts's diagnostics route, `doctor`) feed this into. */
|
|
29
|
+
languageDrift?: {
|
|
30
|
+
projectLanguage: 'javascript' | 'typescript';
|
|
31
|
+
configPathExtension: string;
|
|
32
|
+
};
|
|
33
|
+
registerNodes?: (registry: NodeRegistry) => void;
|
|
34
|
+
registerVerifiers?: (registry: VerifierRegistry) => void;
|
|
35
|
+
errorOperation?: string;
|
|
36
|
+
}
|
|
37
|
+
/** The resolved emberflow.config.* path for a project dir, or undefined. */
|
|
38
|
+
export declare function configPathFor(dir: string): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Loads a project's config. `fresh: true` cache-busts the dynamic import so an
|
|
41
|
+
* edited config re-imports (the module loader caches by URL) — used by the
|
|
42
|
+
* in-process node hot-reload so editing registerNodes takes effect without a
|
|
43
|
+
* process restart (which would kill in-flight agent runs).
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadProjectConfig(dir: string, opts?: {
|
|
46
|
+
fresh?: boolean;
|
|
47
|
+
}): Promise<ProjectConfig | null>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Project mode: a consumer repo drops an emberflow.config file at its root
|
|
6
|
+
* and Emberflow serves THEIR flows and nodes. This loader finds and imports
|
|
7
|
+
* that config. The runner runs under tsx, so dynamic import handles .ts
|
|
8
|
+
* configs as well as .js/.mjs.
|
|
9
|
+
*/
|
|
10
|
+
const CONFIG_BASENAMES = ['emberflow.config.mjs', 'emberflow.config.js', 'emberflow.config.ts'];
|
|
11
|
+
/** Identity helper so consumer configs get typing: `export default defineConfig({...})`. */
|
|
12
|
+
export function defineConfig(config) {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
/** The resolved emberflow.config.* path for a project dir, or undefined. */
|
|
16
|
+
export function configPathFor(dir) {
|
|
17
|
+
const root = resolve(dir);
|
|
18
|
+
return CONFIG_BASENAMES.map((b) => join(root, b)).find(existsSync);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Loads a project's config. `fresh: true` cache-busts the dynamic import so an
|
|
22
|
+
* edited config re-imports (the module loader caches by URL) — used by the
|
|
23
|
+
* in-process node hot-reload so editing registerNodes takes effect without a
|
|
24
|
+
* process restart (which would kill in-flight agent runs).
|
|
25
|
+
*/
|
|
26
|
+
export async function loadProjectConfig(dir, opts = {}) {
|
|
27
|
+
const root = resolve(dir);
|
|
28
|
+
const file = CONFIG_BASENAMES.map((b) => join(root, b)).find(existsSync);
|
|
29
|
+
if (!file)
|
|
30
|
+
return null;
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
const url = pathToFileURL(file).href + (opts.fresh ? `?t=${Date.now()}` : '');
|
|
34
|
+
const mod = (await import(url));
|
|
35
|
+
raw = mod.default ?? {};
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new Error(`Failed to load ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
const flowsDir = raw.flowsDir
|
|
41
|
+
? isAbsolute(raw.flowsDir) ? raw.flowsDir : join(root, raw.flowsDir)
|
|
42
|
+
: join(root, 'emberflow', 'flows');
|
|
43
|
+
// Explicit `language` wins; otherwise infer from the config file's own
|
|
44
|
+
// extension — a .ts config implies a TypeScript project, everything else JS.
|
|
45
|
+
const inferredLanguage = file.endsWith('.ts') ? 'typescript' : 'javascript';
|
|
46
|
+
const language = raw.language ?? inferredLanguage;
|
|
47
|
+
// Drift is only detectable when the field is EXPLICIT: inferred language
|
|
48
|
+
// agrees with the extension by construction, so there's nothing to flag.
|
|
49
|
+
const languageDrift = raw.language && raw.language !== inferredLanguage
|
|
50
|
+
? { projectLanguage: raw.language, configPathExtension: extname(file) }
|
|
51
|
+
: undefined;
|
|
52
|
+
return {
|
|
53
|
+
root,
|
|
54
|
+
flowsDir,
|
|
55
|
+
language,
|
|
56
|
+
...(languageDrift ? { languageDrift } : {}),
|
|
57
|
+
...(raw.registerNodes ? { registerNodes: raw.registerNodes } : {}),
|
|
58
|
+
...(raw.registerVerifiers ? { registerVerifiers: raw.registerVerifiers } : {}),
|
|
59
|
+
...(raw.errorOperation ? { errorOperation: raw.errorOperation } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlowStore } from './flowStore.js';
|
|
2
|
+
import { ApiStore } from './apiStore.js';
|
|
3
|
+
import type { NodeRegistry } from '../src/engine/index.js';
|
|
4
|
+
import type { ProjectConfig } from './projectConfig.js';
|
|
5
|
+
/**
|
|
6
|
+
* Project-mode wiring, kept Express-free so tests exercise exactly what the
|
|
7
|
+
* runner boots with. Two registries because validation and execution are
|
|
8
|
+
* separate instances in the runner — the consumer's nodes must exist in both.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Boot-time guard: an explicit EMBERFLOW_PROJECT env var must fail loudly
|
|
12
|
+
* when no config was found there, mirroring the environments-file stance.
|
|
13
|
+
* Implicit cwd detection stays lenient (silent fallback to default mode).
|
|
14
|
+
*/
|
|
15
|
+
export declare function requireProjectWhenExplicit(project: ProjectConfig | null, explicitPath: string | undefined, resolvedDir: string): ProjectConfig | null;
|
|
16
|
+
export declare function buildFlowStore(project: ProjectConfig | null): FlowStore;
|
|
17
|
+
/**
|
|
18
|
+
* The `apis/` tree replaces the flat `flows/` layout. Runs the one-time
|
|
19
|
+
* migration before constructing the store so a legacy project boots straight
|
|
20
|
+
* into the new layout.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildApiStore(project: ProjectConfig | null): ApiStore;
|
|
23
|
+
export declare function buildRegistries(project: ProjectConfig | null): {
|
|
24
|
+
validation: NodeRegistry;
|
|
25
|
+
execution: NodeRegistry;
|
|
26
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { FlowStore } from './flowStore.js';
|
|
3
|
+
import { ApiStore } from './apiStore.js';
|
|
4
|
+
import { migrateFlowsToApis } from './migrateFlows.js';
|
|
5
|
+
import { createDefaultRegistry } from '../src/nodes/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Project-mode wiring, kept Express-free so tests exercise exactly what the
|
|
8
|
+
* runner boots with. Two registries because validation and execution are
|
|
9
|
+
* separate instances in the runner — the consumer's nodes must exist in both.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Boot-time guard: an explicit EMBERFLOW_PROJECT env var must fail loudly
|
|
13
|
+
* when no config was found there, mirroring the environments-file stance.
|
|
14
|
+
* Implicit cwd detection stays lenient (silent fallback to default mode).
|
|
15
|
+
*/
|
|
16
|
+
export function requireProjectWhenExplicit(project, explicitPath, resolvedDir) {
|
|
17
|
+
if (explicitPath !== undefined && project === null) {
|
|
18
|
+
throw new Error(`EMBERFLOW_PROJECT points at ${resolvedDir} but no emberflow.config.(mjs|js|ts) was found there`);
|
|
19
|
+
}
|
|
20
|
+
return project;
|
|
21
|
+
}
|
|
22
|
+
export function buildFlowStore(project) {
|
|
23
|
+
return project ? new FlowStore(project.flowsDir, { scenarioSidecars: true }) : new FlowStore();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The `apis/` tree replaces the flat `flows/` layout. Runs the one-time
|
|
27
|
+
* migration before constructing the store so a legacy project boots straight
|
|
28
|
+
* into the new layout.
|
|
29
|
+
*/
|
|
30
|
+
export function buildApiStore(project) {
|
|
31
|
+
const flowsDir = project ? project.flowsDir : resolve(process.cwd(), 'workflows', 'flows');
|
|
32
|
+
const apisDir = join(dirname(flowsDir), 'apis');
|
|
33
|
+
migrateFlowsToApis(flowsDir, apisDir);
|
|
34
|
+
return new ApiStore(apisDir);
|
|
35
|
+
}
|
|
36
|
+
export function buildRegistries(project) {
|
|
37
|
+
const validation = createDefaultRegistry();
|
|
38
|
+
const execution = createDefaultRegistry();
|
|
39
|
+
if (project?.registerNodes) {
|
|
40
|
+
project.registerNodes(validation);
|
|
41
|
+
project.registerNodes(execution);
|
|
42
|
+
}
|
|
43
|
+
return { validation, execution };
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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 declare function redactSecrets<T>(payload: T, secrets: Record<string, string>): T;
|
|
@@ -0,0 +1,37 @@
|
|
|
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(payload, secrets) {
|
|
7
|
+
const replacements = [];
|
|
8
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
9
|
+
if (value.length < 6)
|
|
10
|
+
continue;
|
|
11
|
+
const token = `«secret:${key}»`;
|
|
12
|
+
const encoded = encodeURIComponent(value);
|
|
13
|
+
const base64 = Buffer.from(value).toString('base64');
|
|
14
|
+
replacements.push([value, token]);
|
|
15
|
+
if (encoded !== value)
|
|
16
|
+
replacements.push([encoded, token]);
|
|
17
|
+
if (base64 !== value && base64 !== encoded)
|
|
18
|
+
replacements.push([base64, token]);
|
|
19
|
+
}
|
|
20
|
+
// Longest value first, so an encoded superstring is replaced before the raw substring it contains.
|
|
21
|
+
replacements.sort((a, b) => b[0].length - a[0].length);
|
|
22
|
+
const redactString = (input) => replacements.reduce((acc, [value, token]) => acc.split(value).join(token), input);
|
|
23
|
+
const walk = (node) => {
|
|
24
|
+
if (typeof node === 'string')
|
|
25
|
+
return redactString(node);
|
|
26
|
+
if (Array.isArray(node))
|
|
27
|
+
return node.map(walk);
|
|
28
|
+
if (node !== null && typeof node === 'object') {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [k, v] of Object.entries(node))
|
|
31
|
+
out[k] = walk(v);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
return node;
|
|
35
|
+
};
|
|
36
|
+
return walk(payload);
|
|
37
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { LogLine, NodeExecutionSample, NodeRunState, WorkflowDefinition, WorkflowRun } from '../src/engine/index.js';
|
|
2
|
+
import { InMemoryTraceSink, type FlowRun, type NodeRegistry } from '../src/engine/index.js';
|
|
3
|
+
/** SSE-shaped events, mirroring ExecutorEvents 1:1. */
|
|
4
|
+
export type RunEvent = {
|
|
5
|
+
type: 'nodeState';
|
|
6
|
+
nodeId: string;
|
|
7
|
+
state: NodeRunState;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'log';
|
|
10
|
+
line: LogLine;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'finished';
|
|
13
|
+
run: WorkflowRun;
|
|
14
|
+
/** Present only when this run is an error-handler invocation: the
|
|
15
|
+
* workflowId of the run whose failure fired it. */
|
|
16
|
+
errorHandler?: {
|
|
17
|
+
firedBy: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
type Listener = (event: RunEvent) => void;
|
|
21
|
+
/** Manages live runs: buffers engine events per run and fans them out to SSE subscribers. */
|
|
22
|
+
export declare class RunRegistry {
|
|
23
|
+
private readonly loadFlow?;
|
|
24
|
+
private readonly errorOperation?;
|
|
25
|
+
private readonly registry;
|
|
26
|
+
private readonly runs;
|
|
27
|
+
readonly sink: InMemoryTraceSink;
|
|
28
|
+
/**
|
|
29
|
+
* @param loadFlow Resolves a workflow by id for Subflow nodes (server hosts
|
|
30
|
+
* this via flowStore.load). Absent → Subflow nodes fail with a clear message.
|
|
31
|
+
* @param registry Execution registry to run against. Defaults to the
|
|
32
|
+
* built-in node set; project mode passes one preloaded with consumer nodes.
|
|
33
|
+
* @param errorOperation Op id (resolved via `loadFlow`, same as Subflow
|
|
34
|
+
* nodes) to run whenever a run finishes with status 'failed'. Best-effort:
|
|
35
|
+
* never throws, never affects the failed run. Absent → disabled.
|
|
36
|
+
*/
|
|
37
|
+
constructor(loadFlow?: ((id: string) => WorkflowDefinition | undefined) | undefined, registry?: NodeRegistry, errorOperation?: string | undefined);
|
|
38
|
+
/** Number of registered node types (for boot logging). */
|
|
39
|
+
get nodeCount(): number;
|
|
40
|
+
/** The registry runs execute against — used to verify artifact node hashes. */
|
|
41
|
+
get executionRegistry(): NodeRegistry;
|
|
42
|
+
/** Samples recorded for a node across all runs on this registry (newest first, capped). */
|
|
43
|
+
samplesFor(nodeId: string): NodeExecutionSample[];
|
|
44
|
+
/** Create a run; wires engine events into a per-run buffer + attached subscribers. */
|
|
45
|
+
create(flow: WorkflowDefinition, opts: {
|
|
46
|
+
secrets: Record<string, string>;
|
|
47
|
+
vars: Record<string, string>;
|
|
48
|
+
environment: string;
|
|
49
|
+
safeMode: boolean;
|
|
50
|
+
pins?: Record<string, unknown>;
|
|
51
|
+
input?: Record<string, unknown>;
|
|
52
|
+
/** When true, runs in Mock mode: mocked nodes return their canned
|
|
53
|
+
* output, unmocked infra nodes fail loud. See `StartRunOptions.mockRun`. */
|
|
54
|
+
mockRun?: boolean;
|
|
55
|
+
/** nodeId -> canned output, consulted only when `mockRun` is true. */
|
|
56
|
+
mocks?: Record<string, unknown>;
|
|
57
|
+
/** Internal: marks this run as an error-workflow invocation so it never
|
|
58
|
+
* itself triggers `errorOperation` (recursion guard — a flag, not an id
|
|
59
|
+
* comparison, since an error op can legitimately fail on its own). */
|
|
60
|
+
isErrorHandler?: boolean;
|
|
61
|
+
/** Internal: the workflowId of the run that failed and fired this
|
|
62
|
+
* error-handler run — stamped onto this run's `finished` event as
|
|
63
|
+
* `errorHandler: { firedBy }`. Only meaningful alongside isErrorHandler. */
|
|
64
|
+
errorHandlerFiredBy?: string;
|
|
65
|
+
}): {
|
|
66
|
+
runId: string;
|
|
67
|
+
handle: FlowRun;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Best-effort: fires `errorOperation` (resolved via the same `loadFlow`
|
|
71
|
+
* callback Subflow nodes use) as a new run when `failedRun` finished with
|
|
72
|
+
* status 'failed'. Never throws and never affects the failed run — any
|
|
73
|
+
* problem (unset config, unknown op id, the error op itself failing to
|
|
74
|
+
* start) is logged and swallowed.
|
|
75
|
+
*/
|
|
76
|
+
private fireErrorWorkflow;
|
|
77
|
+
get(runId: string): FlowRun | undefined;
|
|
78
|
+
/** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
|
|
79
|
+
subscribe(runId: string, listener: Listener): (() => void) | undefined;
|
|
80
|
+
private scheduleCleanup;
|
|
81
|
+
/** Cap live entries: drop the oldest finished run, or the oldest run outright. */
|
|
82
|
+
private evictIfNeeded;
|
|
83
|
+
}
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { InMemoryTraceSink, startRun } from '../src/engine/index.js';
|
|
2
|
+
import { createDefaultRegistry } from '../src/nodes/index.js';
|
|
3
|
+
import { redactSecrets } from './redact.js';
|
|
4
|
+
import { makeSubflowRunner } from './subflowRunner.js';
|
|
5
|
+
const RETAIN_FINISHED_MS = 10 * 60 * 1000; // 10 minutes
|
|
6
|
+
const MAX_LIVE_RUNS = 100;
|
|
7
|
+
/** Manages live runs: buffers engine events per run and fans them out to SSE subscribers. */
|
|
8
|
+
export class RunRegistry {
|
|
9
|
+
loadFlow;
|
|
10
|
+
errorOperation;
|
|
11
|
+
registry;
|
|
12
|
+
runs = new Map();
|
|
13
|
+
sink = new InMemoryTraceSink();
|
|
14
|
+
/**
|
|
15
|
+
* @param loadFlow Resolves a workflow by id for Subflow nodes (server hosts
|
|
16
|
+
* this via flowStore.load). Absent → Subflow nodes fail with a clear message.
|
|
17
|
+
* @param registry Execution registry to run against. Defaults to the
|
|
18
|
+
* built-in node set; project mode passes one preloaded with consumer nodes.
|
|
19
|
+
* @param errorOperation Op id (resolved via `loadFlow`, same as Subflow
|
|
20
|
+
* nodes) to run whenever a run finishes with status 'failed'. Best-effort:
|
|
21
|
+
* never throws, never affects the failed run. Absent → disabled.
|
|
22
|
+
*/
|
|
23
|
+
constructor(loadFlow, registry = createDefaultRegistry(), errorOperation) {
|
|
24
|
+
this.loadFlow = loadFlow;
|
|
25
|
+
this.errorOperation = errorOperation;
|
|
26
|
+
this.registry = registry;
|
|
27
|
+
}
|
|
28
|
+
/** Number of registered node types (for boot logging). */
|
|
29
|
+
get nodeCount() {
|
|
30
|
+
return this.registry.list().length;
|
|
31
|
+
}
|
|
32
|
+
/** The registry runs execute against — used to verify artifact node hashes. */
|
|
33
|
+
get executionRegistry() {
|
|
34
|
+
return this.registry;
|
|
35
|
+
}
|
|
36
|
+
/** Samples recorded for a node across all runs on this registry (newest first, capped). */
|
|
37
|
+
samplesFor(nodeId) {
|
|
38
|
+
return this.sink.samplesFor(nodeId).slice(0, 100);
|
|
39
|
+
}
|
|
40
|
+
/** Create a run; wires engine events into a per-run buffer + attached subscribers. */
|
|
41
|
+
create(flow, opts) {
|
|
42
|
+
this.evictIfNeeded();
|
|
43
|
+
// Placeholder entry so the ExecutorEvents closures can reach the live run.
|
|
44
|
+
const entry = { handle: undefined, buffer: [], listeners: new Set(), finished: false };
|
|
45
|
+
// Redact secret values from every event before it reaches the buffer
|
|
46
|
+
// (replayed to SSE subscribers) or any listener — internal run state
|
|
47
|
+
// (`entry.handle`, `handle.run.nodeStates`) is never routed through
|
|
48
|
+
// `emit` and stays raw.
|
|
49
|
+
const scrub = (e) => redactSecrets(e, opts.secrets ?? {});
|
|
50
|
+
const emit = (event) => {
|
|
51
|
+
const scrubbed = scrub(event);
|
|
52
|
+
entry.buffer.push(scrubbed);
|
|
53
|
+
for (const listener of entry.listeners)
|
|
54
|
+
listener(scrubbed);
|
|
55
|
+
};
|
|
56
|
+
// Subflow runner (shared factory — see server/subflowRunner.ts): resolves
|
|
57
|
+
// the child via loadFlow, runs it to completion on the same registry
|
|
58
|
+
// sharing the parent's environment, and forwards its (prefixed) logs into
|
|
59
|
+
// this run's SSE stream via `onLog`.
|
|
60
|
+
const subflowRunner = makeSubflowRunner({
|
|
61
|
+
loadFlow: (id) => this.loadFlow?.(id),
|
|
62
|
+
registry: this.registry,
|
|
63
|
+
secrets: opts.secrets,
|
|
64
|
+
vars: opts.vars,
|
|
65
|
+
environment: opts.environment,
|
|
66
|
+
safeMode: opts.safeMode,
|
|
67
|
+
mockRun: opts.mockRun,
|
|
68
|
+
trace: this.sink,
|
|
69
|
+
onLog: (line) => emit({ type: 'log', line }),
|
|
70
|
+
}, [flow.id]);
|
|
71
|
+
const handle = startRun({
|
|
72
|
+
flow,
|
|
73
|
+
registry: this.registry,
|
|
74
|
+
secrets: opts.secrets,
|
|
75
|
+
vars: opts.vars,
|
|
76
|
+
environment: opts.environment,
|
|
77
|
+
safeMode: opts.safeMode,
|
|
78
|
+
pins: opts.pins,
|
|
79
|
+
input: opts.input,
|
|
80
|
+
mockRun: opts.mockRun,
|
|
81
|
+
mocks: opts.mocks,
|
|
82
|
+
trace: this.sink,
|
|
83
|
+
subflowRunner,
|
|
84
|
+
events: {
|
|
85
|
+
onNodeStateChange: (nodeId, state) => emit({ type: 'nodeState', nodeId, state }),
|
|
86
|
+
onLog: (line) => emit({ type: 'log', line }),
|
|
87
|
+
onRunFinished: (run) => {
|
|
88
|
+
emit({
|
|
89
|
+
type: 'finished',
|
|
90
|
+
run,
|
|
91
|
+
...(opts.isErrorHandler && opts.errorHandlerFiredBy
|
|
92
|
+
? { errorHandler: { firedBy: opts.errorHandlerFiredBy } }
|
|
93
|
+
: {}),
|
|
94
|
+
});
|
|
95
|
+
entry.finished = true;
|
|
96
|
+
this.scheduleCleanup(run.id);
|
|
97
|
+
if (run.status === 'failed' && !opts.isErrorHandler)
|
|
98
|
+
this.fireErrorWorkflow(run, flow, opts);
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
entry.handle = handle;
|
|
103
|
+
this.runs.set(handle.run.id, entry);
|
|
104
|
+
return { runId: handle.run.id, handle };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort: fires `errorOperation` (resolved via the same `loadFlow`
|
|
108
|
+
* callback Subflow nodes use) as a new run when `failedRun` finished with
|
|
109
|
+
* status 'failed'. Never throws and never affects the failed run — any
|
|
110
|
+
* problem (unset config, unknown op id, the error op itself failing to
|
|
111
|
+
* start) is logged and swallowed.
|
|
112
|
+
*/
|
|
113
|
+
fireErrorWorkflow(failedRun, failedFlow, opts) {
|
|
114
|
+
if (!this.errorOperation)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
const errorFlow = this.loadFlow?.(this.errorOperation);
|
|
118
|
+
if (!errorFlow) {
|
|
119
|
+
console.error(`[error-workflow] unknown errorOperation "${this.errorOperation}"`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Report the failure that HALTED the run: an `optional` (fail-soft) node
|
|
123
|
+
// can fail earlier without aborting, so prefer the first failed state
|
|
124
|
+
// whose node is not optional. Fall back to the first failed state only
|
|
125
|
+
// when every failed node is optional.
|
|
126
|
+
const failedEntries = Object.entries(failedRun.nodeStates).filter(([, s]) => s.status === 'failed');
|
|
127
|
+
const failedEntry = failedEntries.find(([id]) => !failedFlow.nodes.find((n) => n.id === id)?.optional)
|
|
128
|
+
?? failedEntries[0];
|
|
129
|
+
const failedNodeId = failedEntry?.[0];
|
|
130
|
+
const error = failedEntry?.[1] && 'error' in failedEntry[1] ? failedEntry[1].error : undefined;
|
|
131
|
+
const { handle } = this.create(errorFlow, {
|
|
132
|
+
secrets: opts.secrets,
|
|
133
|
+
vars: opts.vars,
|
|
134
|
+
environment: opts.environment,
|
|
135
|
+
safeMode: opts.safeMode,
|
|
136
|
+
// Mock mode propagates with the ERROR op's own op-level mocks
|
|
137
|
+
// (nodeIds are flow-scoped — the failed run's map doesn't apply).
|
|
138
|
+
mockRun: opts.mockRun,
|
|
139
|
+
mocks: errorFlow.mocks ?? {},
|
|
140
|
+
input: {
|
|
141
|
+
failedRunId: failedRun.id,
|
|
142
|
+
failedWorkflowId: failedRun.workflowId,
|
|
143
|
+
failedNodeId,
|
|
144
|
+
error,
|
|
145
|
+
environment: opts.environment,
|
|
146
|
+
},
|
|
147
|
+
isErrorHandler: true,
|
|
148
|
+
errorHandlerFiredBy: failedRun.workflowId,
|
|
149
|
+
});
|
|
150
|
+
handle.runToEnd().catch((err) => {
|
|
151
|
+
console.error('[error-workflow] error op run failed', err instanceof Error ? err.message : String(err));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
console.error('[error-workflow] failed to fire error operation', err instanceof Error ? err.message : String(err));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
get(runId) {
|
|
159
|
+
return this.runs.get(runId)?.handle;
|
|
160
|
+
}
|
|
161
|
+
/** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
|
|
162
|
+
subscribe(runId, listener) {
|
|
163
|
+
const entry = this.runs.get(runId);
|
|
164
|
+
if (!entry)
|
|
165
|
+
return undefined;
|
|
166
|
+
for (const event of entry.buffer)
|
|
167
|
+
listener(event);
|
|
168
|
+
// A run that already finished has its full buffer replayed above; no live events follow.
|
|
169
|
+
entry.listeners.add(listener);
|
|
170
|
+
return () => entry.listeners.delete(listener);
|
|
171
|
+
}
|
|
172
|
+
scheduleCleanup(runId) {
|
|
173
|
+
const entry = this.runs.get(runId);
|
|
174
|
+
if (!entry)
|
|
175
|
+
return;
|
|
176
|
+
if (entry.cleanupTimer)
|
|
177
|
+
clearTimeout(entry.cleanupTimer);
|
|
178
|
+
entry.cleanupTimer = setTimeout(() => this.runs.delete(runId), RETAIN_FINISHED_MS);
|
|
179
|
+
}
|
|
180
|
+
/** Cap live entries: drop the oldest finished run, or the oldest run outright. */
|
|
181
|
+
evictIfNeeded() {
|
|
182
|
+
if (this.runs.size < MAX_LIVE_RUNS)
|
|
183
|
+
return;
|
|
184
|
+
let victim;
|
|
185
|
+
for (const [id, entry] of this.runs) {
|
|
186
|
+
if (entry.finished) {
|
|
187
|
+
victim = id;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!victim)
|
|
192
|
+
victim = this.runs.keys().next().value;
|
|
193
|
+
if (victim) {
|
|
194
|
+
const entry = this.runs.get(victim);
|
|
195
|
+
if (entry?.cleanupTimer)
|
|
196
|
+
clearTimeout(entry.cleanupTimer);
|
|
197
|
+
this.runs.delete(victim);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RequestHandler, Router } from 'express';
|
|
2
|
+
/** A single routed operation's method + path. */
|
|
3
|
+
export interface RegisteredOp {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The seam between the runner and whatever HTTP runtime serves it. Express is
|
|
9
|
+
* the only implementation today; Deno/Bun adapters can implement this same
|
|
10
|
+
* interface later without touching the runner.
|
|
11
|
+
*/
|
|
12
|
+
export interface RuntimeAdapter {
|
|
13
|
+
/** Register a routed operation's handler at `{method, path}`. Throws on a
|
|
14
|
+
* duplicate method+path registration. */
|
|
15
|
+
registerOperation(op: RegisteredOp, handler: RequestHandler): void;
|
|
16
|
+
/** Mount a sub-router at a base path (used for `/api` + the studio). */
|
|
17
|
+
mountRouter(basePath: string, router: Router): void;
|
|
18
|
+
/** Start listening; resolves once the server is up. */
|
|
19
|
+
listen(port: number, host: string): Promise<void>;
|
|
20
|
+
/** Stop the server; resolves once fully closed. */
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Express, type RequestHandler, type Router } from 'express';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import type { RegisteredOp, RuntimeAdapter } from './RuntimeAdapter.js';
|
|
4
|
+
/** Express implementation of the `RuntimeAdapter` seam. Keep it thin — this is
|
|
5
|
+
* the seam other runtimes (Deno/Bun) implement later. */
|
|
6
|
+
export declare class ExpressAdapter implements RuntimeAdapter {
|
|
7
|
+
private readonly _app;
|
|
8
|
+
private server?;
|
|
9
|
+
private readonly seen;
|
|
10
|
+
constructor(app?: Express);
|
|
11
|
+
get app(): Express;
|
|
12
|
+
/** The bound address once `listen()` resolves (useful in tests to find an
|
|
13
|
+
* ephemeral port). `null` before listening / after close. */
|
|
14
|
+
get address(): AddressInfo | null;
|
|
15
|
+
registerOperation(op: RegisteredOp, handler: RequestHandler): void;
|
|
16
|
+
mountRouter(basePath: string, router: Router): void;
|
|
17
|
+
listen(port: number, host: string): Promise<void>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|