stageflow 0.23.0 → 0.27.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/README.md +24 -5
- package/dist/a2a/configDiscovery.d.ts +9 -0
- package/dist/a2a/configDiscovery.js +18 -0
- package/dist/a2a/contracts.d.ts +94 -0
- package/dist/a2a/contracts.js +164 -0
- package/dist/a2a/limits.d.ts +14 -0
- package/dist/a2a/limits.js +29 -0
- package/dist/a2a/registry.d.ts +46 -0
- package/dist/a2a/registry.js +178 -0
- package/dist/a2a/server.d.ts +36 -0
- package/dist/a2a/server.js +344 -0
- package/dist/a2a/service.d.ts +97 -0
- package/dist/a2a/service.js +545 -0
- package/dist/a2a/store.d.ts +109 -0
- package/dist/a2a/store.js +202 -0
- package/dist/agent/activity.d.ts +3 -0
- package/dist/agent/bootProviderConfig.d.ts +21 -0
- package/dist/agent/bootProviderConfig.js +74 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +58 -3
- package/dist/agent/piIsolatedMcp.d.ts +2 -0
- package/dist/agent/piIsolatedMcp.js +10 -2
- package/dist/agent/port.d.ts +6 -0
- package/dist/agent/streamLogRedact.d.ts +1 -1
- package/dist/agent/streamLogRedact.js +4 -11
- package/dist/cli/a2aCommand.d.ts +8 -0
- package/dist/cli/a2aCommand.js +131 -0
- package/dist/cli/artifactCommand.js +9 -16
- package/dist/cli/backupCommand.d.ts +9 -0
- package/dist/cli/backupCommand.js +111 -0
- package/dist/cli/debugRunCommand.d.ts +10 -0
- package/dist/cli/debugRunCommand.js +109 -0
- package/dist/cli/doctorCommand.d.ts +32 -0
- package/dist/cli/doctorCommand.js +453 -0
- package/dist/cli/envelopeCommand.js +9 -2
- package/dist/cli/exportAllCommand.d.ts +55 -0
- package/dist/cli/exportAllCommand.js +218 -0
- package/dist/cli/exportRunCommand.js +23 -21
- package/dist/cli/hostClient.d.ts +21 -3
- package/dist/cli/hostClient.js +93 -12
- package/dist/cli/initTemplates.d.ts +1 -1
- package/dist/cli/initTemplates.js +8 -1
- package/dist/cli/resolveSafeOutPath.d.ts +2 -0
- package/dist/cli/resolveSafeOutPath.js +40 -0
- package/dist/cli/restoreCommand.d.ts +10 -0
- package/dist/cli/restoreCommand.js +90 -0
- package/dist/cli/runCommand.d.ts +3 -2
- package/dist/cli/runCommand.js +93 -11
- package/dist/cli/runOutput.d.ts +4 -0
- package/dist/cli/runOutput.js +33 -7
- package/dist/cli/runStageCommand.d.ts +34 -0
- package/dist/cli/runStageCommand.js +426 -0
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +162 -8
- package/dist/cli.js +190 -30
- package/dist/config/catalogRelativePath.d.ts +62 -0
- package/dist/config/catalogRelativePath.js +118 -0
- package/dist/config/configOrigin.d.ts +27 -0
- package/dist/config/configOrigin.js +59 -0
- package/dist/config/hostConfig.d.ts +55 -0
- package/dist/config/hostConfig.js +333 -0
- package/dist/config/listSkills.d.ts +26 -0
- package/dist/config/listSkills.js +190 -3
- package/dist/config/loadOutcome.d.ts +1 -1
- package/dist/config/loadPipeline.js +23 -2
- package/dist/config/loadStage.js +18 -0
- package/dist/config/loadTask.d.ts +2 -0
- package/dist/config/loadTask.js +57 -18
- package/dist/config/materializeStageModels.js +5 -1
- package/dist/config/mergePipelineIncludes.d.ts +3 -0
- package/dist/config/mergePipelineIncludes.js +19 -0
- package/dist/config/migrateYaml.js +6 -11
- package/dist/config/multiProjectCatalog.d.ts +30 -0
- package/dist/config/multiProjectCatalog.js +142 -0
- package/dist/config/normalizePipelineStageEntry.js +20 -1
- package/dist/config/pipelineStageKeys.js +2 -0
- package/dist/config/resolveCatalogRoots.d.ts +27 -0
- package/dist/config/resolveCatalogRoots.js +76 -0
- package/dist/config/resolveModel.d.ts +7 -1
- package/dist/config/resolveModel.js +11 -1
- package/dist/config/resolveStageMcpServers.d.ts +9 -1
- package/dist/config/resolveStageMcpServers.js +36 -9
- package/dist/config/secretFromEnvOrFile.d.ts +9 -0
- package/dist/config/secretFromEnvOrFile.js +34 -0
- package/dist/config/seededCatalog.d.ts +11 -0
- package/dist/config/seededCatalog.js +29 -0
- package/dist/config/toolRequires.d.ts +24 -0
- package/dist/config/toolRequires.js +115 -0
- package/dist/config/validateCatalog.d.ts +5 -1
- package/dist/config/validateCatalog.js +140 -1
- package/dist/diagnostics/checks.d.ts +39 -0
- package/dist/diagnostics/checks.js +135 -0
- package/dist/errors/codes.d.ts +14 -0
- package/dist/errors/codes.js +59 -0
- package/dist/git/cache.d.ts +14 -0
- package/dist/git/cache.js +150 -0
- package/dist/git/credentials.d.ts +9 -0
- package/dist/git/credentials.js +88 -0
- package/dist/git/errors.d.ts +20 -0
- package/dist/git/errors.js +94 -0
- package/dist/git/exec.d.ts +16 -0
- package/dist/git/exec.js +135 -0
- package/dist/git/operations.d.ts +29 -0
- package/dist/git/operations.js +153 -0
- package/dist/git/version.d.ts +1 -0
- package/dist/git/version.js +10 -0
- package/dist/logging/audit.d.ts +16 -0
- package/dist/logging/audit.js +19 -0
- package/dist/logging/commonWords.d.ts +2 -0
- package/dist/logging/commonWords.js +13 -0
- package/dist/logging/logger.d.ts +49 -0
- package/dist/logging/logger.js +193 -0
- package/dist/logging/namedSecrets.d.ts +6 -0
- package/dist/logging/namedSecrets.js +58 -0
- package/dist/logging/redact.d.ts +13 -0
- package/dist/logging/redact.js +88 -0
- package/dist/mcp/catalogTools.js +591 -76
- package/dist/mcp/checkoutTools.d.ts +52 -0
- package/dist/mcp/checkoutTools.js +332 -0
- package/dist/mcp/controlTools.js +179 -5
- package/dist/mcp/deps.d.ts +1 -0
- package/dist/mcp/getStartedTool.d.ts +3 -0
- package/dist/mcp/getStartedTool.js +62 -0
- package/dist/mcp/readArtifact.d.ts +11 -0
- package/dist/mcp/readArtifact.js +46 -30
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/net/proxy.d.ts +14 -0
- package/dist/net/proxy.js +78 -0
- package/dist/package-meta.d.ts +2 -1
- package/dist/package-meta.js +2 -1
- package/dist/preflight/bash.d.ts +8 -0
- package/dist/preflight/bash.js +34 -0
- package/dist/preflight/claudeRoot.d.ts +11 -0
- package/dist/preflight/claudeRoot.js +23 -0
- package/dist/preflight/mcpCommands.d.ts +8 -0
- package/dist/preflight/mcpCommands.js +36 -0
- package/dist/preflight/pipelinePreflight.d.ts +40 -0
- package/dist/preflight/pipelinePreflight.js +192 -0
- package/dist/preflight/tls.d.ts +6 -0
- package/dist/preflight/tls.js +18 -0
- package/dist/preflight/toolchain.d.ts +45 -0
- package/dist/preflight/toolchain.js +219 -0
- package/dist/project/findProjectRoot.js +3 -4
- package/dist/project/globalHome.d.ts +7 -0
- package/dist/project/globalHome.js +63 -2
- package/dist/project/resolveProjectContext.js +2 -2
- package/dist/project/resolveStageflowContext.js +2 -2
- package/dist/projection/projectRun.d.ts +2 -1
- package/dist/projection/projectRun.js +1 -0
- package/dist/runstore/assertTmpdir.d.ts +1 -0
- package/dist/runstore/assertTmpdir.js +21 -0
- package/dist/runstore/backup.d.ts +52 -0
- package/dist/runstore/backup.js +292 -0
- package/dist/runstore/createStore.d.ts +27 -0
- package/dist/runstore/createStore.js +75 -7
- package/dist/runstore/debugBundle.d.ts +60 -0
- package/dist/runstore/debugBundle.js +143 -0
- package/dist/runstore/diskUsage.d.ts +50 -0
- package/dist/runstore/diskUsage.js +214 -0
- package/dist/runstore/exportRunPayload.d.ts +8 -0
- package/dist/runstore/exportRunPayload.js +23 -0
- package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +10 -0
- package/dist/runstore/paths.d.ts +5 -0
- package/dist/runstore/paths.js +145 -1
- package/dist/runstore/port.d.ts +119 -8
- package/dist/runstore/port.js +8 -1
- package/dist/runstore/restore.d.ts +81 -0
- package/dist/runstore/restore.js +355 -0
- package/dist/runstore/retention.d.ts +27 -0
- package/dist/runstore/retention.js +74 -0
- package/dist/runstore/runManifest.d.ts +146 -0
- package/dist/runstore/runManifest.js +288 -0
- package/dist/runstore/runProjection.d.ts +3 -1
- package/dist/runstore/runProjection.js +63 -1
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +37 -3
- package/dist/runstore/sqlite/SqliteRunStore.js +401 -227
- package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
- package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
- package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
- package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
- package/dist/runstore/sqlite/migrations/index.js +72 -0
- package/dist/runstore/sqlite/schema.d.ts +20 -1
- package/dist/runstore/sqlite/schema.js +283 -1
- package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
- package/dist/runstore/sqlite/storeOpenError.js +11 -0
- package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
- package/dist/runstore/sqlite/storeSchemaError.js +8 -0
- package/dist/runstore/stageExecution.js +3 -0
- package/dist/runstore/storeFilesystem.d.ts +32 -0
- package/dist/runstore/storeFilesystem.js +148 -0
- package/dist/runstore/submission.d.ts +11 -0
- package/dist/runstore/submission.js +7 -0
- package/dist/runstore/trackProjection.js +2 -0
- package/dist/runstore/workspaceLayout.d.ts +13 -0
- package/dist/runstore/workspaceLayout.js +76 -0
- package/dist/runtime/completionCheckRunner.d.ts +3 -0
- package/dist/runtime/completionCheckRunner.js +45 -7
- package/dist/runtime/containerLimits.d.ts +16 -0
- package/dist/runtime/containerLimits.js +84 -0
- package/dist/runtime/credentialMaterialisation.d.ts +28 -0
- package/dist/runtime/credentialMaterialisation.js +128 -0
- package/dist/runtime/gitCheckoutCapability.js +7 -13
- package/dist/runtime/pipelineRunner.d.ts +38 -1
- package/dist/runtime/pipelineRunner.js +179 -15
- package/dist/runtime/pipelineScheduler.d.ts +15 -0
- package/dist/runtime/pipelineScheduler.js +167 -42
- package/dist/runtime/reloadRunCatalog.js +12 -1
- package/dist/runtime/repositoryMaterialize.d.ts +47 -0
- package/dist/runtime/repositoryMaterialize.js +235 -0
- package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
- package/dist/runtime/resolveEnvelopeRef.js +79 -0
- package/dist/runtime/resumeTimedOut.d.ts +1 -1
- package/dist/runtime/resumeTimedOut.js +5 -2
- package/dist/runtime/runChangeBus.js +9 -0
- package/dist/runtime/runDeletion.d.ts +7 -0
- package/dist/runtime/runDeletion.js +37 -0
- package/dist/runtime/runManager.d.ts +179 -4
- package/dist/runtime/runManager.js +1644 -83
- package/dist/runtime/runRetentionSweep.d.ts +31 -0
- package/dist/runtime/runRetentionSweep.js +328 -0
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +19 -2
- package/dist/runtime/runSkills.d.ts +40 -0
- package/dist/runtime/runSkills.js +173 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
- package/dist/runtime/stageAttemptBootstrap.js +195 -24
- package/dist/runtime/stageCacheEnv.d.ts +6 -0
- package/dist/runtime/stageCacheEnv.js +34 -0
- package/dist/runtime/stageConcurrency.d.ts +2 -1
- package/dist/runtime/stageConcurrency.js +4 -2
- package/dist/runtime/stageEnvironment.d.ts +38 -0
- package/dist/runtime/stageEnvironment.js +193 -0
- package/dist/runtime/stageProcessLauncher.d.ts +18 -0
- package/dist/runtime/stageProcessLauncher.js +194 -38
- package/dist/runtime/stageRecovery.d.ts +3 -1
- package/dist/runtime/stageRecovery.js +19 -4
- package/dist/runtime/stageRoots.d.ts +38 -1
- package/dist/runtime/stageRoots.js +118 -0
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +3 -1
- package/dist/runtime/stageSecretDecl.d.ts +9 -0
- package/dist/runtime/stageSecretDecl.js +102 -0
- package/dist/runtime/stageSecrets.d.ts +40 -0
- package/dist/runtime/stageSecrets.js +181 -0
- package/dist/runtime/stageWorker.d.ts +3 -2
- package/dist/runtime/stageWorker.js +58 -10
- package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
- package/dist/runtime/stageWorkerProtocol.js +8 -0
- package/dist/runtime/startPayload.d.ts +32 -0
- package/dist/runtime/startPayload.js +59 -0
- package/dist/runtime/taskInput.js +14 -5
- package/dist/runtime/verifiedStageExecution.d.ts +1 -0
- package/dist/runtime/verifiedStageExecution.js +2 -1
- package/dist/runtime/workspaceBinding.d.ts +20 -0
- package/dist/runtime/workspaceBinding.js +60 -0
- package/dist/server/allowedHosts.d.ts +19 -0
- package/dist/server/allowedHosts.js +179 -0
- package/dist/server/bootstrap.d.ts +38 -2
- package/dist/server/bootstrap.js +219 -4
- package/dist/server/controlToken.d.ts +47 -0
- package/dist/server/controlToken.js +241 -0
- package/dist/server/createHttpHost.d.ts +11 -2
- package/dist/server/createHttpHost.js +109 -10
- package/dist/server/ensureGlobalService.d.ts +5 -1
- package/dist/server/ensureGlobalService.js +35 -1
- package/dist/server/healthSurfaces.d.ts +6 -0
- package/dist/server/healthSurfaces.js +98 -0
- package/dist/server/http.d.ts +12 -1
- package/dist/server/http.js +1085 -485
- package/dist/server/listenHost.d.ts +5 -0
- package/dist/server/listenHost.js +56 -0
- package/dist/server/mcpHost.d.ts +10 -1
- package/dist/server/mcpHost.js +36 -6
- package/dist/server/operatorResults.d.ts +2 -2
- package/dist/server/operatorResults.js +40 -29
- package/dist/server/requestAuthContext.d.ts +12 -0
- package/dist/server/requestAuthContext.js +21 -0
- package/dist/server/shutdown.d.ts +69 -0
- package/dist/server/shutdown.js +219 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/stage.d.ts +8 -0
- package/dist/types/task.d.ts +8 -0
- package/dist/ui/assets/index-B6hlSXXn.js +120 -0
- package/dist/ui/index.html +1 -1
- package/examples/README.md +98 -0
- package/examples/archify-on-pr/README.md +213 -0
- package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
- package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
- package/examples/archify-on-pr/author-diagrams.yaml +106 -0
- package/examples/archify-on-pr/detect-changes.yaml +89 -0
- package/examples/ci-validate/README.md +35 -0
- package/examples/ci-validate/check.yaml +13 -0
- package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
- package/examples/ci-validate/validate.sh +5 -0
- package/examples/compliance-review-demo/README.md +125 -0
- package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
- package/examples/compliance-review-demo/connect-account.yaml +47 -0
- package/examples/compliance-review-demo/map-to-control.yaml +123 -0
- package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
- package/examples/compliance-review-demo/publish-report.yaml +79 -0
- package/examples/compliance-review-demo/review-verdict.yaml +122 -0
- package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
- package/examples/compliance-review-demo/store-evidence.yaml +143 -0
- package/examples/conditional-fork/README.md +89 -0
- package/examples/conditional-fork/branch-a.yaml +13 -0
- package/examples/conditional-fork/branch-b.yaml +13 -0
- package/examples/conditional-fork/decide.yaml +35 -0
- package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
- package/examples/conditional-fork/fork-demo.task.yaml +8 -0
- package/examples/context7-mcp/README.md +42 -0
- package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
- package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
- package/examples/context7-mcp/fetch-docs.yaml +52 -0
- package/examples/context7-mcp/resolve-library.yaml +31 -0
- package/examples/context7-mcp/write-brief.yaml +52 -0
- package/examples/feature-loop/README.md +104 -0
- package/examples/feature-loop/address-feedback.yaml +148 -0
- package/examples/feature-loop/align.yaml +116 -0
- package/examples/feature-loop/decompose.yaml +86 -0
- package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
- package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
- package/examples/feature-loop/feature-loop.task.yaml +25 -0
- package/examples/feature-loop/implement.yaml +128 -0
- package/examples/feature-loop/plan.yaml +111 -0
- package/examples/feature-loop/publish.yaml +167 -0
- package/examples/feature-loop/review.yaml +146 -0
- package/examples/feature-loop/verify.yaml +106 -0
- package/examples/feedback-loop/README.md +72 -0
- package/examples/feedback-loop/VERIFY.md +208 -0
- package/examples/feedback-loop/continue-only.task.yaml +4 -0
- package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
- package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
- package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
- package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
- package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
- package/examples/feedback-loop/implement.yaml +14 -0
- package/examples/feedback-loop/plan.yaml +14 -0
- package/examples/feedback-loop/review.yaml +15 -0
- package/examples/feedback-loop/send-back-once.task.yaml +6 -0
- package/examples/feedback-loop/submit.yaml +12 -0
- package/examples/generic-fan-in/README.md +151 -0
- package/examples/generic-fan-in/clarify.yaml +18 -0
- package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
- package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
- package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
- package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
- package/examples/generic-fan-in/research.yaml +45 -0
- package/examples/generic-fan-in/synthesize.yaml +66 -0
- package/examples/generic-fan-in/validation.yaml +45 -0
- package/examples/github-release/README.md +78 -0
- package/examples/github-release/draft-release-notes.yaml +64 -0
- package/examples/github-release/github-release.pipeline.yaml +11 -0
- package/examples/github-release/github-release.task.yaml +13 -0
- package/examples/github-release/publish-github-release.yaml +64 -0
- package/examples/hello-world/README.md +28 -0
- package/examples/hello-world/hello.pipeline.yaml +4 -0
- package/examples/hello-world/my-task.task.yaml +6 -0
- package/examples/hello-world/research.yaml +18 -0
- package/examples/mcp-hitl-tour/README.md +113 -0
- package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
- package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
- package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
- package/examples/mcp-hitl-tour/gather.yaml +31 -0
- package/examples/mcp-hitl-tour/intake.yaml +62 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
- package/examples/mcp-hitl-tour/polish.yaml +33 -0
- package/examples/mcp-hitl-tour/review.yaml +17 -0
- package/examples/mcp-hitl-tour/ship.yaml +25 -0
- package/examples/oss-issue-contribution/README.md +218 -0
- package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
- package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
- package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
- package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
- package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
- package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
- package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
- package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
- package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
- package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
- package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
- package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
- package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
- package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
- package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
- package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
- package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
- package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
- package/examples/oss-quick-fix/README.md +65 -0
- package/examples/oss-quick-fix/implement-fix.yaml +84 -0
- package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
- package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
- package/examples/oss-quick-fix/ship-fix.yaml +81 -0
- package/examples/oss-quick-fix/understand-issue.yaml +67 -0
- package/examples/oss-quick-fix/verify-fix.yaml +73 -0
- package/examples/plan-review/README.md +37 -0
- package/examples/plan-review/plan-review-followup.yaml +13 -0
- package/examples/plan-review/plan-review.pipeline.yaml +9 -0
- package/examples/plan-review/plan-review.task.yaml +9 -0
- package/examples/plan-review/plan-review.yaml +25 -0
- package/examples/playwright-mcp/README.md +39 -0
- package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
- package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
- package/examples/playwright-mcp/screenshot.yaml +40 -0
- package/examples/retry-tour/README.md +75 -0
- package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
- package/examples/retry-tour/retry-tour.task.yaml +9 -0
- package/examples/route-if-tour/README.md +202 -0
- package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
- package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
- package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
- package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
- package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
- package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
- package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
- package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
- package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
- package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
- package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
- package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
- package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
- package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
- package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
- package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
- package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
- package/examples/route-wiring-smoke-test/README.md +377 -0
- package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
- package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
- package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
- package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
- package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
- package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
- package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
- package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
- package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
- package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
- package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
- package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
- package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
- package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
- package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
- package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
- package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
- package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
- package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
- package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
- package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
- package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
- package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
- package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
- package/examples/ship-feature/README.md +190 -0
- package/examples/ship-feature/feat-address-feedback.yaml +119 -0
- package/examples/ship-feature/feat-implement.yaml +180 -0
- package/examples/ship-feature/feat-plan.yaml +120 -0
- package/examples/ship-feature/feat-review.yaml +109 -0
- package/examples/ship-feature/feat-ship.yaml +203 -0
- package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
- package/examples/ship-feature/ship-feature.task.yaml +78 -0
- package/examples/stage-mcp/README.md +144 -0
- package/examples/stage-mcp/echo-mcp.mjs +20 -0
- package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
- package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
- package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
- package/examples/stage-mcp/use-echo.yaml +20 -0
- package/examples/verify-tour/README.md +80 -0
- package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
- package/examples/verify-tour/verify-tour.task.yaml +11 -0
- package/package.json +6 -1
- package/skills/stageflow/SKILL.md +6 -6
- package/skills/stageflow/references/control-surface.md +11 -5
- package/skills/stageflow/scripts/detect-host.mjs +14 -1
- package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
- package/skills/stageflow-author/SKILL.md +4 -3
- package/skills/stageflow-author/references/catalog-mapping.md +16 -0
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-run/SKILL.md +20 -12
- package/skills/stageflow-run/references/mcp-call.md +9 -1
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
- package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
- package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
- package/skills/stageflow-setup/SKILL.md +40 -8
- package/dist/ui/assets/index-Da5EHU6M.js +0 -119
package/dist/server/http.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, access } from "node:fs/promises";
|
|
2
|
+
import { createReadStream, statSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
|
|
@@ -7,19 +8,38 @@ import { handleProviderRoutes } from "./providerRoutes.js";
|
|
|
7
8
|
import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
|
|
8
9
|
import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
|
|
9
10
|
import { createStage, parseCreateStageBody } from "../config/createStage.js";
|
|
10
|
-
import {
|
|
11
|
+
import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
|
|
12
|
+
import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
|
|
11
13
|
import { listExtensions } from "../config/listExtensions.js";
|
|
12
14
|
import { listSkills } from "../config/listSkills.js";
|
|
13
|
-
import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
|
|
15
|
+
import { artifactMediaType, classifyArtifactContent, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
|
|
16
|
+
import { getRunDiff, listCheckoutChanges, readCheckoutFileBytes, } from "../mcp/checkoutTools.js";
|
|
14
17
|
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
18
|
+
import { BackupError, createBackup, resolveBackupDownloadPath, } from "../runstore/backup.js";
|
|
19
|
+
import { RestoreError, resolveBackupNameForRestore, stageRestoreForBoot, } from "../runstore/restore.js";
|
|
20
|
+
import { iterateExportNdjson } from "../cli/exportAllCommand.js";
|
|
21
|
+
import { buildRunExportPayload, runDetailWithRedactedManifest, } from "../runstore/exportRunPayload.js";
|
|
22
|
+
import { buildDebugBundle } from "../runstore/debugBundle.js";
|
|
23
|
+
import { parseShutdownGraceMs } from "./shutdown.js";
|
|
24
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
15
25
|
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
16
26
|
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
27
|
+
import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
|
|
17
28
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
|
|
18
29
|
import { isTaskFile } from "../runtime/taskInput.js";
|
|
30
|
+
import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
|
|
19
31
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
20
32
|
import { bootstrapStageflowHost, } from "./bootstrap.js";
|
|
33
|
+
import { assertAllowedHttpAccess, isTrustedLocalHttpRequest, resolveAllowedHosts, } from "./allowedHosts.js";
|
|
34
|
+
import { enforceBearerAuth, loadControlTokens, requiredScopeFor, } from "./controlToken.js";
|
|
21
35
|
import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
|
|
36
|
+
import { handleApiHealth } from "./healthSurfaces.js";
|
|
22
37
|
import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "./operatorResults.js";
|
|
38
|
+
import { installShutdownController, makeDrainableHostFromOptional, } from "./shutdown.js";
|
|
39
|
+
import { writeAudit } from "../logging/audit.js";
|
|
40
|
+
import { logger as rootLogger } from "../logging/logger.js";
|
|
41
|
+
import { callerIdFromRequestAuth, getRequestAuth, requestAuthFromBearer, runWithRequestAuth, } from "./requestAuthContext.js";
|
|
42
|
+
const auditLog = rootLogger.child({ component: "audit" });
|
|
23
43
|
function textPlain(res, status, body) {
|
|
24
44
|
res.writeHead(status, {
|
|
25
45
|
"Content-Type": "text/plain; charset=utf-8",
|
|
@@ -82,27 +102,6 @@ async function serveStatic(res, uiDistDir, urlPath) {
|
|
|
82
102
|
res.end(data);
|
|
83
103
|
return true;
|
|
84
104
|
}
|
|
85
|
-
function isMutatingApi(method, pathname) {
|
|
86
|
-
if (method !== "POST")
|
|
87
|
-
return false;
|
|
88
|
-
return (pathname === "/api/runs" ||
|
|
89
|
-
pathname === "/api/settings" ||
|
|
90
|
-
pathname === "/api/stages" ||
|
|
91
|
-
pathname === "/api/pipelines" ||
|
|
92
|
-
/^\/api\/runs\/[^/]+\/rerun$/.test(pathname) ||
|
|
93
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/answer$/.test(pathname) ||
|
|
94
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/feedback-decision$/.test(pathname) ||
|
|
95
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/retry$/.test(pathname) ||
|
|
96
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/resume$/.test(pathname) ||
|
|
97
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery$/.test(pathname) ||
|
|
98
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery\/stop$/.test(pathname) ||
|
|
99
|
-
/^\/api\/runs\/[^/]+\/stages\/[^/]+\/abandon$/.test(pathname) ||
|
|
100
|
-
/^\/api\/providers\/[^/]+\/login$/.test(pathname) ||
|
|
101
|
-
/^\/api\/providers\/[^/]+\/login\/[^/]+\/answer$/.test(pathname) ||
|
|
102
|
-
/^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
|
|
103
|
-
/^\/api\/providers\/[^/]+\/logout$/.test(pathname) ||
|
|
104
|
-
/^\/api\/project-mcp\/[^/]+\/probe$/.test(pathname));
|
|
105
|
-
}
|
|
106
105
|
function isCredentialMutatingApi(method, pathname) {
|
|
107
106
|
if (method !== "POST")
|
|
108
107
|
return false;
|
|
@@ -111,68 +110,6 @@ function isCredentialMutatingApi(method, pathname) {
|
|
|
111
110
|
/^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
|
|
112
111
|
/^\/api\/providers\/[^/]+\/logout$/.test(pathname));
|
|
113
112
|
}
|
|
114
|
-
/** Login/logout require a non-empty loopback Origin (stricter than other mutating APIs). */
|
|
115
|
-
function assertCredentialMutatingOrigin(req, res) {
|
|
116
|
-
const origin = req.headers.origin;
|
|
117
|
-
if (typeof origin !== "string" || origin.length === 0) {
|
|
118
|
-
json(res, 403, { error: "Origin required" });
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
const originHost = new URL(origin).hostname;
|
|
123
|
-
if (!isLoopbackHostname(originHost)) {
|
|
124
|
-
json(res, 403, { error: "Forbidden origin" });
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
json(res, 403, { error: "Forbidden origin" });
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
function hostnameFromHostHeader(hostHeader) {
|
|
135
|
-
if (hostHeader.startsWith("[")) {
|
|
136
|
-
const end = hostHeader.indexOf("]");
|
|
137
|
-
return end === -1 ? hostHeader : hostHeader.slice(1, end);
|
|
138
|
-
}
|
|
139
|
-
const colon = hostHeader.lastIndexOf(":");
|
|
140
|
-
if (colon > 0 && /^\d+$/.test(hostHeader.slice(colon + 1))) {
|
|
141
|
-
return hostHeader.slice(0, colon);
|
|
142
|
-
}
|
|
143
|
-
return hostHeader;
|
|
144
|
-
}
|
|
145
|
-
function isLoopbackHostname(hostname) {
|
|
146
|
-
const h = hostname.toLowerCase();
|
|
147
|
-
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
148
|
-
}
|
|
149
|
-
/** REST-friendly Host/Origin gate (plain JSON 403). Absent Origin is allowed. */
|
|
150
|
-
function assertLoopbackHttpAccess(req, res) {
|
|
151
|
-
const hostHeader = req.headers.host;
|
|
152
|
-
if (typeof hostHeader !== "string" || hostHeader.length === 0) {
|
|
153
|
-
json(res, 403, { error: "Forbidden host" });
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
if (!isLoopbackHostname(hostnameFromHostHeader(hostHeader))) {
|
|
157
|
-
json(res, 403, { error: "Forbidden host" });
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
const origin = req.headers.origin;
|
|
161
|
-
if (typeof origin === "string" && origin.length > 0) {
|
|
162
|
-
try {
|
|
163
|
-
const originHost = new URL(origin).hostname;
|
|
164
|
-
if (!isLoopbackHostname(originHost)) {
|
|
165
|
-
json(res, 403, { error: "Forbidden origin" });
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
json(res, 403, { error: "Forbidden origin" });
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
113
|
export function defaultUiDistDir() {
|
|
177
114
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
178
115
|
if (path.basename(path.dirname(here)) === "src") {
|
|
@@ -189,494 +126,1136 @@ export function defaultUiDistDir() {
|
|
|
189
126
|
*/
|
|
190
127
|
export function createOperatorRoutes(deps) {
|
|
191
128
|
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
129
|
+
const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
|
|
130
|
+
const controlTokens = deps.controlTokens ?? loadControlTokens();
|
|
131
|
+
return async ({ req, res, url, pathname, method, boot }) => {
|
|
132
|
+
if (boot.serveBlocked !== undefined && pathname.startsWith("/api/")) {
|
|
133
|
+
json(res, 503, {
|
|
134
|
+
error: boot.serveBlocked.reason,
|
|
135
|
+
code: boot.serveBlocked.code,
|
|
136
|
+
});
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (pathname.startsWith("/api/")) {
|
|
140
|
+
if (!assertAllowedHttpAccess(allowedHosts, req, res, {
|
|
141
|
+
requireOrigin: isCredentialMutatingApi(method, pathname),
|
|
142
|
+
})) {
|
|
195
143
|
return true;
|
|
196
144
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
145
|
+
const scope = requiredScopeFor(method, pathname);
|
|
146
|
+
if (scope !== null) {
|
|
147
|
+
const authResult = enforceBearerAuth(controlTokens, req, res, scope);
|
|
148
|
+
if (!authResult.ok)
|
|
149
|
+
return true;
|
|
150
|
+
return runWithRequestAuth(requestAuthFromBearer(authResult.auth, "rest"), () => handleOperatorRequest());
|
|
200
151
|
}
|
|
201
152
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
153
|
+
return handleOperatorRequest();
|
|
154
|
+
async function handleOperatorRequest() {
|
|
155
|
+
try {
|
|
156
|
+
if (method === "GET" && pathname === "/api/runs") {
|
|
157
|
+
const filter = {};
|
|
158
|
+
const status = url.searchParams.get("status");
|
|
159
|
+
const since = url.searchParams.get("since");
|
|
160
|
+
const pipeline = url.searchParams.get("pipeline");
|
|
161
|
+
const callerId = url.searchParams.get("caller_id");
|
|
162
|
+
if (status !== null)
|
|
163
|
+
filter.status = status;
|
|
164
|
+
if (since !== null)
|
|
165
|
+
filter.since = since;
|
|
166
|
+
if (pipeline !== null)
|
|
167
|
+
filter.pipeline = pipeline;
|
|
168
|
+
if (callerId !== null)
|
|
169
|
+
filter.caller_id = callerId;
|
|
170
|
+
json(res, 200, {
|
|
171
|
+
runs: await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined),
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (method === "POST" && pathname === "/api/backup") {
|
|
176
|
+
const body = (await readJsonBody(req));
|
|
177
|
+
const callerId = callerIdFromRequestAuth();
|
|
178
|
+
try {
|
|
179
|
+
const result = await createBackup({
|
|
180
|
+
store,
|
|
181
|
+
homeDir: globalStageflowHome(),
|
|
182
|
+
outPath: typeof body.out === "string" ? body.out : undefined,
|
|
183
|
+
dbOnly: body.db_only === true,
|
|
184
|
+
noCredentials: body.no_credentials === true,
|
|
185
|
+
includeA2aArtifacts: body.include_a2a_artifacts === true,
|
|
186
|
+
});
|
|
187
|
+
writeAudit(auditLog, {
|
|
188
|
+
caller_id: callerId,
|
|
189
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
190
|
+
action: "backup",
|
|
191
|
+
outcome: "ok",
|
|
192
|
+
});
|
|
193
|
+
json(res, 200, result);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (err instanceof BackupError) {
|
|
197
|
+
writeAudit(auditLog, {
|
|
198
|
+
caller_id: callerId,
|
|
199
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
200
|
+
action: "backup",
|
|
201
|
+
outcome: "error",
|
|
202
|
+
error_code: err.code,
|
|
203
|
+
});
|
|
204
|
+
const status = err.code === "backup_insufficient_disk" ? 507 : 400;
|
|
205
|
+
json(res, status, { error: err.message, code: err.code });
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
const backupGetMatch = pathname.match(/^\/api\/backup\/([^/]+)$/);
|
|
213
|
+
if (method === "GET" && backupGetMatch) {
|
|
214
|
+
const name = decodeURIComponent(backupGetMatch[1] ?? "");
|
|
215
|
+
try {
|
|
216
|
+
const filePath = await resolveBackupDownloadPath(name, globalStageflowHome());
|
|
217
|
+
const st = statSync(filePath);
|
|
219
218
|
res.writeHead(200, {
|
|
220
|
-
"Content-Type":
|
|
221
|
-
"Content-Length":
|
|
219
|
+
"Content-Type": "application/octet-stream",
|
|
220
|
+
"Content-Length": st.size,
|
|
221
|
+
"Content-Disposition": `attachment; filename="${name}"`,
|
|
222
222
|
});
|
|
223
|
-
|
|
223
|
+
createReadStream(filePath).pipe(res);
|
|
224
224
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (err instanceof BackupError) {
|
|
227
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
throw err;
|
|
228
231
|
}
|
|
232
|
+
return true;
|
|
229
233
|
}
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
+
if (method === "POST" && pathname === "/api/restore") {
|
|
235
|
+
const body = (await readJsonBody(req));
|
|
236
|
+
const backupName = typeof body.backup === "string"
|
|
237
|
+
? body.backup
|
|
238
|
+
: typeof body.path === "string"
|
|
239
|
+
? body.path
|
|
240
|
+
: undefined;
|
|
241
|
+
if (backupName === undefined) {
|
|
242
|
+
json(res, 400, {
|
|
243
|
+
error: "body.backup (backup name under backups/) is required",
|
|
244
|
+
code: "restore_not_found",
|
|
245
|
+
});
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const callerId = callerIdFromRequestAuth();
|
|
249
|
+
try {
|
|
250
|
+
const archivePath = await resolveBackupNameForRestore(path.basename(backupName), globalStageflowHome());
|
|
251
|
+
const graceMs = parseShutdownGraceMs();
|
|
252
|
+
const staged = await stageRestoreForBoot({
|
|
253
|
+
archivePath,
|
|
254
|
+
homeDir: globalStageflowHome(),
|
|
255
|
+
graceMs,
|
|
256
|
+
});
|
|
257
|
+
writeAudit(auditLog, {
|
|
258
|
+
caller_id: callerId,
|
|
259
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
260
|
+
action: "restore",
|
|
261
|
+
outcome: "ok",
|
|
262
|
+
});
|
|
263
|
+
json(res, 202, {
|
|
264
|
+
ok: true,
|
|
265
|
+
staged: staged.stagedPath,
|
|
266
|
+
drain_deadline: staged.marker.drain_deadline,
|
|
267
|
+
});
|
|
268
|
+
const shutdown = deps.getShutdown?.();
|
|
269
|
+
if (shutdown !== undefined) {
|
|
270
|
+
void shutdown.beginDrain();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (err instanceof RestoreError || err instanceof BackupError) {
|
|
275
|
+
writeAudit(auditLog, {
|
|
276
|
+
caller_id: callerId,
|
|
277
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
278
|
+
action: "restore",
|
|
279
|
+
outcome: "error",
|
|
280
|
+
error_code: err.code,
|
|
281
|
+
});
|
|
282
|
+
json(res, 400, {
|
|
283
|
+
error: err.message,
|
|
284
|
+
code: err.code,
|
|
285
|
+
});
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
234
291
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
292
|
+
if (method === "GET" && pathname === "/api/export") {
|
|
293
|
+
const filter = {};
|
|
294
|
+
const status = url.searchParams.get("status");
|
|
295
|
+
const since = url.searchParams.get("since");
|
|
296
|
+
const pipeline = url.searchParams.get("pipeline");
|
|
297
|
+
const callerId = url.searchParams.get("caller_id");
|
|
298
|
+
if (status)
|
|
299
|
+
filter.status = status;
|
|
300
|
+
if (since)
|
|
301
|
+
filter.since = since;
|
|
302
|
+
if (pipeline)
|
|
303
|
+
filter.pipeline = pipeline;
|
|
304
|
+
if (callerId)
|
|
305
|
+
filter.caller_id = callerId;
|
|
306
|
+
res.writeHead(200, {
|
|
307
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
for await (const line of iterateExportNdjson({
|
|
311
|
+
store,
|
|
312
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
313
|
+
})) {
|
|
314
|
+
if (req.aborted || res.writableEnded || res.destroyed) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
if (!res.write(line)) {
|
|
318
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
322
|
+
res.end();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
if (res.headersSent) {
|
|
327
|
+
res.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
243
334
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
335
|
+
const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
336
|
+
if (method === "GET" && artifactMatch) {
|
|
337
|
+
const runId = decodeURIComponent(artifactMatch[1] ?? "");
|
|
338
|
+
const artifactPath = url.searchParams.get("path");
|
|
339
|
+
if (artifactPath === null || artifactPath.trim().length === 0) {
|
|
340
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const mediaType = artifactMediaType(artifactPath);
|
|
345
|
+
if (mediaType !== undefined) {
|
|
346
|
+
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
347
|
+
res.writeHead(200, {
|
|
348
|
+
"Content-Type": mediaType,
|
|
349
|
+
"Content-Length": bytes.length,
|
|
350
|
+
});
|
|
351
|
+
res.end(bytes);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
355
|
+
textPlain(res, 200, content);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
360
|
+
const status = mapped.kind === "denied" ? 403 : mapped.status;
|
|
361
|
+
json(res, status, { error: mapped.error });
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
247
364
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const rest = pathname.slice("/api/runs/".length);
|
|
252
|
-
if (rest && !rest.includes("/")) {
|
|
365
|
+
const changesMatch = pathname.match(/^\/api\/runs\/([^/]+)\/changes$/);
|
|
366
|
+
if (method === "GET" && changesMatch) {
|
|
367
|
+
const runId = decodeURIComponent(changesMatch[1] ?? "");
|
|
253
368
|
try {
|
|
254
|
-
|
|
369
|
+
const result = await listCheckoutChanges(store, runId);
|
|
370
|
+
if (!result.ok) {
|
|
371
|
+
json(res, result.status, {
|
|
372
|
+
error: result.error,
|
|
373
|
+
code: result.code,
|
|
374
|
+
});
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
json(res, 200, {
|
|
378
|
+
runId,
|
|
379
|
+
changes: result.changes,
|
|
380
|
+
truncated: result.truncated,
|
|
381
|
+
});
|
|
255
382
|
}
|
|
256
383
|
catch (err) {
|
|
257
384
|
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
258
|
-
json(res,
|
|
385
|
+
json(res, mapped.status, { error: mapped.error });
|
|
259
386
|
}
|
|
260
387
|
return true;
|
|
261
388
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
389
|
+
const diffMatch = pathname.match(/^\/api\/runs\/([^/]+)\/diff$/);
|
|
390
|
+
if (method === "GET" && diffMatch) {
|
|
391
|
+
const runId = decodeURIComponent(diffMatch[1] ?? "");
|
|
392
|
+
const modeParam = url.searchParams.get("mode");
|
|
393
|
+
const mode = modeParam === "patch" || modeParam === "stat" ? modeParam : undefined;
|
|
394
|
+
const filterPath = url.searchParams.get("path") ?? undefined;
|
|
395
|
+
const maxBytesRaw = url.searchParams.get("maxBytes");
|
|
396
|
+
const maxBytes = maxBytesRaw !== null && maxBytesRaw.trim() !== ""
|
|
397
|
+
? Number(maxBytesRaw)
|
|
398
|
+
: undefined;
|
|
399
|
+
try {
|
|
400
|
+
const result = await getRunDiff(store, runId, {
|
|
401
|
+
mode,
|
|
402
|
+
path: filterPath ?? undefined,
|
|
403
|
+
maxBytes: maxBytes !== undefined && Number.isFinite(maxBytes)
|
|
404
|
+
? maxBytes
|
|
405
|
+
: undefined,
|
|
406
|
+
});
|
|
407
|
+
if (!result.ok) {
|
|
408
|
+
json(res, result.status, {
|
|
409
|
+
error: result.error,
|
|
410
|
+
code: result.code,
|
|
411
|
+
});
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
json(res, 200, {
|
|
415
|
+
runId,
|
|
416
|
+
mode: result.mode,
|
|
417
|
+
base_sha: result.base_sha,
|
|
418
|
+
base_source: result.base_source,
|
|
419
|
+
content: result.content,
|
|
420
|
+
truncated: result.truncated,
|
|
421
|
+
untracked: result.untracked,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
426
|
+
json(res, mapped.status, { error: mapped.error });
|
|
427
|
+
}
|
|
269
428
|
return true;
|
|
270
429
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
430
|
+
const checkoutFileMatch = pathname.match(/^\/api\/runs\/([^/]+)\/file$/);
|
|
431
|
+
if (method === "GET" && checkoutFileMatch) {
|
|
432
|
+
const runId = decodeURIComponent(checkoutFileMatch[1] ?? "");
|
|
433
|
+
const filePath = url.searchParams.get("path");
|
|
434
|
+
if (filePath === null || filePath.trim().length === 0) {
|
|
435
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const result = await readCheckoutFileBytes(store, runId, filePath);
|
|
440
|
+
if (!result.ok) {
|
|
441
|
+
json(res, result.status, {
|
|
442
|
+
error: result.error,
|
|
443
|
+
code: result.code,
|
|
444
|
+
});
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
const mediaType = artifactMediaType(filePath);
|
|
448
|
+
if (mediaType !== undefined) {
|
|
449
|
+
res.writeHead(200, {
|
|
450
|
+
"Content-Type": mediaType,
|
|
451
|
+
"Content-Length": result.bytes.length,
|
|
452
|
+
});
|
|
453
|
+
res.end(result.bytes);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
const classified = classifyArtifactContent(filePath, result.bytes);
|
|
457
|
+
if (classified.kind !== "utf8") {
|
|
458
|
+
json(res, 400, {
|
|
459
|
+
error: "Checkout file is not valid UTF-8 text",
|
|
460
|
+
});
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
textPlain(res, 200, result.bytes.toString("utf8"));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
468
|
+
const status = mapped.kind === "denied" ? 403 : mapped.status;
|
|
469
|
+
json(res, status, { error: mapped.error });
|
|
470
|
+
}
|
|
275
471
|
return true;
|
|
276
472
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
473
|
+
const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
|
|
474
|
+
if (method === "GET" && verificationMatch) {
|
|
475
|
+
const runId = decodeURIComponent(verificationMatch[1] ?? "");
|
|
476
|
+
const stageId = decodeURIComponent(verificationMatch[2] ?? "");
|
|
477
|
+
try {
|
|
478
|
+
json(res, 200, await readStageVerificationHistory(store, runId, stageId));
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
482
|
+
json(res, mapped.status, { error: mapped.error });
|
|
483
|
+
}
|
|
280
484
|
return true;
|
|
281
485
|
}
|
|
282
|
-
|
|
283
|
-
|
|
486
|
+
const exportMatch = pathname.match(/^\/api\/runs\/([^/]+)\/export$/);
|
|
487
|
+
if (method === "GET" && exportMatch) {
|
|
488
|
+
const runId = decodeURIComponent(exportMatch[1] ?? "");
|
|
489
|
+
try {
|
|
490
|
+
const detail = await store.readRun(runId);
|
|
491
|
+
json(res, 200, buildRunExportPayload(detail));
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
495
|
+
json(res, mapped.status, { error: mapped.error });
|
|
496
|
+
}
|
|
284
497
|
return true;
|
|
285
498
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
499
|
+
const debugBundleMatch = pathname.match(/^\/api\/runs\/([^/]+)\/debug-bundle$/);
|
|
500
|
+
if (method === "GET" && debugBundleMatch) {
|
|
501
|
+
const runId = decodeURIComponent(debugBundleMatch[1] ?? "");
|
|
502
|
+
try {
|
|
503
|
+
json(res, 200, await buildDebugBundle(store, runId));
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
507
|
+
json(res, mapped.status, { error: mapped.error });
|
|
290
508
|
}
|
|
509
|
+
return true;
|
|
291
510
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
511
|
+
if (method === "GET" && pathname.startsWith("/api/runs/")) {
|
|
512
|
+
const rest = pathname.slice("/api/runs/".length);
|
|
513
|
+
if (rest && !rest.includes("/")) {
|
|
514
|
+
try {
|
|
515
|
+
const detail = await store.readRun(decodeURIComponent(rest));
|
|
516
|
+
json(res, 200, runDetailWithRedactedManifest(detail));
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
520
|
+
json(res, 404, { error: mapped.error });
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
305
524
|
}
|
|
306
|
-
|
|
307
|
-
if (
|
|
525
|
+
if (method === "POST" && pathname === "/api/projects") {
|
|
526
|
+
if (!isTrustedLocalHttpRequest(req)) {
|
|
527
|
+
json(res, 403, {
|
|
528
|
+
error: "ensure_project is only allowed from trusted local clients (loopback peer and Host)",
|
|
529
|
+
code: "ensure_project_not_allowed",
|
|
530
|
+
});
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
let body;
|
|
534
|
+
try {
|
|
535
|
+
body = await readJsonBody(req);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
const projectRoot = body !== null &&
|
|
542
|
+
typeof body === "object" &&
|
|
543
|
+
!Array.isArray(body) &&
|
|
544
|
+
typeof body.project_root === "string"
|
|
545
|
+
? body.project_root.trim()
|
|
546
|
+
: "";
|
|
547
|
+
if (!projectRoot) {
|
|
548
|
+
json(res, 400, { error: "project_root is required" });
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
if (!path.isAbsolute(projectRoot)) {
|
|
308
552
|
json(res, 400, {
|
|
309
|
-
error: "
|
|
310
|
-
validation: err.result,
|
|
553
|
+
error: "project_root must be an absolute path",
|
|
311
554
|
});
|
|
312
555
|
return true;
|
|
313
556
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
557
|
+
try {
|
|
558
|
+
const registered = await store.ensureProject(projectRoot);
|
|
559
|
+
json(res, 200, { project_root: registered });
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
563
|
+
json(res, 400, {
|
|
564
|
+
error: message,
|
|
565
|
+
code: "invalid_project_root",
|
|
566
|
+
});
|
|
567
|
+
}
|
|
318
568
|
return true;
|
|
319
569
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
570
|
+
if (method === "POST" && pathname === "/api/runs") {
|
|
571
|
+
const body = (await readJsonBody(req));
|
|
572
|
+
const tokenField = findTokenShapedField(body);
|
|
573
|
+
if (tokenField !== undefined) {
|
|
574
|
+
json(res, 400, tokenRejectedPayload(tokenField));
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
const typed = body;
|
|
578
|
+
if (typeof typed.pipeline !== "string" ||
|
|
579
|
+
!typed.pipeline.trim() ||
|
|
580
|
+
typed.task === undefined) {
|
|
581
|
+
json(res, 400, { error: "task and pipeline are required" });
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
if (typeof typed.task !== "string" && !isTaskFile(typed.task)) {
|
|
585
|
+
json(res, 400, {
|
|
586
|
+
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
587
|
+
});
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
if (typed.checkoutOverride !== undefined &&
|
|
591
|
+
typeof typed.checkoutOverride !== "string") {
|
|
592
|
+
json(res, 400, { error: "checkoutOverride must be a string" });
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
if (typed.skipGates !== undefined && typeof typed.skipGates !== "boolean") {
|
|
596
|
+
json(res, 400, { error: "skipGates must be a boolean" });
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
|
|
600
|
+
if (typed[field] !== undefined && typeof typed[field] !== "string") {
|
|
601
|
+
json(res, 400, { error: `${field} must be a string` });
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
let wireRoot;
|
|
606
|
+
let roots;
|
|
607
|
+
try {
|
|
608
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, typed.project_root));
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
if (err instanceof CatalogPathError) {
|
|
612
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
throw err;
|
|
616
|
+
}
|
|
617
|
+
const wireProjectRoot = wireRoot.project_root;
|
|
618
|
+
let pipelinePath = typed.pipeline.trim();
|
|
619
|
+
let taskInput = typed.task;
|
|
620
|
+
let checkoutOverride;
|
|
621
|
+
try {
|
|
622
|
+
pipelinePath = resolveCatalogRelativePath({
|
|
623
|
+
inputPath: pipelinePath,
|
|
624
|
+
projectRoot: wireProjectRoot,
|
|
625
|
+
roots,
|
|
626
|
+
fieldName: "pipeline",
|
|
627
|
+
}).absolutePath;
|
|
628
|
+
if (typeof typed.task === "string") {
|
|
629
|
+
taskInput = resolveCatalogRelativePath({
|
|
630
|
+
inputPath: typed.task,
|
|
631
|
+
projectRoot: wireProjectRoot,
|
|
632
|
+
roots,
|
|
633
|
+
fieldName: "task",
|
|
634
|
+
}).absolutePath;
|
|
635
|
+
}
|
|
636
|
+
if (typed.checkoutOverride !== undefined) {
|
|
637
|
+
checkoutOverride = resolveCatalogRelativePath({
|
|
638
|
+
inputPath: typed.checkoutOverride,
|
|
639
|
+
projectRoot: wireProjectRoot,
|
|
640
|
+
roots,
|
|
641
|
+
fieldName: "checkout",
|
|
642
|
+
}).absolutePath;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
if (err instanceof CatalogPathError) {
|
|
647
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
652
|
+
let result;
|
|
653
|
+
const callerId = callerIdFromRequestAuth();
|
|
654
|
+
try {
|
|
655
|
+
result = await manager.startRun({
|
|
656
|
+
task: taskInput,
|
|
657
|
+
pipeline: pipelinePath,
|
|
658
|
+
projectRoot: wireRoot.path,
|
|
659
|
+
...(checkoutOverride !== undefined
|
|
660
|
+
? { checkoutOverride }
|
|
661
|
+
: {}),
|
|
662
|
+
...(typed.skipGates !== undefined ? { skipGates: typed.skipGates } : {}),
|
|
663
|
+
...(typed.gitSha !== undefined ? { gitSha: typed.gitSha } : {}),
|
|
664
|
+
...(typed.ciPrUrl !== undefined ? { ciPrUrl: typed.ciPrUrl } : {}),
|
|
665
|
+
...(typed.ciJobUrl !== undefined ? { ciJobUrl: typed.ciJobUrl } : {}),
|
|
666
|
+
...(typed.skills !== undefined ? { skills: typed.skills } : {}),
|
|
667
|
+
callerId,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
if (err instanceof PipelineValidationError) {
|
|
672
|
+
writeAudit(auditLog, {
|
|
673
|
+
caller_id: callerId,
|
|
674
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
675
|
+
action: "start_run",
|
|
676
|
+
outcome: "error",
|
|
677
|
+
error_code: "pipeline_validation",
|
|
678
|
+
});
|
|
679
|
+
json(res, 400, {
|
|
680
|
+
error: "Pipeline validation failed",
|
|
681
|
+
validation: err.result,
|
|
682
|
+
});
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
if (err instanceof PipelinePreflightError) {
|
|
686
|
+
writeAudit(auditLog, {
|
|
687
|
+
caller_id: callerId,
|
|
688
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
689
|
+
action: "start_run",
|
|
690
|
+
outcome: "error",
|
|
691
|
+
error_code: err.code,
|
|
692
|
+
});
|
|
693
|
+
json(res, 400, err.toNetworkBody());
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
throw err;
|
|
697
|
+
}
|
|
698
|
+
if (!result.ok) {
|
|
699
|
+
writeAudit(auditLog, {
|
|
700
|
+
caller_id: callerId,
|
|
701
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
702
|
+
action: "start_run",
|
|
703
|
+
outcome: "error",
|
|
704
|
+
error_code: result.code,
|
|
705
|
+
});
|
|
706
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
writeAudit(auditLog, {
|
|
710
|
+
caller_id: callerId,
|
|
711
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
712
|
+
action: "start_run",
|
|
713
|
+
target_run_id: result.runId,
|
|
714
|
+
outcome: "ok",
|
|
715
|
+
});
|
|
716
|
+
json(res, 202, {
|
|
717
|
+
runId: result.runId,
|
|
718
|
+
...(result.queued === true
|
|
719
|
+
? {
|
|
720
|
+
queued: true,
|
|
721
|
+
queuePosition: result.queuePosition,
|
|
722
|
+
...(result.queuedCode !== undefined
|
|
723
|
+
? { queuedCode: result.queuedCode }
|
|
724
|
+
: {}),
|
|
725
|
+
}
|
|
726
|
+
: {}),
|
|
727
|
+
});
|
|
328
728
|
return true;
|
|
329
729
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
730
|
+
if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
|
|
731
|
+
const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
|
|
732
|
+
const body = (await readJsonBody(req));
|
|
733
|
+
if (body.pinned !== undefined && typeof body.pinned !== "boolean") {
|
|
734
|
+
json(res, 400, { error: "pinned must be a boolean" });
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const callerId = callerIdFromRequestAuth();
|
|
738
|
+
const result = await manager.rerun(runId, {
|
|
739
|
+
...(body.pinned !== undefined ? { pinned: body.pinned } : {}),
|
|
740
|
+
callerId,
|
|
741
|
+
});
|
|
742
|
+
if (!result.ok) {
|
|
743
|
+
writeAudit(auditLog, {
|
|
744
|
+
caller_id: callerId,
|
|
745
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
746
|
+
action: "rerun",
|
|
747
|
+
target_run_id: runId,
|
|
748
|
+
outcome: "error",
|
|
749
|
+
error_code: result.code,
|
|
750
|
+
});
|
|
751
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
writeAudit(auditLog, {
|
|
755
|
+
caller_id: callerId,
|
|
756
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
757
|
+
action: "rerun",
|
|
758
|
+
target_run_id: result.runId,
|
|
759
|
+
outcome: "ok",
|
|
344
760
|
});
|
|
761
|
+
json(res, 202, { runId: result.runId });
|
|
345
762
|
return true;
|
|
346
763
|
}
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
764
|
+
const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
|
|
765
|
+
if (method === "POST" && answerMatch) {
|
|
766
|
+
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
767
|
+
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
768
|
+
let answer;
|
|
769
|
+
try {
|
|
770
|
+
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
json(res, 400, {
|
|
774
|
+
error: err instanceof Error ? err.message : String(err),
|
|
775
|
+
});
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
779
|
+
if (!result.ok) {
|
|
780
|
+
json(res, result.status, { error: result.reason });
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
json(res, 202, { ok: true });
|
|
350
784
|
return true;
|
|
351
785
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
786
|
+
const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
|
|
787
|
+
if (method === "POST" && feedbackDecisionMatch) {
|
|
788
|
+
const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
|
|
789
|
+
const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
|
|
790
|
+
let body;
|
|
791
|
+
try {
|
|
792
|
+
body = await readJsonBody(req);
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
799
|
+
json(res, 400, { error: "Body must be a JSON object" });
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
const raw = body;
|
|
803
|
+
if (raw.decision !== "extend" &&
|
|
804
|
+
raw.decision !== "continue" &&
|
|
805
|
+
raw.decision !== "abandon") {
|
|
806
|
+
json(res, 400, {
|
|
807
|
+
error: "decision must be extend, continue, or abandon",
|
|
808
|
+
});
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
|
|
812
|
+
json(res, 400, { error: "loopId must be a string" });
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
816
|
+
json(res, 400, { error: "reason must be a string" });
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
820
|
+
decision: raw.decision,
|
|
821
|
+
...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
|
|
822
|
+
...(raw.reason !== undefined ? { reason: raw.reason } : {}),
|
|
823
|
+
});
|
|
824
|
+
if (!result.ok) {
|
|
825
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
json(res, 202, {
|
|
829
|
+
ok: true,
|
|
830
|
+
effect: result.effect,
|
|
831
|
+
loopId: result.loopId,
|
|
832
|
+
});
|
|
365
833
|
return true;
|
|
366
834
|
}
|
|
367
|
-
|
|
368
|
-
|
|
835
|
+
const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
|
|
836
|
+
if (method === "POST" && retryMatch) {
|
|
837
|
+
await readJsonBody(req);
|
|
838
|
+
const runId = decodeURIComponent(retryMatch[1] ?? "");
|
|
839
|
+
const stageId = decodeURIComponent(retryMatch[2] ?? "");
|
|
840
|
+
const result = await manager.retryStage(runId, stageId);
|
|
841
|
+
if (!result.ok) {
|
|
842
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
json(res, 202, {
|
|
846
|
+
runId: result.runId,
|
|
847
|
+
stageId: result.stageId,
|
|
848
|
+
attemptIndex: result.attemptIndex,
|
|
849
|
+
});
|
|
369
850
|
return true;
|
|
370
851
|
}
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
852
|
+
const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
|
|
853
|
+
if (method === "POST" && resumeMatch) {
|
|
854
|
+
await readJsonBody(req);
|
|
855
|
+
const runId = decodeURIComponent(resumeMatch[1] ?? "");
|
|
856
|
+
const stageId = decodeURIComponent(resumeMatch[2] ?? "");
|
|
857
|
+
const result = await manager.resumeTimedOutStage(runId, stageId);
|
|
858
|
+
if (!result.ok) {
|
|
859
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
json(res, 202, {
|
|
863
|
+
runId: result.runId,
|
|
864
|
+
stageId: result.stageId,
|
|
865
|
+
attemptIndex: result.attemptIndex,
|
|
377
866
|
});
|
|
378
867
|
return true;
|
|
379
868
|
}
|
|
380
|
-
|
|
381
|
-
|
|
869
|
+
const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
|
|
870
|
+
if (method === "POST" && recoverMatch) {
|
|
871
|
+
let body;
|
|
872
|
+
try {
|
|
873
|
+
body = await readJsonBody(req);
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
880
|
+
? body.guidance
|
|
881
|
+
: undefined;
|
|
882
|
+
if (guidance !== undefined && typeof guidance !== "string") {
|
|
883
|
+
json(res, 400, { error: "guidance must be a string" });
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
const runId = decodeURIComponent(recoverMatch[1] ?? "");
|
|
887
|
+
const stageId = decodeURIComponent(recoverMatch[2] ?? "");
|
|
888
|
+
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
889
|
+
if (!result.ok) {
|
|
890
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
json(res, 202, {
|
|
894
|
+
runId: result.runId,
|
|
895
|
+
stageId: result.stageId,
|
|
896
|
+
attemptIndex: result.attemptIndex,
|
|
897
|
+
});
|
|
382
898
|
return true;
|
|
383
899
|
}
|
|
384
|
-
|
|
385
|
-
|
|
900
|
+
const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
|
|
901
|
+
if (method === "POST" && stopRecoveryMatch) {
|
|
902
|
+
await readJsonBody(req);
|
|
903
|
+
const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
|
|
904
|
+
const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
|
|
905
|
+
const result = await manager.stopManualRecovery(runId, stageId);
|
|
906
|
+
if (!result.ok) {
|
|
907
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
json(res, 202, result);
|
|
386
911
|
return true;
|
|
387
912
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
913
|
+
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
914
|
+
if (method === "POST" && abandonMatch) {
|
|
915
|
+
await readJsonBody(req);
|
|
916
|
+
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
917
|
+
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
918
|
+
const result = await manager.abandonStage(runId, stageId);
|
|
919
|
+
if (!result.ok) {
|
|
920
|
+
json(res, result.status ?? 500, {
|
|
921
|
+
error: result.reason,
|
|
922
|
+
});
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
json(res, 202, {
|
|
926
|
+
ok: true,
|
|
927
|
+
runId: result.runId,
|
|
928
|
+
stageId: result.stageId,
|
|
929
|
+
});
|
|
395
930
|
return true;
|
|
396
931
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
stageId: result.stageId,
|
|
417
|
-
attemptIndex: result.attemptIndex,
|
|
418
|
-
});
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
|
|
422
|
-
if (method === "POST" && resumeMatch) {
|
|
423
|
-
await readJsonBody(req);
|
|
424
|
-
const runId = decodeURIComponent(resumeMatch[1] ?? "");
|
|
425
|
-
const stageId = decodeURIComponent(resumeMatch[2] ?? "");
|
|
426
|
-
const result = await manager.resumeTimedOutStage(runId, stageId);
|
|
427
|
-
if (!result.ok) {
|
|
428
|
-
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
json(res, 202, {
|
|
432
|
-
runId: result.runId,
|
|
433
|
-
stageId: result.stageId,
|
|
434
|
-
attemptIndex: result.attemptIndex,
|
|
435
|
-
});
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
|
|
439
|
-
if (method === "POST" && recoverMatch) {
|
|
440
|
-
let body;
|
|
441
|
-
try {
|
|
442
|
-
body = await readJsonBody(req);
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
932
|
+
const cancelMatch = pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
933
|
+
if (method === "POST" && cancelMatch) {
|
|
934
|
+
const body = (await readJsonBody(req));
|
|
935
|
+
const runId = decodeURIComponent(cancelMatch[1] ?? "");
|
|
936
|
+
if (typeof body.reason !== "string" || body.reason.trim().length === 0) {
|
|
937
|
+
json(res, 400, { error: "Cancel reason is required" });
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
const result = await manager.cancelRun(runId, body.reason);
|
|
941
|
+
if (!result.ok) {
|
|
942
|
+
json(res, result.status ?? 500, {
|
|
943
|
+
error: result.reason,
|
|
944
|
+
});
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
json(res, 202, {
|
|
948
|
+
ok: true,
|
|
949
|
+
runId: result.runId,
|
|
950
|
+
});
|
|
446
951
|
return true;
|
|
447
952
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
953
|
+
if (method === "POST" && pathname === "/api/runs/gc") {
|
|
954
|
+
const body = (await readJsonBody(req));
|
|
955
|
+
const execute = body.execute === true;
|
|
956
|
+
const result = await manager.gcRuns({ execute, channel: "rest" });
|
|
957
|
+
if (!result.ok) {
|
|
958
|
+
json(res, result.status ?? 500, {
|
|
959
|
+
error: result.reason,
|
|
960
|
+
});
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
json(res, 200, {
|
|
964
|
+
slimmed: result.slimmed,
|
|
965
|
+
purged: result.purged,
|
|
966
|
+
bareCachesEvicted: result.bareCachesEvicted,
|
|
967
|
+
});
|
|
453
968
|
return true;
|
|
454
969
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
970
|
+
const deleteMatch = pathname.match(/^\/api\/runs\/([^/]+)$/);
|
|
971
|
+
if (method === "DELETE" && deleteMatch) {
|
|
972
|
+
const runId = decodeURIComponent(deleteMatch[1] ?? "");
|
|
973
|
+
const forceParam = url.searchParams.get("force");
|
|
974
|
+
const force = forceParam === "true" || forceParam === "1";
|
|
975
|
+
const result = await manager.deleteRun(runId, {
|
|
976
|
+
force,
|
|
977
|
+
channel: "rest",
|
|
978
|
+
});
|
|
979
|
+
if (!result.ok) {
|
|
980
|
+
json(res, result.status ?? 500, {
|
|
981
|
+
error: result.reason,
|
|
982
|
+
});
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
json(res, 200, {
|
|
986
|
+
ok: true,
|
|
987
|
+
runId: result.runId,
|
|
988
|
+
});
|
|
460
989
|
return true;
|
|
461
990
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
991
|
+
if (method === "GET" && pathname === "/api/tasks") {
|
|
992
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
993
|
+
const result = await listTasksMultiProject({
|
|
994
|
+
store,
|
|
995
|
+
bootCwd: cwd,
|
|
996
|
+
projectRootFilter: filter,
|
|
997
|
+
});
|
|
998
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
999
|
+
result.items.length === 0) {
|
|
1000
|
+
json(res, 400, {
|
|
1001
|
+
error: result.root_errors[0].message,
|
|
1002
|
+
code: "unknown_project_root",
|
|
1003
|
+
root_errors: result.root_errors,
|
|
1004
|
+
});
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
json(res, 200, {
|
|
1008
|
+
tasks: result.items,
|
|
1009
|
+
root_errors: result.root_errors,
|
|
1010
|
+
});
|
|
477
1011
|
return true;
|
|
478
1012
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1013
|
+
if (method === "GET" && pathname === "/api/pipelines") {
|
|
1014
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1015
|
+
const result = await listPipelinesMultiProject({
|
|
1016
|
+
store,
|
|
1017
|
+
bootCwd: cwd,
|
|
1018
|
+
projectRootFilter: filter,
|
|
1019
|
+
});
|
|
1020
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1021
|
+
result.items.length === 0) {
|
|
1022
|
+
json(res, 400, {
|
|
1023
|
+
error: result.root_errors[0].message,
|
|
1024
|
+
code: "unknown_project_root",
|
|
1025
|
+
root_errors: result.root_errors,
|
|
1026
|
+
});
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
json(res, 200, {
|
|
1030
|
+
pipelines: result.items,
|
|
1031
|
+
root_errors: result.root_errors,
|
|
491
1032
|
});
|
|
492
1033
|
return true;
|
|
493
1034
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
if (method === "GET" && pathname === "/api/tasks") {
|
|
502
|
-
const catalog = await browseCatalog(cwd);
|
|
503
|
-
json(res, 200, { tasks: catalog.tasks });
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
if (method === "GET" && pathname === "/api/pipelines") {
|
|
507
|
-
const catalog = await browseCatalog(cwd);
|
|
508
|
-
json(res, 200, { pipelines: catalog.pipelines });
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
if (method === "GET" && pathname === "/api/stages") {
|
|
512
|
-
json(res, 404, {
|
|
513
|
-
error: "Global stage library removed; stages are pipeline-scoped",
|
|
514
|
-
});
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
if (method === "POST" && pathname === "/api/stages") {
|
|
518
|
-
let body;
|
|
519
|
-
try {
|
|
520
|
-
body = await readJsonBody(req);
|
|
1035
|
+
if (method === "GET" && pathname === "/api/stages") {
|
|
1036
|
+
json(res, 404, {
|
|
1037
|
+
error: "Global stage library removed; stages are pipeline-scoped",
|
|
1038
|
+
});
|
|
1039
|
+
return true;
|
|
521
1040
|
}
|
|
522
|
-
|
|
523
|
-
|
|
1041
|
+
if (method === "POST" && pathname === "/api/stages") {
|
|
1042
|
+
let body;
|
|
1043
|
+
try {
|
|
1044
|
+
body = await readJsonBody(req);
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
const writeRoot = body !== null &&
|
|
1051
|
+
typeof body === "object" &&
|
|
1052
|
+
!Array.isArray(body) &&
|
|
1053
|
+
typeof body.project_root === "string"
|
|
1054
|
+
? body.project_root
|
|
1055
|
+
: undefined;
|
|
1056
|
+
let stageWriteRoot;
|
|
1057
|
+
try {
|
|
1058
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1059
|
+
stageWriteRoot = selected.path;
|
|
1060
|
+
}
|
|
1061
|
+
catch (err) {
|
|
1062
|
+
if (err instanceof CatalogPathError) {
|
|
1063
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
1068
|
+
const parsed = parseCreateStageBody(body);
|
|
1069
|
+
if ("ok" in parsed) {
|
|
1070
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
const ctx = await resolveStageflowContext(stageWriteRoot);
|
|
1074
|
+
if (!ctx.isGitProject) {
|
|
1075
|
+
json(res, 400, {
|
|
1076
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1077
|
+
});
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
const result = await createStage(ctx.projectRoot, parsed);
|
|
1081
|
+
if (!result.ok) {
|
|
1082
|
+
json(res, result.status, { error: result.error });
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
json(res, 201, result.stage);
|
|
524
1086
|
return true;
|
|
525
1087
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1088
|
+
if (method === "POST" && pathname === "/api/pipelines") {
|
|
1089
|
+
let body;
|
|
1090
|
+
try {
|
|
1091
|
+
body = await readJsonBody(req);
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
const writeRoot = body !== null &&
|
|
1098
|
+
typeof body === "object" &&
|
|
1099
|
+
!Array.isArray(body) &&
|
|
1100
|
+
typeof body.project_root === "string"
|
|
1101
|
+
? body.project_root
|
|
1102
|
+
: undefined;
|
|
1103
|
+
let pipelineWriteRoot;
|
|
1104
|
+
try {
|
|
1105
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1106
|
+
pipelineWriteRoot = selected.path;
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
if (err instanceof CatalogPathError) {
|
|
1110
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
throw err;
|
|
1114
|
+
}
|
|
1115
|
+
const parsed = parseCreatePipelineBody(body);
|
|
1116
|
+
if ("ok" in parsed) {
|
|
1117
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
const ctx = await resolveStageflowContext(pipelineWriteRoot);
|
|
1121
|
+
if (!ctx.isGitProject) {
|
|
1122
|
+
json(res, 400, {
|
|
1123
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1124
|
+
});
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
const result = await createPipeline(ctx.projectRoot, parsed);
|
|
1128
|
+
if (!result.ok) {
|
|
1129
|
+
json(res, result.status, { error: result.error });
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
json(res, 201, result.pipeline);
|
|
529
1133
|
return true;
|
|
530
1134
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1135
|
+
if (method === "GET" && pathname === "/api/models") {
|
|
1136
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1137
|
+
const result = await listModelsMultiProject({
|
|
1138
|
+
store,
|
|
1139
|
+
bootCwd: cwd,
|
|
1140
|
+
projectRootFilter: filter,
|
|
1141
|
+
});
|
|
1142
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1143
|
+
result.items.length === 0) {
|
|
1144
|
+
json(res, 400, {
|
|
1145
|
+
error: result.root_errors[0].message,
|
|
1146
|
+
code: "unknown_project_root",
|
|
1147
|
+
root_errors: result.root_errors,
|
|
1148
|
+
});
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
json(res, 200, {
|
|
1152
|
+
models: result.models,
|
|
1153
|
+
entries: result.items,
|
|
1154
|
+
root_errors: result.root_errors,
|
|
535
1155
|
});
|
|
536
1156
|
return true;
|
|
537
1157
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1158
|
+
if (await handleProviderRoutes(req, res, {
|
|
1159
|
+
cwd,
|
|
1160
|
+
readJsonBody,
|
|
1161
|
+
json,
|
|
1162
|
+
providerAuthContext,
|
|
1163
|
+
})) {
|
|
541
1164
|
return true;
|
|
542
1165
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
let body;
|
|
548
|
-
try {
|
|
549
|
-
body = await readJsonBody(req);
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
1166
|
+
if (await handleProjectMcpRoutes(req, res, {
|
|
1167
|
+
projectRoot: rootDir,
|
|
1168
|
+
json,
|
|
1169
|
+
})) {
|
|
553
1170
|
return true;
|
|
554
1171
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
json(res, parsed.status, { error: parsed.error });
|
|
1172
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
1173
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
558
1174
|
return true;
|
|
559
1175
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
json(res, 400, {
|
|
563
|
-
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
564
|
-
});
|
|
1176
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
1177
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
565
1178
|
return true;
|
|
566
1179
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
json(res, result.status, { error: result.error });
|
|
1180
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
1181
|
+
await handleApiHealth(req, res, boot);
|
|
570
1182
|
return true;
|
|
571
1183
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
if (await handleProviderRoutes(req, res, {
|
|
581
|
-
cwd,
|
|
582
|
-
readJsonBody,
|
|
583
|
-
json,
|
|
584
|
-
providerAuthContext,
|
|
585
|
-
})) {
|
|
586
|
-
return true;
|
|
587
|
-
}
|
|
588
|
-
if (await handleProjectMcpRoutes(req, res, {
|
|
589
|
-
projectRoot: rootDir,
|
|
590
|
-
json,
|
|
591
|
-
})) {
|
|
592
|
-
return true;
|
|
593
|
-
}
|
|
594
|
-
if (method === "GET" && pathname === "/api/skills") {
|
|
595
|
-
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
if (method === "GET" && pathname === "/api/extensions") {
|
|
599
|
-
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
if (method === "GET" && pathname === "/api/health") {
|
|
603
|
-
json(res, 200, manager.getHealth());
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
if (method === "GET" && pathname === "/api/settings") {
|
|
607
|
-
const health = manager.getHealth();
|
|
608
|
-
const credential = getCredentialSourceSettings(cwd);
|
|
609
|
-
json(res, 200, {
|
|
610
|
-
maxConcurrent: health.maxConcurrent,
|
|
611
|
-
...credential,
|
|
612
|
-
});
|
|
613
|
-
return true;
|
|
614
|
-
}
|
|
615
|
-
if (method === "POST" && pathname === "/api/settings") {
|
|
616
|
-
let body;
|
|
617
|
-
try {
|
|
618
|
-
body = await readJsonBody(req);
|
|
619
|
-
}
|
|
620
|
-
catch {
|
|
621
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
622
|
-
return true;
|
|
623
|
-
}
|
|
624
|
-
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
625
|
-
? body
|
|
626
|
-
: {};
|
|
627
|
-
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
628
|
-
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
629
|
-
if (!hasMax && !hasCredentialSource) {
|
|
630
|
-
json(res, 400, {
|
|
631
|
-
error: "maxConcurrent or credentialSource is required",
|
|
1184
|
+
if (method === "GET" && pathname === "/api/settings") {
|
|
1185
|
+
const health = manager.getHealth();
|
|
1186
|
+
const credential = getCredentialSourceSettings(cwd);
|
|
1187
|
+
json(res, 200, {
|
|
1188
|
+
maxConcurrent: health.maxConcurrent,
|
|
1189
|
+
...credential,
|
|
632
1190
|
});
|
|
633
1191
|
return true;
|
|
634
1192
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const n = parseSlotCount(record.maxConcurrent);
|
|
638
|
-
if (n === undefined) {
|
|
639
|
-
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
640
|
-
return true;
|
|
641
|
-
}
|
|
642
|
-
health = manager.setMaxConcurrent(n);
|
|
643
|
-
}
|
|
644
|
-
let credential = getCredentialSourceSettings(cwd);
|
|
645
|
-
if (hasCredentialSource) {
|
|
1193
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
1194
|
+
let body;
|
|
646
1195
|
try {
|
|
647
|
-
|
|
1196
|
+
body = await readJsonBody(req);
|
|
648
1197
|
}
|
|
649
|
-
catch
|
|
650
|
-
|
|
651
|
-
json(res, mapped.status, mapped.body);
|
|
1198
|
+
catch {
|
|
1199
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
652
1200
|
return true;
|
|
653
1201
|
}
|
|
1202
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
1203
|
+
? body
|
|
1204
|
+
: {};
|
|
1205
|
+
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
1206
|
+
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
1207
|
+
if (!hasMax && !hasCredentialSource) {
|
|
1208
|
+
json(res, 400, {
|
|
1209
|
+
error: "maxConcurrent or credentialSource is required",
|
|
1210
|
+
});
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
let health = manager.getHealth();
|
|
1214
|
+
if (hasMax) {
|
|
1215
|
+
const n = parseSlotCount(record.maxConcurrent);
|
|
1216
|
+
if (n === undefined) {
|
|
1217
|
+
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
health = manager.setMaxConcurrent(n);
|
|
1221
|
+
}
|
|
1222
|
+
let credential = getCredentialSourceSettings(cwd);
|
|
1223
|
+
if (hasCredentialSource) {
|
|
1224
|
+
try {
|
|
1225
|
+
credential = setCredentialSource(cwd, record.credentialSource);
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
const mapped = mapProviderAuthError(err);
|
|
1229
|
+
json(res, mapped.status, mapped.body);
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
json(res, 200, {
|
|
1234
|
+
...health,
|
|
1235
|
+
...credential,
|
|
1236
|
+
});
|
|
1237
|
+
return true;
|
|
654
1238
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const served = await serveStatic(res, uiDistDir, pathname);
|
|
663
|
-
if (served)
|
|
1239
|
+
if (method === "GET" && uiDistDir !== undefined) {
|
|
1240
|
+
const served = await serveStatic(res, uiDistDir, pathname);
|
|
1241
|
+
if (served)
|
|
1242
|
+
return true;
|
|
1243
|
+
json(res, 404, {
|
|
1244
|
+
error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
|
|
1245
|
+
});
|
|
664
1246
|
return true;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
});
|
|
1247
|
+
}
|
|
1248
|
+
json(res, 404, { error: "Not found" });
|
|
668
1249
|
return true;
|
|
669
1250
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
});
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
if (!res.headersSent) {
|
|
1253
|
+
json(res, 500, {
|
|
1254
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
return true;
|
|
678
1258
|
}
|
|
679
|
-
return true;
|
|
680
1259
|
}
|
|
681
1260
|
};
|
|
682
1261
|
}
|
|
@@ -685,21 +1264,42 @@ export async function startUiServer(options) {
|
|
|
685
1264
|
const port = options.port ?? DEFAULT_PORT;
|
|
686
1265
|
const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
|
|
687
1266
|
const boot = await bootstrapStageflowHost(options);
|
|
688
|
-
const {
|
|
1267
|
+
const { cwd, agentDir, rootDir } = boot;
|
|
689
1268
|
const providerAuthContext = boot.providerAuthContext;
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1269
|
+
const allowedHosts = options.allowedHosts ?? resolveAllowedHosts();
|
|
1270
|
+
const controlTokens = options.controlTokens ?? loadControlTokens();
|
|
1271
|
+
let shutdown;
|
|
1272
|
+
const routes = boot.serveBlocked !== undefined ||
|
|
1273
|
+
boot.manager === undefined ||
|
|
1274
|
+
boot.store === undefined
|
|
1275
|
+
? async () => false
|
|
1276
|
+
: createOperatorRoutes({
|
|
1277
|
+
manager: boot.manager,
|
|
1278
|
+
store: boot.store,
|
|
697
1279
|
cwd,
|
|
698
1280
|
agentDir,
|
|
699
1281
|
rootDir,
|
|
700
1282
|
providerAuthContext,
|
|
701
1283
|
uiDistDir,
|
|
702
|
-
|
|
1284
|
+
allowedHosts,
|
|
1285
|
+
controlTokens,
|
|
1286
|
+
getShutdown: () => shutdown,
|
|
1287
|
+
});
|
|
1288
|
+
const envelope = await createHttpHost({
|
|
1289
|
+
boot,
|
|
1290
|
+
host,
|
|
1291
|
+
port,
|
|
1292
|
+
allowedHosts,
|
|
1293
|
+
controlTokens,
|
|
1294
|
+
routes,
|
|
1295
|
+
});
|
|
1296
|
+
shutdown = installShutdownController({
|
|
1297
|
+
server: envelope.server,
|
|
1298
|
+
host: makeDrainableHostFromOptional(envelope.manager, envelope.store),
|
|
1299
|
+
});
|
|
1300
|
+
envelope.server.on("close", () => {
|
|
1301
|
+
shutdown?.uninstall();
|
|
703
1302
|
});
|
|
1303
|
+
return { ...envelope, shutdown };
|
|
704
1304
|
}
|
|
705
1305
|
export { DEFAULT_PORT };
|