@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,259 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { changedFiles, diffSince, isGitRepo, revert as gitRevert, snapshot } from './gitScope.js';
|
|
3
|
+
import { buildPrompt } from './prompt.js';
|
|
4
|
+
import { spawnCodex } from './codexAdapter.js';
|
|
5
|
+
import { spawnClaude } from './claudeAdapter.js';
|
|
6
|
+
import { isPathWithin } from '../pathSafety.js';
|
|
7
|
+
/** Thrown by `start()`; `status` maps directly onto the HTTP status the endpoint should return. */
|
|
8
|
+
export class AgentStartError extends Error {
|
|
9
|
+
status;
|
|
10
|
+
constructor(message, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.name = 'AgentStartError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Max number of terminal (done/error) runs kept around for diff/revert/replay before eviction. */
|
|
17
|
+
const MAX_TERMINAL_RUNS = 20;
|
|
18
|
+
/**
|
|
19
|
+
* Rejects anything that isn't a safe flow id. Operation ids are path-style
|
|
20
|
+
* (e.g. `"billing/charge"`, the on-disk relative path), so a slash-separated
|
|
21
|
+
* id is legitimate and must be allowed — only traversal (`..` segments),
|
|
22
|
+
* absolute ids, backslashes, and empty segments are rejected, and the file it
|
|
23
|
+
* resolves to must stay inside `apisDir`. Defense in depth — the agent
|
|
24
|
+
* sandbox already confines actual writes — but it keeps a bogus flowId like
|
|
25
|
+
* `"../../etc/passwd"` from ever becoming an existence-probe against paths
|
|
26
|
+
* outside apisDir.
|
|
27
|
+
*/
|
|
28
|
+
function isSafeFlowId(flowId, apisDir) {
|
|
29
|
+
if (!flowId)
|
|
30
|
+
return false;
|
|
31
|
+
return isPathWithin(apisDir, `${flowId}.json`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves a `new-operation` intent's user-supplied `location` (a folder path
|
|
35
|
+
* under `apisDir`, e.g. `"billing"` or `"billing/charges"`) to the directory
|
|
36
|
+
* the new operation file should be created in. An empty string or `"default"`
|
|
37
|
+
* both mean "the default API".
|
|
38
|
+
*/
|
|
39
|
+
function resolveLocationDir(location) {
|
|
40
|
+
const trimmed = location.trim();
|
|
41
|
+
return trimmed === '' ? 'default' : trimmed;
|
|
42
|
+
}
|
|
43
|
+
/** Same defense-in-depth as `isSafeFlowId`, but for a target *directory* rather than a flow file. */
|
|
44
|
+
function isSafeLocationDir(dir, apisDir) {
|
|
45
|
+
return isPathWithin(apisDir, dir);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Owns in-flight coding-agent runs for a single project: spawns the adapter,
|
|
49
|
+
* buffers its events for SSE replay + live subscribers, and exposes
|
|
50
|
+
* git-scoped diff/revert against the snapshot taken at run start. One active
|
|
51
|
+
* run per project at a time.
|
|
52
|
+
*/
|
|
53
|
+
export class AgentRunManager {
|
|
54
|
+
projectDir;
|
|
55
|
+
apisDir;
|
|
56
|
+
pathOf;
|
|
57
|
+
availableNodes;
|
|
58
|
+
projectLanguage;
|
|
59
|
+
infrastructure;
|
|
60
|
+
runs = new Map();
|
|
61
|
+
activeRunId = null;
|
|
62
|
+
constructor(projectDir, apisDir, pathOf,
|
|
63
|
+
/**
|
|
64
|
+
* Getter (not a static list) so the prompt always reflects the
|
|
65
|
+
* currently-registered node types, even if the registry is populated or
|
|
66
|
+
* changes after this manager is constructed.
|
|
67
|
+
*/
|
|
68
|
+
availableNodes = () => [],
|
|
69
|
+
/** The loaded project's authored language (`ProjectConfig.language`,
|
|
70
|
+
* explicit-or-inferred), threaded into every prompt this manager builds.
|
|
71
|
+
* Defaults to 'typescript' — the Emberflow repo itself, when constructed
|
|
72
|
+
* without a loaded consumer project. */
|
|
73
|
+
projectLanguage = 'typescript',
|
|
74
|
+
/**
|
|
75
|
+
* Getter (not a static value) for the loaded `emberflow/infrastructure.json`
|
|
76
|
+
* manifest, so every prompt reflects the manifest currently on disk — a scout
|
|
77
|
+
* run mid-session updates what later runs see. Returns null when the project
|
|
78
|
+
* hasn't been scouted or the file is malformed. Injected as a prompt preamble
|
|
79
|
+
* so agents REUSE known infrastructure instead of inventing parallel config.
|
|
80
|
+
*/
|
|
81
|
+
infrastructure = () => null) {
|
|
82
|
+
this.projectDir = projectDir;
|
|
83
|
+
this.apisDir = apisDir;
|
|
84
|
+
this.pathOf = pathOf;
|
|
85
|
+
this.availableNodes = availableNodes;
|
|
86
|
+
this.projectLanguage = projectLanguage;
|
|
87
|
+
this.infrastructure = infrastructure;
|
|
88
|
+
}
|
|
89
|
+
/** Validates the project + flow, snapshots git, spawns the adapter, and returns the new run's id. */
|
|
90
|
+
start(intent, opts = {}) {
|
|
91
|
+
if (this.activeRunId) {
|
|
92
|
+
throw new AgentStartError('An agent run is already active for this project', 409);
|
|
93
|
+
}
|
|
94
|
+
if (!isGitRepo(this.projectDir)) {
|
|
95
|
+
throw new AgentStartError(`${this.projectDir} is not a git repository`, 400);
|
|
96
|
+
}
|
|
97
|
+
// `new-operation` creates a brand-new flow, so there's no existing
|
|
98
|
+
// flowId to validate against pathOf — it validates `location` instead.
|
|
99
|
+
// `setup-auth` targets an environment, not a flow file, and
|
|
100
|
+
// `setup-environments` targets the environments file itself, so
|
|
101
|
+
// neither has a relPath to resolve.
|
|
102
|
+
let relPath;
|
|
103
|
+
if (intent.action === 'new-operation') {
|
|
104
|
+
const dir = resolveLocationDir(intent.location);
|
|
105
|
+
if (!isSafeLocationDir(dir, this.apisDir)) {
|
|
106
|
+
throw new AgentStartError('Invalid location', 400);
|
|
107
|
+
}
|
|
108
|
+
relPath = dir;
|
|
109
|
+
}
|
|
110
|
+
else if (intent.action === 'setup-auth' ||
|
|
111
|
+
intent.action === 'setup-environments' ||
|
|
112
|
+
intent.action === 'scout-infrastructure') {
|
|
113
|
+
// No flow file: setup-auth targets an environment, setup-environments the
|
|
114
|
+
// environments file, and scout-infrastructure reads the whole project and
|
|
115
|
+
// writes emberflow/infrastructure.json — none has a relPath to resolve.
|
|
116
|
+
relPath = '';
|
|
117
|
+
}
|
|
118
|
+
else if (intent.action === 'ask') {
|
|
119
|
+
// `ask`'s flowId is optional: with one, resolve the op's relPath like
|
|
120
|
+
// edit-flow does; without, resolve to the apis root (empty relPath) —
|
|
121
|
+
// there's no specific operation file to point the prompt at.
|
|
122
|
+
if (intent.flowId) {
|
|
123
|
+
if (!isSafeFlowId(intent.flowId, this.apisDir)) {
|
|
124
|
+
throw new AgentStartError('Invalid flowId', 400);
|
|
125
|
+
}
|
|
126
|
+
const found = this.pathOf(intent.flowId);
|
|
127
|
+
if (!found) {
|
|
128
|
+
throw new AgentStartError(`Unknown flow: ${intent.flowId}`, 400);
|
|
129
|
+
}
|
|
130
|
+
relPath = found;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
relPath = '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (!isSafeFlowId(intent.flowId, this.apisDir)) {
|
|
138
|
+
throw new AgentStartError('Invalid flowId', 400);
|
|
139
|
+
}
|
|
140
|
+
const found = this.pathOf(intent.flowId);
|
|
141
|
+
if (!found) {
|
|
142
|
+
throw new AgentStartError(`Unknown flow: ${intent.flowId}`, 400);
|
|
143
|
+
}
|
|
144
|
+
relPath = found;
|
|
145
|
+
}
|
|
146
|
+
this.pruneTerminalRuns();
|
|
147
|
+
const snap = snapshot(this.projectDir);
|
|
148
|
+
const prompt = buildPrompt(intent, this.apisDir, relPath, this.availableNodes(), this.projectLanguage, this.infrastructure());
|
|
149
|
+
const adapter = opts.agent === 'claude'
|
|
150
|
+
? spawnClaude(prompt, this.projectDir, {
|
|
151
|
+
model: opts.model,
|
|
152
|
+
bin: process.env.EMBERFLOW_CLAUDE_BIN,
|
|
153
|
+
})
|
|
154
|
+
: spawnCodex(prompt, this.projectDir, {
|
|
155
|
+
model: opts.model,
|
|
156
|
+
reasoning: opts.reasoning ?? 'medium',
|
|
157
|
+
bin: process.env.EMBERFLOW_CODEX_BIN,
|
|
158
|
+
});
|
|
159
|
+
const id = randomUUID();
|
|
160
|
+
const run = { id, snapshot: snap, adapter, buffer: [], listeners: new Set(), status: 'running' };
|
|
161
|
+
this.runs.set(id, run);
|
|
162
|
+
this.activeRunId = id;
|
|
163
|
+
void this.consume(run);
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
/** Drains the adapter's events into the run's buffer, fanning each out to live subscribers. */
|
|
167
|
+
async consume(run) {
|
|
168
|
+
const emit = (event) => {
|
|
169
|
+
run.buffer.push(event);
|
|
170
|
+
for (const listener of run.listeners)
|
|
171
|
+
listener(event);
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
for await (const event of run.adapter.events) {
|
|
175
|
+
// Clear the busy slot as soon as a terminal event is *observed*, not
|
|
176
|
+
// once the adapter's generator fully drains — a subscriber reacting
|
|
177
|
+
// synchronously to `done`/`error` (e.g. by starting a new run) must
|
|
178
|
+
// not race against this loop's own bookkeeping.
|
|
179
|
+
if (event.type === 'done') {
|
|
180
|
+
run.status = 'done';
|
|
181
|
+
this.clearActive(run.id);
|
|
182
|
+
}
|
|
183
|
+
else if (event.type === 'error') {
|
|
184
|
+
run.status = 'error';
|
|
185
|
+
this.clearActive(run.id);
|
|
186
|
+
}
|
|
187
|
+
emit(event);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
run.status = 'error';
|
|
192
|
+
this.clearActive(run.id);
|
|
193
|
+
emit({ type: 'error', text: err instanceof Error ? err.message : String(err) });
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
this.clearActive(run.id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
clearActive(id) {
|
|
200
|
+
if (this.activeRunId === id)
|
|
201
|
+
this.activeRunId = null;
|
|
202
|
+
}
|
|
203
|
+
/** Replay the buffer to the listener, then stream live events. Returns an unsubscribe fn. */
|
|
204
|
+
subscribe(id, listener) {
|
|
205
|
+
const run = this.runs.get(id);
|
|
206
|
+
if (!run)
|
|
207
|
+
return undefined;
|
|
208
|
+
for (const event of run.buffer)
|
|
209
|
+
listener(event);
|
|
210
|
+
run.listeners.add(listener);
|
|
211
|
+
return () => run.listeners.delete(listener);
|
|
212
|
+
}
|
|
213
|
+
has(id) {
|
|
214
|
+
return this.runs.has(id);
|
|
215
|
+
}
|
|
216
|
+
diff(id) {
|
|
217
|
+
const run = this.runs.get(id);
|
|
218
|
+
if (!run)
|
|
219
|
+
return undefined;
|
|
220
|
+
return {
|
|
221
|
+
diff: diffSince(this.projectDir, run.snapshot),
|
|
222
|
+
files: changedFiles(this.projectDir, run.snapshot),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
revert(id) {
|
|
226
|
+
const run = this.runs.get(id);
|
|
227
|
+
if (!run)
|
|
228
|
+
return undefined;
|
|
229
|
+
const files = changedFiles(this.projectDir, run.snapshot);
|
|
230
|
+
gitRevert(this.projectDir, run.snapshot, files);
|
|
231
|
+
return { reverted: files };
|
|
232
|
+
}
|
|
233
|
+
cancel(id) {
|
|
234
|
+
const run = this.runs.get(id);
|
|
235
|
+
if (!run)
|
|
236
|
+
return false;
|
|
237
|
+
run.adapter.cancel();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Evicts oldest terminal (done|error) runs beyond MAX_TERMINAL_RUNS so the
|
|
242
|
+
* `runs` map — and each run's full event buffer — doesn't grow unbounded
|
|
243
|
+
* over a long-lived server process. The active run (if any) is never
|
|
244
|
+
* evicted; insertion order on the Map gives us oldest-first for free.
|
|
245
|
+
*/
|
|
246
|
+
pruneTerminalRuns() {
|
|
247
|
+
const terminalIds = [];
|
|
248
|
+
for (const [id, run] of this.runs) {
|
|
249
|
+
if (run.status === 'done' || run.status === 'error')
|
|
250
|
+
terminalIds.push(id);
|
|
251
|
+
}
|
|
252
|
+
const excess = terminalIds.length - MAX_TERMINAL_RUNS;
|
|
253
|
+
if (excess <= 0)
|
|
254
|
+
return;
|
|
255
|
+
for (const id of terminalIds.slice(0, excess)) {
|
|
256
|
+
this.runs.delete(id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type AgentKind = 'codex' | 'claude';
|
|
2
|
+
export interface AgentEvent {
|
|
3
|
+
type: 'started' | 'message' | 'command' | 'mcp' | 'approval-request' | 'done' | 'error';
|
|
4
|
+
text?: string;
|
|
5
|
+
command?: string;
|
|
6
|
+
commandStatus?: 'in_progress' | 'completed' | 'failed';
|
|
7
|
+
exitCode?: number | null;
|
|
8
|
+
output?: string;
|
|
9
|
+
toolUseId?: string;
|
|
10
|
+
approvalId?: string;
|
|
11
|
+
usage?: Record<string, number>;
|
|
12
|
+
mcpServer?: string;
|
|
13
|
+
mcpTool?: string;
|
|
14
|
+
mcpStatus?: 'in_progress' | 'completed' | 'failed';
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AuthPolicy, HttpTrigger, WorkflowDefinition } from '../src/engine/index.js';
|
|
2
|
+
export interface OpSummary {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
http?: HttpTrigger;
|
|
7
|
+
}
|
|
8
|
+
export interface FolderNode {
|
|
9
|
+
name: string;
|
|
10
|
+
folders: FolderNode[];
|
|
11
|
+
operations: OpSummary[];
|
|
12
|
+
}
|
|
13
|
+
export interface ApiNode {
|
|
14
|
+
name: string;
|
|
15
|
+
folders: FolderNode[];
|
|
16
|
+
operations: OpSummary[];
|
|
17
|
+
}
|
|
18
|
+
export interface ApiTree {
|
|
19
|
+
apis: ApiNode[];
|
|
20
|
+
}
|
|
21
|
+
/** File-backed store over an `apis/<api>/<folder…>/<op>.json` tree. Keys ops by
|
|
22
|
+
* their in-file `id`; tracks each op's relative path (POSIX, no `.json`). */
|
|
23
|
+
export declare class ApiStore {
|
|
24
|
+
readonly dir: string;
|
|
25
|
+
/** id → relative path (POSIX, no extension). Rebuilt on every scan. */
|
|
26
|
+
private index;
|
|
27
|
+
constructor(dir: string);
|
|
28
|
+
private sidecarFor;
|
|
29
|
+
/** Sidecar files come in two shapes: the legacy bare array (scenarios only —
|
|
30
|
+
* still written when an op has no mocks, for human-editability/back-compat)
|
|
31
|
+
* and `{ scenarios?, mocks? }`, used once an op carries op-level mocks. */
|
|
32
|
+
private withSidecar;
|
|
33
|
+
/** Reads just the sidecar's top-level `mocks`, ignoring `scenarios` — used
|
|
34
|
+
* by `save` to preserve mocks when the incoming flow doesn't carry them. */
|
|
35
|
+
private readSidecarMocks;
|
|
36
|
+
/** Recursively collect operation files as { flow, relPath }. */
|
|
37
|
+
private scan;
|
|
38
|
+
list(): WorkflowDefinition[];
|
|
39
|
+
/** One entry per operation with its on-disk relative path and `http` metadata
|
|
40
|
+
* (id/name/http from the file itself), for the studio's tree view. */
|
|
41
|
+
listSummaries(): OpSummary[];
|
|
42
|
+
/** True when an operation file already sits at `relPath` — used by
|
|
43
|
+
* POST /operations to refuse to silently overwrite an existing op with a
|
|
44
|
+
* brand-new one saved at the same path. */
|
|
45
|
+
existsAt(relPath: string): boolean;
|
|
46
|
+
load(id: string): WorkflowDefinition | undefined;
|
|
47
|
+
/** id → relative path. Always rescans so it reflects the current on-disk state
|
|
48
|
+
* (files may be added/renamed/deleted out-of-band by a coding agent). */
|
|
49
|
+
pathOf(id: string): string | undefined;
|
|
50
|
+
tree(): ApiTree;
|
|
51
|
+
save(flow: WorkflowDefinition, relPath: string): void;
|
|
52
|
+
remove(id: string): boolean;
|
|
53
|
+
/** Resolves the effective auth policy for an operation: walks the op's path
|
|
54
|
+
* from the API root down through each folder, reading `_meta.json`'s `auth`
|
|
55
|
+
* (nearest ancestor default), then applies the op's own `http.auth`
|
|
56
|
+
* override — `'none'` -> explicitly public (null), a policy object -> that
|
|
57
|
+
* policy, `'inherit'`/absent -> the inherited default. Returns the
|
|
58
|
+
* effective policy or null (no auth — public by default). */
|
|
59
|
+
resolveAuth(id: string): AuthPolicy | null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/** File-backed store over an `apis/<api>/<folder…>/<op>.json` tree. Keys ops by
|
|
4
|
+
* their in-file `id`; tracks each op's relative path (POSIX, no `.json`). */
|
|
5
|
+
export class ApiStore {
|
|
6
|
+
dir;
|
|
7
|
+
/** id → relative path (POSIX, no extension). Rebuilt on every scan. */
|
|
8
|
+
index = new Map();
|
|
9
|
+
constructor(dir) {
|
|
10
|
+
this.dir = dir;
|
|
11
|
+
mkdirSync(this.dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
sidecarFor(relPath) {
|
|
14
|
+
return join(this.dir, ...relPath.split('/')) + '.scenarios.json';
|
|
15
|
+
}
|
|
16
|
+
/** Sidecar files come in two shapes: the legacy bare array (scenarios only —
|
|
17
|
+
* still written when an op has no mocks, for human-editability/back-compat)
|
|
18
|
+
* and `{ scenarios?, mocks? }`, used once an op carries op-level mocks. */
|
|
19
|
+
withSidecar(flow, relPath) {
|
|
20
|
+
const path = this.sidecarFor(relPath);
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return flow;
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
25
|
+
if (Array.isArray(raw)) {
|
|
26
|
+
return raw.length > 0 ? { ...flow, scenarios: raw } : flow;
|
|
27
|
+
}
|
|
28
|
+
if (raw && typeof raw === 'object') {
|
|
29
|
+
const { scenarios, mocks } = raw;
|
|
30
|
+
let out = flow;
|
|
31
|
+
if (Array.isArray(scenarios) && scenarios.length > 0)
|
|
32
|
+
out = { ...out, scenarios };
|
|
33
|
+
if (mocks && typeof mocks === 'object')
|
|
34
|
+
out = { ...out, mocks };
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
return flow;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.scenarios.json: ${String(err)}`);
|
|
41
|
+
return flow;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Reads just the sidecar's top-level `mocks`, ignoring `scenarios` — used
|
|
45
|
+
* by `save` to preserve mocks when the incoming flow doesn't carry them. */
|
|
46
|
+
readSidecarMocks(relPath) {
|
|
47
|
+
const path = this.sidecarFor(relPath);
|
|
48
|
+
if (!existsSync(path))
|
|
49
|
+
return undefined;
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
52
|
+
if (Array.isArray(raw) || !raw || typeof raw !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
const { mocks } = raw;
|
|
55
|
+
return mocks && typeof mocks === 'object' ? mocks : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Recursively collect operation files as { flow, relPath }. */
|
|
62
|
+
scan() {
|
|
63
|
+
const out = [];
|
|
64
|
+
this.index = new Map();
|
|
65
|
+
const walk = (absDir, relParts) => {
|
|
66
|
+
for (const entry of readdirSync(absDir)) {
|
|
67
|
+
const abs = join(absDir, entry);
|
|
68
|
+
if (statSync(abs).isDirectory()) {
|
|
69
|
+
walk(abs, [...relParts, entry]);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry === '_meta.json')
|
|
73
|
+
continue;
|
|
74
|
+
if (!entry.endsWith('.json') || entry.endsWith('.scenarios.json'))
|
|
75
|
+
continue;
|
|
76
|
+
const relPath = [...relParts, entry.slice(0, -'.json'.length)].join('/');
|
|
77
|
+
try {
|
|
78
|
+
const flow = JSON.parse(readFileSync(abs, 'utf8'));
|
|
79
|
+
const existing = this.index.get(flow.id);
|
|
80
|
+
if (existing !== undefined && existing !== relPath) {
|
|
81
|
+
console.warn(`[apiStore] duplicate operation id "${flow.id}" at ${existing} and ${relPath} — only one is reachable`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.index.set(flow.id, relPath);
|
|
85
|
+
}
|
|
86
|
+
out.push({ flow, relPath });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.json: ${String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(this.dir, []);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
list() {
|
|
97
|
+
return this.scan().map(({ flow, relPath }) => this.withSidecar(flow, relPath));
|
|
98
|
+
}
|
|
99
|
+
/** One entry per operation with its on-disk relative path and `http` metadata
|
|
100
|
+
* (id/name/http from the file itself), for the studio's tree view. */
|
|
101
|
+
listSummaries() {
|
|
102
|
+
return this.scan().map(({ flow, relPath }) => ({ id: flow.id, name: flow.name, path: relPath, http: flow.http }));
|
|
103
|
+
}
|
|
104
|
+
/** True when an operation file already sits at `relPath` — used by
|
|
105
|
+
* POST /operations to refuse to silently overwrite an existing op with a
|
|
106
|
+
* brand-new one saved at the same path. */
|
|
107
|
+
existsAt(relPath) {
|
|
108
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
109
|
+
return existsSync(abs);
|
|
110
|
+
}
|
|
111
|
+
load(id) {
|
|
112
|
+
const relPath = this.pathOf(id);
|
|
113
|
+
if (!relPath)
|
|
114
|
+
return undefined;
|
|
115
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
116
|
+
if (!existsSync(abs))
|
|
117
|
+
return undefined;
|
|
118
|
+
try {
|
|
119
|
+
const flow = JSON.parse(readFileSync(abs, 'utf8'));
|
|
120
|
+
return this.withSidecar(flow, relPath);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.json: ${String(err)}`);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** id → relative path. Always rescans so it reflects the current on-disk state
|
|
128
|
+
* (files may be added/renamed/deleted out-of-band by a coding agent). */
|
|
129
|
+
pathOf(id) {
|
|
130
|
+
this.scan();
|
|
131
|
+
return this.index.get(id);
|
|
132
|
+
}
|
|
133
|
+
tree() {
|
|
134
|
+
const ops = this.scan();
|
|
135
|
+
const apis = new Map();
|
|
136
|
+
const folderAt = (parent, name) => {
|
|
137
|
+
let f = parent.folders.find((x) => x.name === name);
|
|
138
|
+
if (!f) {
|
|
139
|
+
f = { name, folders: [], operations: [] };
|
|
140
|
+
parent.folders.push(f);
|
|
141
|
+
}
|
|
142
|
+
return f;
|
|
143
|
+
};
|
|
144
|
+
for (const { flow, relPath } of ops) {
|
|
145
|
+
const parts = relPath.split('/'); // [api, ...folders, opName]
|
|
146
|
+
const apiName = parts[0];
|
|
147
|
+
let api = apis.get(apiName);
|
|
148
|
+
if (!api) {
|
|
149
|
+
api = { name: apiName, folders: [], operations: [] };
|
|
150
|
+
apis.set(apiName, api);
|
|
151
|
+
}
|
|
152
|
+
const folderParts = parts.slice(1, -1);
|
|
153
|
+
let container = api;
|
|
154
|
+
for (const fp of folderParts)
|
|
155
|
+
container = folderAt(container, fp);
|
|
156
|
+
container.operations.push({ id: flow.id, name: flow.name, path: relPath, http: flow.http });
|
|
157
|
+
}
|
|
158
|
+
return { apis: [...apis.values()] };
|
|
159
|
+
}
|
|
160
|
+
save(flow, relPath) {
|
|
161
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
162
|
+
if (existsSync(abs)) {
|
|
163
|
+
try {
|
|
164
|
+
const existing = JSON.parse(readFileSync(abs, 'utf8'));
|
|
165
|
+
if (existing.id !== undefined && existing.id !== flow.id) {
|
|
166
|
+
console.warn(`[apiStore] overwriting ${relPath} which held a different op id "${existing.id}"`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// unparseable existing file — nothing sensible to compare, proceed with the write.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
174
|
+
const { scenarios, mocks, ...rest } = flow;
|
|
175
|
+
writeFileSync(abs, `${JSON.stringify(rest, null, 2)}\n`, 'utf8');
|
|
176
|
+
const sidecar = this.sidecarFor(relPath);
|
|
177
|
+
// A flow argument that doesn't carry `mocks` at all (undefined — not an
|
|
178
|
+
// empty object) means the caller never touched op-level mocks (e.g. a
|
|
179
|
+
// scenario-only edit round-tripped through a client that doesn't forward
|
|
180
|
+
// unknown fields) — preserve whatever is already on disk rather than
|
|
181
|
+
// silently dropping it.
|
|
182
|
+
const effectiveMocks = mocks !== undefined ? mocks : this.readSidecarMocks(relPath);
|
|
183
|
+
const hasScenarios = scenarios !== undefined && scenarios.length > 0;
|
|
184
|
+
const hasMocks = effectiveMocks !== undefined && Object.keys(effectiveMocks).length > 0;
|
|
185
|
+
if (hasMocks) {
|
|
186
|
+
writeFileSync(sidecar, `${JSON.stringify({ ...(hasScenarios ? { scenarios } : {}), mocks: effectiveMocks }, null, 2)}\n`, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
else if (hasScenarios) {
|
|
189
|
+
writeFileSync(sidecar, `${JSON.stringify(scenarios, null, 2)}\n`, 'utf8');
|
|
190
|
+
}
|
|
191
|
+
else if (existsSync(sidecar)) {
|
|
192
|
+
rmSync(sidecar);
|
|
193
|
+
}
|
|
194
|
+
this.index.set(flow.id, relPath);
|
|
195
|
+
}
|
|
196
|
+
remove(id) {
|
|
197
|
+
const relPath = this.pathOf(id);
|
|
198
|
+
if (!relPath)
|
|
199
|
+
return false;
|
|
200
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
201
|
+
if (!existsSync(abs))
|
|
202
|
+
return false;
|
|
203
|
+
rmSync(abs);
|
|
204
|
+
const sidecar = this.sidecarFor(relPath);
|
|
205
|
+
if (existsSync(sidecar))
|
|
206
|
+
rmSync(sidecar);
|
|
207
|
+
this.index.delete(id);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
/** Resolves the effective auth policy for an operation: walks the op's path
|
|
211
|
+
* from the API root down through each folder, reading `_meta.json`'s `auth`
|
|
212
|
+
* (nearest ancestor default), then applies the op's own `http.auth`
|
|
213
|
+
* override — `'none'` -> explicitly public (null), a policy object -> that
|
|
214
|
+
* policy, `'inherit'`/absent -> the inherited default. Returns the
|
|
215
|
+
* effective policy or null (no auth — public by default). */
|
|
216
|
+
resolveAuth(id) {
|
|
217
|
+
const relPath = this.pathOf(id);
|
|
218
|
+
if (!relPath)
|
|
219
|
+
return null;
|
|
220
|
+
const parts = relPath.split('/'); // [api, ...folders, opName]
|
|
221
|
+
let inherited = null;
|
|
222
|
+
for (let depth = 1; depth < parts.length; depth++) {
|
|
223
|
+
const metaPath = join(this.dir, ...parts.slice(0, depth), '_meta.json');
|
|
224
|
+
if (!existsSync(metaPath))
|
|
225
|
+
continue;
|
|
226
|
+
try {
|
|
227
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
228
|
+
if (meta.auth)
|
|
229
|
+
inherited = meta.auth;
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
// A present-but-corrupt _meta.json (partial write, typo, etc.) must
|
|
233
|
+
// never silently resolve to `null` (= public) — that's a fail-open
|
|
234
|
+
// security hole. Fail closed: throw and let the caller deny the op
|
|
235
|
+
// rather than mount it open. A merely ABSENT _meta.json is fine (see
|
|
236
|
+
// the `existsSync` check above) — only a broken one throws.
|
|
237
|
+
const metaRelPath = `${parts.slice(0, depth).join('/')}/_meta.json`;
|
|
238
|
+
throw new Error(`unparseable _meta.json at ${metaRelPath} — refusing to resolve auth (fail closed): ${String(err)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const op = this.load(id);
|
|
242
|
+
const override = op?.http?.auth;
|
|
243
|
+
if (override === 'none')
|
|
244
|
+
return null;
|
|
245
|
+
if (override && override !== 'inherit')
|
|
246
|
+
return override;
|
|
247
|
+
return inherited;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { enforceAuth } from '../../src/engine/index.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-exports the shared enforceAuth from src/engine/authVerify.ts. Kept as its
|
|
2
|
+
// own module (rather than importing src/engine directly at call sites) so the
|
|
3
|
+
// server-side auth call path has a stable, server-scoped entry point mirroring
|
|
4
|
+
// the file map in the plan; the underlying logic is shared with the
|
|
5
|
+
// requireAuth node (src/nodes/requireAuth.ts) to avoid divergence.
|
|
6
|
+
export { enforceAuth } from '../../src/engine/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { AuthPolicy, VerifyResult, Verifier } from '../../src/engine/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { bearerVerifier, apiKeyVerifier, VerifierRegistry, createDefaultVerifierRegistry, } from '../../src/engine/index.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Re-exports the shared, browser-safe implementation from src/engine/authVerify.ts
|
|
2
|
+
// so server-side auth enforcement and the requireAuth node (bundled into the
|
|
3
|
+
// studio browser build) share one implementation instead of two diverging copies.
|
|
4
|
+
export { bearerVerifier, apiKeyVerifier, VerifierRegistry, createDefaultVerifierRegistry, } from '../../src/engine/index.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EnvAuth } from './environments.js';
|
|
2
|
+
type LoginCapture = NonNullable<EnvAuth['login']>['capture'];
|
|
3
|
+
/**
|
|
4
|
+
* Attaches a captured credential value onto a headers object for a run.
|
|
5
|
+
* Pure and non-mutating: returns a NEW headers object.
|
|
6
|
+
* Additive + non-destructive — if the target header/cookie-name is already
|
|
7
|
+
* present, the input is returned unchanged (caller-explicit wins).
|
|
8
|
+
*/
|
|
9
|
+
export declare function attachCredential(headers: Record<string, unknown>, attach: EnvAuth['attach'], value: string): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Extracts a captured credential value from a login response.
|
|
12
|
+
* Pure with respect to its inputs (no I/O of its own) — the caller performs
|
|
13
|
+
* the actual fetch and passes in the response-like object.
|
|
14
|
+
*/
|
|
15
|
+
export declare function captureCredential(res: {
|
|
16
|
+
headers: Headers;
|
|
17
|
+
json: () => Promise<any>;
|
|
18
|
+
}, capture: LoginCapture): Promise<string | null>;
|
|
19
|
+
export {};
|