stageflow 0.25.0 → 0.30.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 +14 -3
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/a2a/contracts.d.ts +1 -0
- package/dist/a2a/contracts.js +1 -0
- package/dist/a2a/registry.d.ts +2 -0
- package/dist/a2a/registry.js +20 -1
- package/dist/a2a/server.d.ts +7 -0
- package/dist/a2a/server.js +5 -2
- package/dist/a2a/service.d.ts +1 -1
- package/dist/a2a/service.js +57 -8
- package/dist/a2a/store.d.ts +8 -5
- package/dist/a2a/store.js +24 -67
- 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/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +2 -0
- package/dist/agent/cursorProvider.js +30 -5
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +11 -24
- package/dist/agent/piAdapter.js +107 -50
- package/dist/agent/piIsolatedMcp.d.ts +2 -0
- package/dist/agent/piIsolatedMcp.js +10 -2
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +8 -0
- package/dist/agent/streamLogRedact.d.ts +7 -1
- package/dist/agent/streamLogRedact.js +26 -11
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -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/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -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 +41 -3
- package/dist/cli/hostClient.js +129 -14
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +32 -2
- 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 +1 -0
- package/dist/cli/runStageCommand.js +44 -25
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +168 -10
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +202 -32
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +63 -0
- package/dist/config/catalogRelativePath.js +126 -0
- package/dist/config/configOrigin.d.ts +27 -0
- package/dist/config/configOrigin.js +59 -0
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +57 -0
- package/dist/config/hostConfig.js +360 -0
- package/dist/config/listSkills.d.ts +26 -0
- package/dist/config/listSkills.js +206 -5
- package/dist/config/loadOutcome.d.ts +7 -1
- package/dist/config/loadPipeline.js +39 -2
- package/dist/config/loadStage.js +25 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTask.d.ts +2 -0
- package/dist/config/loadTask.js +57 -18
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- 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 +32 -0
- package/dist/config/multiProjectCatalog.js +150 -0
- package/dist/config/normalizePipelineStageEntry.js +27 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +3 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- 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/resolvePipelineDag.js +1 -1
- package/dist/config/resolveStageMcpServers.d.ts +9 -1
- package/dist/config/resolveStageMcpServers.js +36 -9
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- 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/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/toolRequires.d.ts +24 -0
- package/dist/config/toolRequires.js +115 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +17 -3
- package/dist/config/validateCatalog.js +223 -3
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/diagnostics/checks.d.ts +39 -0
- package/dist/diagnostics/checks.js +135 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -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 +17 -0
- package/dist/logging/redact.js +105 -0
- package/dist/mcp/catalogTools.js +492 -81
- package/dist/mcp/checkoutTools.d.ts +52 -0
- package/dist/mcp/checkoutTools.js +332 -0
- package/dist/mcp/controlTools.js +182 -6
- package/dist/mcp/deps.d.ts +3 -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 +2 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/net/proxy.d.ts +14 -0
- package/dist/net/proxy.js +78 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -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/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -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.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +46 -30
- 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 +25 -0
- package/dist/runstore/backup.d.ts +52 -0
- package/dist/runstore/backup.js +292 -0
- package/dist/runstore/createStore.d.ts +18 -1
- package/dist/runstore/createStore.js +64 -5
- 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 +139 -13
- 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 +35 -3
- package/dist/runstore/sqlite/SqliteRunStore.js +442 -219
- 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/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
- package/dist/runstore/sqlite/migrations/index.js +78 -0
- package/dist/runstore/sqlite/schema.d.ts +20 -1
- package/dist/runstore/sqlite/schema.js +277 -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/trackProjection.js +2 -0
- package/dist/runstore/workspaceLayout.d.ts +13 -0
- package/dist/runstore/workspaceLayout.js +76 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +15 -1
- package/dist/runtime/completionCheckRunner.js +67 -7
- package/dist/runtime/containerLimits.d.ts +16 -0
- package/dist/runtime/containerLimits.js +84 -0
- package/dist/runtime/credentialBinding.d.ts +2 -0
- package/dist/runtime/credentialBinding.js +3 -0
- package/dist/runtime/credentialMaterialisation.d.ts +28 -0
- package/dist/runtime/credentialMaterialisation.js +128 -0
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/gitCheckoutCapability.js +7 -13
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +40 -1
- package/dist/runtime/pipelineRunner.js +181 -17
- package/dist/runtime/pipelineScheduler.d.ts +17 -0
- package/dist/runtime/pipelineScheduler.js +220 -28
- package/dist/runtime/reloadRunCatalog.js +6 -1
- package/dist/runtime/repositoryMaterialize.d.ts +47 -0
- package/dist/runtime/repositoryMaterialize.js +235 -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 +169 -4
- package/dist/runtime/runManager.js +1748 -83
- package/dist/runtime/runRetentionSweep.d.ts +31 -0
- package/dist/runtime/runRetentionSweep.js +328 -0
- package/dist/runtime/runRetryCoordinator.d.ts +4 -0
- package/dist/runtime/runRetryCoordinator.js +20 -2
- package/dist/runtime/runSkills.d.ts +40 -0
- package/dist/runtime/runSkills.js +173 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +5 -0
- package/dist/runtime/settingsFile.js +61 -54
- package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
- package/dist/runtime/stageAttemptBootstrap.js +250 -27
- 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 +194 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +20 -0
- package/dist/runtime/stageProcessLauncher.js +236 -41
- 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 +6 -0
- package/dist/runtime/stageRunner.js +32 -6
- 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/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- 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/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +8 -0
- package/dist/runtime/verifiedStageExecution.js +16 -1
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- 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 +42 -2
- package/dist/server/bootstrap.js +273 -5
- package/dist/server/controlToken.d.ts +47 -0
- package/dist/server/controlToken.js +254 -0
- package/dist/server/createHttpHost.d.ts +11 -2
- package/dist/server/createHttpHost.js +106 -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 +123 -0
- package/dist/server/http.d.ts +27 -1
- package/dist/server/http.js +2254 -477
- 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/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +10 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +22 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/task.d.ts +8 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-CntB4bZR.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +99 -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/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -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 +145 -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/stageflow.yaml +51 -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 +9 -1
- package/skills/stageflow/SKILL.md +6 -6
- package/skills/stageflow/references/control-surface.md +13 -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-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +22 -12
- package/skills/stageflow-run/references/mcp-call.md +9 -1
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +19 -6
- 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 +51 -8
- package/dist/ui/assets/index-Da5EHU6M.js +0 -119
package/dist/server/http.js
CHANGED
|
@@ -1,25 +1,54 @@
|
|
|
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";
|
|
5
6
|
import { mapProviderAuthError } from "../agent/providerInspect.js";
|
|
6
7
|
import { handleProviderRoutes } from "./providerRoutes.js";
|
|
7
8
|
import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
|
|
9
|
+
import { handleTriggerRoutes } from "./triggerRoutes.js";
|
|
8
10
|
import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
|
|
9
11
|
import { createStage, parseCreateStageBody } from "../config/createStage.js";
|
|
10
|
-
import {
|
|
12
|
+
import { loadDraftPackage, loadTaskArtifact, parseAttachTaskBody, parseCreateDraftPackageBody, parseDraftPackageBody, parseOpenDraftPackageBody, parseOverwriteDraftPackageBody, validateDraftPackage, } from "../config/draftPackage.js";
|
|
13
|
+
import { publishDraftPackage } from "../config/publishDraftPackage.js";
|
|
14
|
+
import { clearWorkshopAutosave, detectDiskChange, draftPackageDiskRelativePaths, fingerprintPackageFiles, parseWorkshopAutosaveRecord, readWorkshopAutosave, resolveWorkshopAutosaveStoreRoot, workshopAutosaveSlotKey, writeWorkshopAutosave, } from "../workshop/autosave.js";
|
|
15
|
+
import { acceptWorkshopSessionMutation, iterateWorkshopChatStreamFrames, runWorkshopChatTurn, stopWorkshopChatTurn, undoWorkshopSessionMutation, WorkshopChatSessionRegistry, WorkshopSessionStoreError, } from "../workshop/chatTurn.js";
|
|
16
|
+
import { readWorkshopOpenModel, resolveWorkshopModel, } from "../workshop/modelSettings.js";
|
|
17
|
+
import { createWorkshopSession, getWorkshopSession, listWorkshopSessions, resolveWorkshopSessionStoreRoot, updateWorkshopSessionActiveBuildId, } from "../workshop/sessionStore.js";
|
|
18
|
+
import { createWorkshopBuild, focusUnboundWorkshopBuild, getWorkshopBuild, listWorkshopBuilds, listWorkshopPickerRows, resolvePickerCatalogPipelines, resolveWorkshopBuildStoreRoot, updateWorkshopBuild, WorkshopBuildStoreError, } from "../workshop/buildStore.js";
|
|
19
|
+
import { createLiveWorkshopOperatorHost, } from "../operatorAgent/index.js";
|
|
20
|
+
import { listModelsMultiProject, listPipelinesMultiProject, listTasksMultiProject, } from "../config/multiProjectCatalog.js";
|
|
21
|
+
import { catalogPathErrorBody, CatalogPathError, resolveCatalogRelativePath, resolveCatalogStartInput, resolveWritableCatalogRoot, } from "../config/catalogRelativePath.js";
|
|
11
22
|
import { listExtensions } from "../config/listExtensions.js";
|
|
12
23
|
import { listSkills } from "../config/listSkills.js";
|
|
13
|
-
import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
|
|
24
|
+
import { artifactMediaType, classifyArtifactContent, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
|
|
25
|
+
import { getRunDiff, listCheckoutChanges, readCheckoutFileBytes, } from "../mcp/checkoutTools.js";
|
|
14
26
|
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
27
|
+
import { BackupError, createBackup, resolveBackupDownloadPath, } from "../runstore/backup.js";
|
|
28
|
+
import { RestoreError, resolveBackupNameForRestore, stageRestoreForBoot, } from "../runstore/restore.js";
|
|
29
|
+
import { iterateExportNdjson } from "../cli/exportAllCommand.js";
|
|
30
|
+
import { buildRunExportPayload, runDetailWithRedactedManifest, } from "../runstore/exportRunPayload.js";
|
|
31
|
+
import { buildDebugBundle } from "../runstore/debugBundle.js";
|
|
32
|
+
import { parseShutdownGraceMs } from "./shutdown.js";
|
|
33
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
15
34
|
import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
|
|
16
35
|
import { PipelineValidationError } from "../runtime/pipelineValidationError.js";
|
|
17
|
-
import {
|
|
36
|
+
import { PipelinePreflightError } from "../runtime/pipelineRunner.js";
|
|
37
|
+
import { INVALID_SLOT_COUNT_MESSAGE, INVALID_WORKSHOP_MODEL_MESSAGE, parseSlotCount, parseWorkshopModelSetting, readFactorySettings, writeFactorySettings, } from "../runtime/settingsFile.js";
|
|
18
38
|
import { isTaskFile } from "../runtime/taskInput.js";
|
|
39
|
+
import { findTokenShapedField, tokenRejectedPayload, } from "../runtime/startPayload.js";
|
|
19
40
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
20
41
|
import { bootstrapStageflowHost, } from "./bootstrap.js";
|
|
42
|
+
import { assertAllowedHttpAccess, isTrustedLocalHttpRequest, resolveAllowedHosts, } from "./allowedHosts.js";
|
|
43
|
+
import { enforceBearerAuth, loadControlTokens, requiredScopeFor, } from "./controlToken.js";
|
|
21
44
|
import { createHttpHost, DEFAULT_PORT, json, } from "./createHttpHost.js";
|
|
45
|
+
import { handleApiHealth } from "./healthSurfaces.js";
|
|
22
46
|
import { mapRetryStageFailure, mapStartFailure, mapStoreLookupError, } from "./operatorResults.js";
|
|
47
|
+
import { installShutdownController, makeDrainableHostFromOptional, } from "./shutdown.js";
|
|
48
|
+
import { writeAudit } from "../logging/audit.js";
|
|
49
|
+
import { logger as rootLogger } from "../logging/logger.js";
|
|
50
|
+
import { callerIdFromRequestAuth, getRequestAuth, requestAuthFromBearer, runWithRequestAuth, } from "./requestAuthContext.js";
|
|
51
|
+
const auditLog = rootLogger.child({ component: "audit" });
|
|
23
52
|
function textPlain(res, status, body) {
|
|
24
53
|
res.writeHead(status, {
|
|
25
54
|
"Content-Type": "text/plain; charset=utf-8",
|
|
@@ -36,6 +65,28 @@ async function readJsonBody(req) {
|
|
|
36
65
|
return {};
|
|
37
66
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
38
67
|
}
|
|
68
|
+
function isPlainObject(value) {
|
|
69
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
function readBuildTieField(body, field) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(body, field)) {
|
|
73
|
+
return { ok: true, present: false };
|
|
74
|
+
}
|
|
75
|
+
const value = body[field];
|
|
76
|
+
if (value === null)
|
|
77
|
+
return { ok: true, present: true, value: null };
|
|
78
|
+
if (typeof value === "string")
|
|
79
|
+
return { ok: true, present: true, value };
|
|
80
|
+
return { ok: false, error: `${field} must be a string or null` };
|
|
81
|
+
}
|
|
82
|
+
/** Raw bytes, unparsed — for callers (webhook signature verification) that need the exact wire body. */
|
|
83
|
+
async function readRawBody(req) {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
for await (const chunk of req) {
|
|
86
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
87
|
+
}
|
|
88
|
+
return Buffer.concat(chunks);
|
|
89
|
+
}
|
|
39
90
|
function contentTypeFor(filePath) {
|
|
40
91
|
const ext = path.extname(filePath).toLowerCase();
|
|
41
92
|
switch (ext) {
|
|
@@ -82,27 +133,6 @@ async function serveStatic(res, uiDistDir, urlPath) {
|
|
|
82
133
|
res.end(data);
|
|
83
134
|
return true;
|
|
84
135
|
}
|
|
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
136
|
function isCredentialMutatingApi(method, pathname) {
|
|
107
137
|
if (method !== "POST")
|
|
108
138
|
return false;
|
|
@@ -111,68 +141,6 @@ function isCredentialMutatingApi(method, pathname) {
|
|
|
111
141
|
/^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
|
|
112
142
|
/^\/api\/providers\/[^/]+\/logout$/.test(pathname));
|
|
113
143
|
}
|
|
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
144
|
export function defaultUiDistDir() {
|
|
177
145
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
178
146
|
if (path.basename(path.dirname(here)) === "src") {
|
|
@@ -188,495 +156,2280 @@ export function defaultUiDistDir() {
|
|
|
188
156
|
* API; only the former needs the UI files.
|
|
189
157
|
*/
|
|
190
158
|
export function createOperatorRoutes(deps) {
|
|
191
|
-
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir } = deps;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
159
|
+
const { manager, store, cwd, agentDir, rootDir, providerAuthContext, uiDistDir, workshopOperatorHost, } = deps;
|
|
160
|
+
const workshopChatRegistry = deps.workshopChatRegistry ??
|
|
161
|
+
new WorkshopChatSessionRegistry(workshopOperatorHost ??
|
|
162
|
+
createLiveWorkshopOperatorHost({
|
|
163
|
+
cwd,
|
|
164
|
+
projectRoot: cwd,
|
|
165
|
+
}));
|
|
166
|
+
const workshopSessionStoreRoot = () => resolveWorkshopSessionStoreRoot();
|
|
167
|
+
const workshopBuildStoreRoot = () => resolveWorkshopBuildStoreRoot();
|
|
168
|
+
const allowedHosts = deps.allowedHosts ?? resolveAllowedHosts();
|
|
169
|
+
const controlTokens = deps.controlTokens ?? loadControlTokens();
|
|
170
|
+
return async ({ req, res, url, pathname, method, boot }) => {
|
|
171
|
+
if (boot.serveBlocked !== undefined && pathname.startsWith("/api/")) {
|
|
172
|
+
json(res, 503, {
|
|
173
|
+
error: boot.serveBlocked.reason,
|
|
174
|
+
code: boot.serveBlocked.code,
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (pathname.startsWith("/api/")) {
|
|
179
|
+
if (!assertAllowedHttpAccess(allowedHosts, req, res, {
|
|
180
|
+
requireOrigin: isCredentialMutatingApi(method, pathname),
|
|
181
|
+
})) {
|
|
195
182
|
return true;
|
|
196
183
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
184
|
+
const scope = requiredScopeFor(method, pathname);
|
|
185
|
+
if (scope !== null) {
|
|
186
|
+
const authResult = enforceBearerAuth(controlTokens, req, res, scope);
|
|
187
|
+
if (!authResult.ok)
|
|
188
|
+
return true;
|
|
189
|
+
return runWithRequestAuth(requestAuthFromBearer(authResult.auth, "rest"), () => handleOperatorRequest());
|
|
200
190
|
}
|
|
201
191
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
192
|
+
return handleOperatorRequest();
|
|
193
|
+
async function handleOperatorRequest() {
|
|
194
|
+
try {
|
|
195
|
+
if (method === "GET" && pathname === "/api/runs") {
|
|
196
|
+
const filter = {};
|
|
197
|
+
const status = url.searchParams.get("status");
|
|
198
|
+
const since = url.searchParams.get("since");
|
|
199
|
+
const pipeline = url.searchParams.get("pipeline");
|
|
200
|
+
const callerId = url.searchParams.get("caller_id");
|
|
201
|
+
if (status !== null)
|
|
202
|
+
filter.status = status;
|
|
203
|
+
if (since !== null)
|
|
204
|
+
filter.since = since;
|
|
205
|
+
if (pipeline !== null)
|
|
206
|
+
filter.pipeline = pipeline;
|
|
207
|
+
if (callerId !== null)
|
|
208
|
+
filter.caller_id = callerId;
|
|
209
|
+
json(res, 200, {
|
|
210
|
+
runs: await store.listRuns(Object.keys(filter).length > 0 ? filter : undefined),
|
|
211
|
+
});
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (method === "POST" && pathname === "/api/backup") {
|
|
215
|
+
const body = (await readJsonBody(req));
|
|
216
|
+
const callerId = callerIdFromRequestAuth();
|
|
217
|
+
try {
|
|
218
|
+
const result = await createBackup({
|
|
219
|
+
store,
|
|
220
|
+
homeDir: globalStageflowHome(),
|
|
221
|
+
outPath: typeof body.out === "string" ? body.out : undefined,
|
|
222
|
+
dbOnly: body.db_only === true,
|
|
223
|
+
noCredentials: body.no_credentials === true,
|
|
224
|
+
includeA2aArtifacts: body.include_a2a_artifacts === true,
|
|
225
|
+
});
|
|
226
|
+
writeAudit(auditLog, {
|
|
227
|
+
caller_id: callerId,
|
|
228
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
229
|
+
action: "backup",
|
|
230
|
+
outcome: "ok",
|
|
231
|
+
});
|
|
232
|
+
json(res, 200, result);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof BackupError) {
|
|
236
|
+
writeAudit(auditLog, {
|
|
237
|
+
caller_id: callerId,
|
|
238
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
239
|
+
action: "backup",
|
|
240
|
+
outcome: "error",
|
|
241
|
+
error_code: err.code,
|
|
242
|
+
});
|
|
243
|
+
const status = err.code === "backup_insufficient_disk" ? 507 : 400;
|
|
244
|
+
json(res, status, { error: err.message, code: err.code });
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const backupGetMatch = pathname.match(/^\/api\/backup\/([^/]+)$/);
|
|
252
|
+
if (method === "GET" && backupGetMatch) {
|
|
253
|
+
const name = decodeURIComponent(backupGetMatch[1] ?? "");
|
|
254
|
+
try {
|
|
255
|
+
const filePath = await resolveBackupDownloadPath(name, globalStageflowHome());
|
|
256
|
+
const st = statSync(filePath);
|
|
219
257
|
res.writeHead(200, {
|
|
220
|
-
"Content-Type":
|
|
221
|
-
"Content-Length":
|
|
258
|
+
"Content-Type": "application/octet-stream",
|
|
259
|
+
"Content-Length": st.size,
|
|
260
|
+
"Content-Disposition": `attachment; filename="${name}"`,
|
|
261
|
+
});
|
|
262
|
+
createReadStream(filePath).pipe(res);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
if (err instanceof BackupError) {
|
|
266
|
+
json(res, 400, { error: err.message, code: err.code });
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (method === "POST" && pathname === "/api/restore") {
|
|
274
|
+
const body = (await readJsonBody(req));
|
|
275
|
+
const backupName = typeof body.backup === "string"
|
|
276
|
+
? body.backup
|
|
277
|
+
: typeof body.path === "string"
|
|
278
|
+
? body.path
|
|
279
|
+
: undefined;
|
|
280
|
+
if (backupName === undefined) {
|
|
281
|
+
json(res, 400, {
|
|
282
|
+
error: "body.backup (backup name under backups/) is required",
|
|
283
|
+
code: "restore_not_found",
|
|
284
|
+
});
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const callerId = callerIdFromRequestAuth();
|
|
288
|
+
try {
|
|
289
|
+
const archivePath = await resolveBackupNameForRestore(path.basename(backupName), globalStageflowHome());
|
|
290
|
+
const graceMs = parseShutdownGraceMs();
|
|
291
|
+
const staged = await stageRestoreForBoot({
|
|
292
|
+
archivePath,
|
|
293
|
+
homeDir: globalStageflowHome(),
|
|
294
|
+
graceMs,
|
|
295
|
+
});
|
|
296
|
+
writeAudit(auditLog, {
|
|
297
|
+
caller_id: callerId,
|
|
298
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
299
|
+
action: "restore",
|
|
300
|
+
outcome: "ok",
|
|
222
301
|
});
|
|
223
|
-
res
|
|
302
|
+
json(res, 202, {
|
|
303
|
+
ok: true,
|
|
304
|
+
staged: staged.stagedPath,
|
|
305
|
+
drain_deadline: staged.marker.drain_deadline,
|
|
306
|
+
});
|
|
307
|
+
const shutdown = deps.getShutdown?.();
|
|
308
|
+
if (shutdown !== undefined) {
|
|
309
|
+
void shutdown.beginDrain();
|
|
310
|
+
}
|
|
224
311
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
312
|
+
catch (err) {
|
|
313
|
+
if (err instanceof RestoreError || err instanceof BackupError) {
|
|
314
|
+
writeAudit(auditLog, {
|
|
315
|
+
caller_id: callerId,
|
|
316
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
317
|
+
action: "restore",
|
|
318
|
+
outcome: "error",
|
|
319
|
+
error_code: err.code,
|
|
320
|
+
});
|
|
321
|
+
json(res, 400, {
|
|
322
|
+
error: err.message,
|
|
323
|
+
code: err.code,
|
|
324
|
+
});
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
228
328
|
}
|
|
329
|
+
return true;
|
|
229
330
|
}
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const status =
|
|
233
|
-
|
|
331
|
+
if (method === "GET" && pathname === "/api/export") {
|
|
332
|
+
const filter = {};
|
|
333
|
+
const status = url.searchParams.get("status");
|
|
334
|
+
const since = url.searchParams.get("since");
|
|
335
|
+
const pipeline = url.searchParams.get("pipeline");
|
|
336
|
+
const callerId = url.searchParams.get("caller_id");
|
|
337
|
+
if (status)
|
|
338
|
+
filter.status = status;
|
|
339
|
+
if (since)
|
|
340
|
+
filter.since = since;
|
|
341
|
+
if (pipeline)
|
|
342
|
+
filter.pipeline = pipeline;
|
|
343
|
+
if (callerId)
|
|
344
|
+
filter.caller_id = callerId;
|
|
345
|
+
res.writeHead(200, {
|
|
346
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
for await (const line of iterateExportNdjson({
|
|
350
|
+
store,
|
|
351
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
352
|
+
})) {
|
|
353
|
+
if (req.aborted || res.writableEnded || res.destroyed) {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (!res.write(line)) {
|
|
357
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
361
|
+
res.end();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
if (res.headersSent) {
|
|
366
|
+
res.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
234
373
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
374
|
+
const artifactMatch = pathname.match(/^\/api\/runs\/([^/]+)\/artifact$/);
|
|
375
|
+
if (method === "GET" && artifactMatch) {
|
|
376
|
+
const runId = decodeURIComponent(artifactMatch[1] ?? "");
|
|
377
|
+
const artifactPath = url.searchParams.get("path");
|
|
378
|
+
if (artifactPath === null || artifactPath.trim().length === 0) {
|
|
379
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const mediaType = artifactMediaType(artifactPath);
|
|
384
|
+
if (mediaType !== undefined) {
|
|
385
|
+
const bytes = await readRunArtifactBytes(store, runId, artifactPath);
|
|
386
|
+
res.writeHead(200, {
|
|
387
|
+
"Content-Type": mediaType,
|
|
388
|
+
"Content-Length": bytes.length,
|
|
389
|
+
});
|
|
390
|
+
res.end(bytes);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const content = await readRunArtifact(store, runId, artifactPath);
|
|
394
|
+
textPlain(res, 200, content);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
399
|
+
const status = mapped.kind === "denied" ? 403 : mapped.status;
|
|
400
|
+
json(res, status, { error: mapped.error });
|
|
401
|
+
}
|
|
402
|
+
return true;
|
|
243
403
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
404
|
+
const changesMatch = pathname.match(/^\/api\/runs\/([^/]+)\/changes$/);
|
|
405
|
+
if (method === "GET" && changesMatch) {
|
|
406
|
+
const runId = decodeURIComponent(changesMatch[1] ?? "");
|
|
407
|
+
try {
|
|
408
|
+
const result = await listCheckoutChanges(store, runId);
|
|
409
|
+
if (!result.ok) {
|
|
410
|
+
json(res, result.status, {
|
|
411
|
+
error: result.error,
|
|
412
|
+
code: result.code,
|
|
413
|
+
});
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
json(res, 200, {
|
|
417
|
+
runId,
|
|
418
|
+
changes: result.changes,
|
|
419
|
+
truncated: result.truncated,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
424
|
+
json(res, mapped.status, { error: mapped.error });
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
247
427
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
428
|
+
const diffMatch = pathname.match(/^\/api\/runs\/([^/]+)\/diff$/);
|
|
429
|
+
if (method === "GET" && diffMatch) {
|
|
430
|
+
const runId = decodeURIComponent(diffMatch[1] ?? "");
|
|
431
|
+
const modeParam = url.searchParams.get("mode");
|
|
432
|
+
const mode = modeParam === "patch" || modeParam === "stat" ? modeParam : undefined;
|
|
433
|
+
const filterPath = url.searchParams.get("path") ?? undefined;
|
|
434
|
+
const maxBytesRaw = url.searchParams.get("maxBytes");
|
|
435
|
+
const maxBytes = maxBytesRaw !== null && maxBytesRaw.trim() !== ""
|
|
436
|
+
? Number(maxBytesRaw)
|
|
437
|
+
: undefined;
|
|
253
438
|
try {
|
|
254
|
-
|
|
439
|
+
const result = await getRunDiff(store, runId, {
|
|
440
|
+
mode,
|
|
441
|
+
path: filterPath ?? undefined,
|
|
442
|
+
maxBytes: maxBytes !== undefined && Number.isFinite(maxBytes)
|
|
443
|
+
? maxBytes
|
|
444
|
+
: undefined,
|
|
445
|
+
});
|
|
446
|
+
if (!result.ok) {
|
|
447
|
+
json(res, result.status, {
|
|
448
|
+
error: result.error,
|
|
449
|
+
code: result.code,
|
|
450
|
+
});
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
json(res, 200, {
|
|
454
|
+
runId,
|
|
455
|
+
mode: result.mode,
|
|
456
|
+
base_sha: result.base_sha,
|
|
457
|
+
base_source: result.base_source,
|
|
458
|
+
content: result.content,
|
|
459
|
+
truncated: result.truncated,
|
|
460
|
+
untracked: result.untracked,
|
|
461
|
+
});
|
|
255
462
|
}
|
|
256
463
|
catch (err) {
|
|
257
464
|
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
258
|
-
json(res,
|
|
465
|
+
json(res, mapped.status, { error: mapped.error });
|
|
259
466
|
}
|
|
260
467
|
return true;
|
|
261
468
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
469
|
+
const checkoutFileMatch = pathname.match(/^\/api\/runs\/([^/]+)\/file$/);
|
|
470
|
+
if (method === "GET" && checkoutFileMatch) {
|
|
471
|
+
const runId = decodeURIComponent(checkoutFileMatch[1] ?? "");
|
|
472
|
+
const filePath = url.searchParams.get("path");
|
|
473
|
+
if (filePath === null || filePath.trim().length === 0) {
|
|
474
|
+
json(res, 400, { error: "path query parameter is required" });
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const result = await readCheckoutFileBytes(store, runId, filePath);
|
|
479
|
+
if (!result.ok) {
|
|
480
|
+
json(res, result.status, {
|
|
481
|
+
error: result.error,
|
|
482
|
+
code: result.code,
|
|
483
|
+
});
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
const mediaType = artifactMediaType(filePath);
|
|
487
|
+
if (mediaType !== undefined) {
|
|
488
|
+
res.writeHead(200, {
|
|
489
|
+
"Content-Type": mediaType,
|
|
490
|
+
"Content-Length": result.bytes.length,
|
|
491
|
+
});
|
|
492
|
+
res.end(result.bytes);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const classified = classifyArtifactContent(filePath, result.bytes);
|
|
496
|
+
if (classified.kind !== "utf8") {
|
|
497
|
+
json(res, 400, {
|
|
498
|
+
error: "Checkout file is not valid UTF-8 text",
|
|
499
|
+
});
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
textPlain(res, 200, result.bytes.toString("utf8"));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
const mapped = mapStoreLookupError(err, { policy: "artifact" });
|
|
507
|
+
const status = mapped.kind === "denied" ? 403 : mapped.status;
|
|
508
|
+
json(res, status, { error: mapped.error });
|
|
509
|
+
}
|
|
269
510
|
return true;
|
|
270
511
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
512
|
+
const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
|
|
513
|
+
if (method === "GET" && verificationMatch) {
|
|
514
|
+
const runId = decodeURIComponent(verificationMatch[1] ?? "");
|
|
515
|
+
const stageId = decodeURIComponent(verificationMatch[2] ?? "");
|
|
516
|
+
try {
|
|
517
|
+
json(res, 200, await readStageVerificationHistory(store, runId, stageId));
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
521
|
+
json(res, mapped.status, { error: mapped.error });
|
|
522
|
+
}
|
|
275
523
|
return true;
|
|
276
524
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
525
|
+
const exportMatch = pathname.match(/^\/api\/runs\/([^/]+)\/export$/);
|
|
526
|
+
if (method === "GET" && exportMatch) {
|
|
527
|
+
const runId = decodeURIComponent(exportMatch[1] ?? "");
|
|
528
|
+
try {
|
|
529
|
+
const detail = await store.readRun(runId);
|
|
530
|
+
json(res, 200, buildRunExportPayload(detail));
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
534
|
+
json(res, mapped.status, { error: mapped.error });
|
|
535
|
+
}
|
|
280
536
|
return true;
|
|
281
537
|
}
|
|
282
|
-
|
|
283
|
-
|
|
538
|
+
const debugBundleMatch = pathname.match(/^\/api\/runs\/([^/]+)\/debug-bundle$/);
|
|
539
|
+
if (method === "GET" && debugBundleMatch) {
|
|
540
|
+
const runId = decodeURIComponent(debugBundleMatch[1] ?? "");
|
|
541
|
+
try {
|
|
542
|
+
json(res, 200, await buildDebugBundle(store, runId));
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
546
|
+
json(res, mapped.status, { error: mapped.error });
|
|
547
|
+
}
|
|
284
548
|
return true;
|
|
285
549
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
550
|
+
if (method === "GET" && pathname.startsWith("/api/runs/")) {
|
|
551
|
+
const rest = pathname.slice("/api/runs/".length);
|
|
552
|
+
if (rest && !rest.includes("/")) {
|
|
553
|
+
try {
|
|
554
|
+
const detail = await store.readRun(decodeURIComponent(rest));
|
|
555
|
+
json(res, 200, runDetailWithRedactedManifest(detail));
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
const mapped = mapStoreLookupError(err, { policy: "run" });
|
|
559
|
+
json(res, 404, { error: mapped.error });
|
|
560
|
+
}
|
|
289
561
|
return true;
|
|
290
562
|
}
|
|
291
563
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
564
|
+
if (method === "POST" && pathname === "/api/projects") {
|
|
565
|
+
if (!isTrustedLocalHttpRequest(req)) {
|
|
566
|
+
json(res, 403, {
|
|
567
|
+
error: "ensure_project is only allowed from trusted local clients (loopback peer and Host)",
|
|
568
|
+
code: "ensure_project_not_allowed",
|
|
569
|
+
});
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
let body;
|
|
573
|
+
try {
|
|
574
|
+
body = await readJsonBody(req);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
const projectRoot = body !== null &&
|
|
581
|
+
typeof body === "object" &&
|
|
582
|
+
!Array.isArray(body) &&
|
|
583
|
+
typeof body.project_root === "string"
|
|
584
|
+
? body.project_root.trim()
|
|
585
|
+
: "";
|
|
586
|
+
if (!projectRoot) {
|
|
587
|
+
json(res, 400, { error: "project_root is required" });
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
if (!path.isAbsolute(projectRoot)) {
|
|
591
|
+
json(res, 400, {
|
|
592
|
+
error: "project_root must be an absolute path",
|
|
593
|
+
});
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const registered = await store.ensureProject(projectRoot);
|
|
598
|
+
json(res, 200, { project_root: registered });
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
602
|
+
json(res, 400, {
|
|
603
|
+
error: message,
|
|
604
|
+
code: "invalid_project_root",
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
305
608
|
}
|
|
306
|
-
|
|
307
|
-
|
|
609
|
+
if (method === "POST" && pathname === "/api/runs") {
|
|
610
|
+
const body = (await readJsonBody(req));
|
|
611
|
+
const tokenField = findTokenShapedField(body);
|
|
612
|
+
if (tokenField !== undefined) {
|
|
613
|
+
json(res, 400, tokenRejectedPayload(tokenField));
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
const typed = body;
|
|
617
|
+
if (typeof typed.pipeline !== "string" ||
|
|
618
|
+
!typed.pipeline.trim() ||
|
|
619
|
+
typed.task === undefined) {
|
|
620
|
+
json(res, 400, { error: "task and pipeline are required" });
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
if (typeof typed.task !== "string" && !isTaskFile(typed.task)) {
|
|
308
624
|
json(res, 400, {
|
|
309
|
-
error: "
|
|
310
|
-
|
|
625
|
+
error: "task must be a path string or TaskFile object (id and goal required)",
|
|
626
|
+
});
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
if (typed.checkoutOverride !== undefined &&
|
|
630
|
+
typeof typed.checkoutOverride !== "string") {
|
|
631
|
+
json(res, 400, { error: "checkoutOverride must be a string" });
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
if (typed.skipGates !== undefined && typeof typed.skipGates !== "boolean") {
|
|
635
|
+
json(res, 400, { error: "skipGates must be a boolean" });
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
for (const field of ["gitSha", "ciPrUrl", "ciJobUrl"]) {
|
|
639
|
+
if (typed[field] !== undefined && typeof typed[field] !== "string") {
|
|
640
|
+
json(res, 400, { error: `${field} must be a string` });
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
let wireRoot;
|
|
645
|
+
let roots;
|
|
646
|
+
try {
|
|
647
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, typed.project_root));
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
if (err instanceof CatalogPathError) {
|
|
651
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
const wireProjectRoot = wireRoot.project_root;
|
|
657
|
+
let pipelinePath = typed.pipeline.trim();
|
|
658
|
+
let taskInput = typed.task;
|
|
659
|
+
let checkoutOverride;
|
|
660
|
+
try {
|
|
661
|
+
pipelinePath = resolveCatalogRelativePath({
|
|
662
|
+
inputPath: pipelinePath,
|
|
663
|
+
projectRoot: wireProjectRoot,
|
|
664
|
+
roots,
|
|
665
|
+
fieldName: "pipeline",
|
|
666
|
+
}).absolutePath;
|
|
667
|
+
if (typeof typed.task === "string") {
|
|
668
|
+
taskInput = resolveCatalogRelativePath({
|
|
669
|
+
inputPath: typed.task,
|
|
670
|
+
projectRoot: wireProjectRoot,
|
|
671
|
+
roots,
|
|
672
|
+
fieldName: "task",
|
|
673
|
+
}).absolutePath;
|
|
674
|
+
}
|
|
675
|
+
if (typed.checkoutOverride !== undefined) {
|
|
676
|
+
checkoutOverride = resolveCatalogRelativePath({
|
|
677
|
+
inputPath: typed.checkoutOverride,
|
|
678
|
+
projectRoot: wireProjectRoot,
|
|
679
|
+
roots,
|
|
680
|
+
fieldName: "checkout",
|
|
681
|
+
}).absolutePath;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
if (err instanceof CatalogPathError) {
|
|
686
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
691
|
+
let result;
|
|
692
|
+
const callerId = callerIdFromRequestAuth();
|
|
693
|
+
try {
|
|
694
|
+
result = await manager.startRun({
|
|
695
|
+
task: taskInput,
|
|
696
|
+
pipeline: pipelinePath,
|
|
697
|
+
projectRoot: wireRoot.path,
|
|
698
|
+
...(checkoutOverride !== undefined
|
|
699
|
+
? { checkoutOverride }
|
|
700
|
+
: {}),
|
|
701
|
+
...(typed.skipGates !== undefined ? { skipGates: typed.skipGates } : {}),
|
|
702
|
+
...(typed.gitSha !== undefined ? { gitSha: typed.gitSha } : {}),
|
|
703
|
+
...(typed.ciPrUrl !== undefined ? { ciPrUrl: typed.ciPrUrl } : {}),
|
|
704
|
+
...(typed.ciJobUrl !== undefined ? { ciJobUrl: typed.ciJobUrl } : {}),
|
|
705
|
+
...(typed.skills !== undefined ? { skills: typed.skills } : {}),
|
|
706
|
+
callerId,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
if (err instanceof PipelineValidationError) {
|
|
711
|
+
writeAudit(auditLog, {
|
|
712
|
+
caller_id: callerId,
|
|
713
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
714
|
+
action: "start_run",
|
|
715
|
+
outcome: "error",
|
|
716
|
+
error_code: "pipeline_validation",
|
|
717
|
+
});
|
|
718
|
+
json(res, 400, {
|
|
719
|
+
error: "Pipeline validation failed",
|
|
720
|
+
validation: err.result,
|
|
721
|
+
});
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
if (err instanceof PipelinePreflightError) {
|
|
725
|
+
writeAudit(auditLog, {
|
|
726
|
+
caller_id: callerId,
|
|
727
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
728
|
+
action: "start_run",
|
|
729
|
+
outcome: "error",
|
|
730
|
+
error_code: err.code,
|
|
731
|
+
});
|
|
732
|
+
json(res, 400, err.toNetworkBody());
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
737
|
+
if (!result.ok) {
|
|
738
|
+
writeAudit(auditLog, {
|
|
739
|
+
caller_id: callerId,
|
|
740
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
741
|
+
action: "start_run",
|
|
742
|
+
outcome: "error",
|
|
743
|
+
error_code: result.code,
|
|
311
744
|
});
|
|
745
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
312
746
|
return true;
|
|
313
747
|
}
|
|
314
|
-
|
|
748
|
+
writeAudit(auditLog, {
|
|
749
|
+
caller_id: callerId,
|
|
750
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
751
|
+
action: "start_run",
|
|
752
|
+
target_run_id: result.runId,
|
|
753
|
+
outcome: "ok",
|
|
754
|
+
});
|
|
755
|
+
json(res, 202, {
|
|
756
|
+
runId: result.runId,
|
|
757
|
+
...(result.queued === true
|
|
758
|
+
? {
|
|
759
|
+
queued: true,
|
|
760
|
+
queuePosition: result.queuePosition,
|
|
761
|
+
...(result.queuedCode !== undefined
|
|
762
|
+
? { queuedCode: result.queuedCode }
|
|
763
|
+
: {}),
|
|
764
|
+
}
|
|
765
|
+
: {}),
|
|
766
|
+
});
|
|
767
|
+
return true;
|
|
315
768
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
769
|
+
if (method === "POST" && pathname.match(/^\/api\/runs\/[^/]+\/rerun$/)) {
|
|
770
|
+
const runId = decodeURIComponent(pathname.split("/")[3] ?? "");
|
|
771
|
+
const body = (await readJsonBody(req));
|
|
772
|
+
if (body.pinned !== undefined && typeof body.pinned !== "boolean") {
|
|
773
|
+
json(res, 400, { error: "pinned must be a boolean" });
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
const callerId = callerIdFromRequestAuth();
|
|
777
|
+
const result = await manager.rerun(runId, {
|
|
778
|
+
...(body.pinned !== undefined ? { pinned: body.pinned } : {}),
|
|
779
|
+
callerId,
|
|
780
|
+
});
|
|
781
|
+
if (!result.ok) {
|
|
782
|
+
writeAudit(auditLog, {
|
|
783
|
+
caller_id: callerId,
|
|
784
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
785
|
+
action: "rerun",
|
|
786
|
+
target_run_id: runId,
|
|
787
|
+
outcome: "error",
|
|
788
|
+
error_code: result.code,
|
|
789
|
+
});
|
|
790
|
+
json(res, result.status ?? 500, mapStartFailure(result));
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
writeAudit(auditLog, {
|
|
794
|
+
caller_id: callerId,
|
|
795
|
+
surface: getRequestAuth()?.surface ?? "rest",
|
|
796
|
+
action: "rerun",
|
|
797
|
+
target_run_id: result.runId,
|
|
798
|
+
outcome: "ok",
|
|
799
|
+
});
|
|
800
|
+
json(res, 202, { runId: result.runId });
|
|
318
801
|
return true;
|
|
319
802
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
803
|
+
const answerMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/answer$/);
|
|
804
|
+
if (method === "POST" && answerMatch) {
|
|
805
|
+
const runId = decodeURIComponent(answerMatch[1] ?? "");
|
|
806
|
+
const stageId = decodeURIComponent(answerMatch[2] ?? "");
|
|
807
|
+
let answer;
|
|
808
|
+
try {
|
|
809
|
+
answer = parseAskOperatorAnswer(await readJsonBody(req));
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
json(res, 400, {
|
|
813
|
+
error: err instanceof Error ? err.message : String(err),
|
|
814
|
+
});
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
const result = await manager.deliverAnswer(runId, stageId, answer);
|
|
818
|
+
if (!result.ok) {
|
|
819
|
+
json(res, result.status, { error: result.reason });
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
json(res, 202, {
|
|
823
|
+
ok: true,
|
|
824
|
+
...(result.verification !== undefined
|
|
825
|
+
? { verification: result.verification }
|
|
826
|
+
: {}),
|
|
827
|
+
});
|
|
328
828
|
return true;
|
|
329
829
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
error:
|
|
830
|
+
const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
|
|
831
|
+
if (method === "POST" && feedbackDecisionMatch) {
|
|
832
|
+
const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
|
|
833
|
+
const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
|
|
834
|
+
let body;
|
|
835
|
+
try {
|
|
836
|
+
body = await readJsonBody(req);
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
843
|
+
json(res, 400, { error: "Body must be a JSON object" });
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
const raw = body;
|
|
847
|
+
if (raw.decision !== "extend" &&
|
|
848
|
+
raw.decision !== "continue" &&
|
|
849
|
+
raw.decision !== "abandon") {
|
|
850
|
+
json(res, 400, {
|
|
851
|
+
error: "decision must be extend, continue, or abandon",
|
|
852
|
+
});
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
|
|
856
|
+
json(res, 400, { error: "loopId must be a string" });
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
if (raw.reason !== undefined && typeof raw.reason !== "string") {
|
|
860
|
+
json(res, 400, { error: "reason must be a string" });
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
864
|
+
decision: raw.decision,
|
|
865
|
+
...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
|
|
866
|
+
...(raw.reason !== undefined ? { reason: raw.reason } : {}),
|
|
867
|
+
});
|
|
868
|
+
if (!result.ok) {
|
|
869
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
json(res, 202, {
|
|
873
|
+
ok: true,
|
|
874
|
+
effect: result.effect,
|
|
875
|
+
loopId: result.loopId,
|
|
344
876
|
});
|
|
345
877
|
return true;
|
|
346
878
|
}
|
|
347
|
-
const
|
|
348
|
-
if (
|
|
349
|
-
|
|
879
|
+
const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
|
|
880
|
+
if (method === "POST" && retryMatch) {
|
|
881
|
+
await readJsonBody(req);
|
|
882
|
+
const runId = decodeURIComponent(retryMatch[1] ?? "");
|
|
883
|
+
const stageId = decodeURIComponent(retryMatch[2] ?? "");
|
|
884
|
+
const result = await manager.retryStage(runId, stageId);
|
|
885
|
+
if (!result.ok) {
|
|
886
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
json(res, 202, {
|
|
890
|
+
runId: result.runId,
|
|
891
|
+
stageId: result.stageId,
|
|
892
|
+
attemptIndex: result.attemptIndex,
|
|
893
|
+
});
|
|
350
894
|
return true;
|
|
351
895
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
896
|
+
const resumeMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/resume$/);
|
|
897
|
+
if (method === "POST" && resumeMatch) {
|
|
898
|
+
await readJsonBody(req);
|
|
899
|
+
const runId = decodeURIComponent(resumeMatch[1] ?? "");
|
|
900
|
+
const stageId = decodeURIComponent(resumeMatch[2] ?? "");
|
|
901
|
+
const result = await manager.resumeTimedOutStage(runId, stageId);
|
|
902
|
+
if (!result.ok) {
|
|
903
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
json(res, 202, {
|
|
907
|
+
runId: result.runId,
|
|
908
|
+
stageId: result.stageId,
|
|
909
|
+
attemptIndex: result.attemptIndex,
|
|
910
|
+
});
|
|
911
|
+
return true;
|
|
362
912
|
}
|
|
363
|
-
|
|
364
|
-
|
|
913
|
+
const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
|
|
914
|
+
if (method === "POST" && recoverMatch) {
|
|
915
|
+
let body;
|
|
916
|
+
try {
|
|
917
|
+
body = await readJsonBody(req);
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
924
|
+
? body.guidance
|
|
925
|
+
: undefined;
|
|
926
|
+
if (guidance !== undefined && typeof guidance !== "string") {
|
|
927
|
+
json(res, 400, { error: "guidance must be a string" });
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
const runId = decodeURIComponent(recoverMatch[1] ?? "");
|
|
931
|
+
const stageId = decodeURIComponent(recoverMatch[2] ?? "");
|
|
932
|
+
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
933
|
+
if (!result.ok) {
|
|
934
|
+
json(res, result.status ?? 500, mapRetryStageFailure(result));
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
json(res, 202, {
|
|
938
|
+
runId: result.runId,
|
|
939
|
+
stageId: result.stageId,
|
|
940
|
+
attemptIndex: result.attemptIndex,
|
|
941
|
+
});
|
|
365
942
|
return true;
|
|
366
943
|
}
|
|
367
|
-
|
|
368
|
-
|
|
944
|
+
const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
|
|
945
|
+
if (method === "POST" && stopRecoveryMatch) {
|
|
946
|
+
await readJsonBody(req);
|
|
947
|
+
const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
|
|
948
|
+
const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
|
|
949
|
+
const result = await manager.stopManualRecovery(runId, stageId);
|
|
950
|
+
if (!result.ok) {
|
|
951
|
+
json(res, result.status ?? 500, { error: result.reason });
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
json(res, 202, result);
|
|
369
955
|
return true;
|
|
370
956
|
}
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
957
|
+
const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
|
|
958
|
+
if (method === "POST" && abandonMatch) {
|
|
959
|
+
await readJsonBody(req);
|
|
960
|
+
const runId = decodeURIComponent(abandonMatch[1] ?? "");
|
|
961
|
+
const stageId = decodeURIComponent(abandonMatch[2] ?? "");
|
|
962
|
+
const result = await manager.abandonStage(runId, stageId);
|
|
963
|
+
if (!result.ok) {
|
|
964
|
+
json(res, result.status ?? 500, {
|
|
965
|
+
error: result.reason,
|
|
966
|
+
});
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
json(res, 202, {
|
|
970
|
+
ok: true,
|
|
971
|
+
runId: result.runId,
|
|
972
|
+
stageId: result.stageId,
|
|
377
973
|
});
|
|
378
974
|
return true;
|
|
379
975
|
}
|
|
380
|
-
|
|
381
|
-
|
|
976
|
+
const cancelMatch = pathname.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
977
|
+
if (method === "POST" && cancelMatch) {
|
|
978
|
+
const body = (await readJsonBody(req));
|
|
979
|
+
const runId = decodeURIComponent(cancelMatch[1] ?? "");
|
|
980
|
+
if (typeof body.reason !== "string" || body.reason.trim().length === 0) {
|
|
981
|
+
json(res, 400, { error: "Cancel reason is required" });
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
const result = await manager.cancelRun(runId, body.reason);
|
|
985
|
+
if (!result.ok) {
|
|
986
|
+
json(res, result.status ?? 500, {
|
|
987
|
+
error: result.reason,
|
|
988
|
+
});
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
json(res, 202, {
|
|
992
|
+
ok: true,
|
|
993
|
+
runId: result.runId,
|
|
994
|
+
});
|
|
382
995
|
return true;
|
|
383
996
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
997
|
+
if (method === "POST" && pathname === "/api/runs/gc") {
|
|
998
|
+
const body = (await readJsonBody(req));
|
|
999
|
+
const execute = body.execute === true;
|
|
1000
|
+
const result = await manager.gcRuns({ execute, channel: "rest" });
|
|
1001
|
+
if (!result.ok) {
|
|
1002
|
+
json(res, result.status ?? 500, {
|
|
1003
|
+
error: result.reason,
|
|
1004
|
+
});
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
json(res, 200, {
|
|
1008
|
+
slimmed: result.slimmed,
|
|
1009
|
+
purged: result.purged,
|
|
1010
|
+
bareCachesEvicted: result.bareCachesEvicted,
|
|
1011
|
+
});
|
|
386
1012
|
return true;
|
|
387
1013
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1014
|
+
const deleteMatch = pathname.match(/^\/api\/runs\/([^/]+)$/);
|
|
1015
|
+
if (method === "DELETE" && deleteMatch) {
|
|
1016
|
+
const runId = decodeURIComponent(deleteMatch[1] ?? "");
|
|
1017
|
+
const forceParam = url.searchParams.get("force");
|
|
1018
|
+
const force = forceParam === "true" || forceParam === "1";
|
|
1019
|
+
const result = await manager.deleteRun(runId, {
|
|
1020
|
+
force,
|
|
1021
|
+
channel: "rest",
|
|
1022
|
+
});
|
|
1023
|
+
if (!result.ok) {
|
|
1024
|
+
json(res, result.status ?? 500, {
|
|
1025
|
+
error: result.reason,
|
|
1026
|
+
});
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
json(res, 200, {
|
|
1030
|
+
ok: true,
|
|
1031
|
+
runId: result.runId,
|
|
1032
|
+
});
|
|
395
1033
|
return true;
|
|
396
1034
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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);
|
|
1035
|
+
if (method === "GET" && pathname === "/api/tasks") {
|
|
1036
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1037
|
+
const result = await listTasksMultiProject({
|
|
1038
|
+
store,
|
|
1039
|
+
bootCwd: cwd,
|
|
1040
|
+
projectRootFilter: filter,
|
|
1041
|
+
});
|
|
1042
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1043
|
+
result.items.length === 0) {
|
|
1044
|
+
json(res, 400, {
|
|
1045
|
+
error: result.root_errors[0].message,
|
|
1046
|
+
code: "unknown_project_root",
|
|
1047
|
+
root_errors: result.root_errors,
|
|
1048
|
+
});
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
json(res, 200, {
|
|
1052
|
+
tasks: result.items,
|
|
1053
|
+
root_errors: result.root_errors,
|
|
1054
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
1055
|
+
});
|
|
1056
|
+
return true;
|
|
443
1057
|
}
|
|
444
|
-
|
|
445
|
-
|
|
1058
|
+
if (method === "GET" && pathname === "/api/pipelines") {
|
|
1059
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1060
|
+
const result = await listPipelinesMultiProject({
|
|
1061
|
+
store,
|
|
1062
|
+
bootCwd: cwd,
|
|
1063
|
+
projectRootFilter: filter,
|
|
1064
|
+
});
|
|
1065
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1066
|
+
result.items.length === 0) {
|
|
1067
|
+
json(res, 400, {
|
|
1068
|
+
error: result.root_errors[0].message,
|
|
1069
|
+
code: "unknown_project_root",
|
|
1070
|
+
root_errors: result.root_errors,
|
|
1071
|
+
});
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
json(res, 200, {
|
|
1075
|
+
pipelines: result.items,
|
|
1076
|
+
root_errors: result.root_errors,
|
|
1077
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
1078
|
+
});
|
|
446
1079
|
return true;
|
|
447
1080
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
json(res, 400, { error: "guidance must be a string" });
|
|
1081
|
+
if (method === "GET" && pathname === "/api/stages") {
|
|
1082
|
+
json(res, 404, {
|
|
1083
|
+
error: "Global stage library removed; stages are pipeline-scoped",
|
|
1084
|
+
});
|
|
453
1085
|
return true;
|
|
454
1086
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1087
|
+
if (method === "POST" && pathname === "/api/stages") {
|
|
1088
|
+
let body;
|
|
1089
|
+
try {
|
|
1090
|
+
body = await readJsonBody(req);
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
const writeRoot = body !== null &&
|
|
1097
|
+
typeof body === "object" &&
|
|
1098
|
+
!Array.isArray(body) &&
|
|
1099
|
+
typeof body.project_root === "string"
|
|
1100
|
+
? body.project_root
|
|
1101
|
+
: undefined;
|
|
1102
|
+
let stageWriteRoot;
|
|
1103
|
+
try {
|
|
1104
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1105
|
+
stageWriteRoot = selected.path;
|
|
1106
|
+
}
|
|
1107
|
+
catch (err) {
|
|
1108
|
+
if (err instanceof CatalogPathError) {
|
|
1109
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
throw err;
|
|
1113
|
+
}
|
|
1114
|
+
const parsed = parseCreateStageBody(body);
|
|
1115
|
+
if ("ok" in parsed) {
|
|
1116
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
const ctx = await resolveStageflowContext(stageWriteRoot);
|
|
1120
|
+
if (ctx.manifestStatus !== "ok") {
|
|
1121
|
+
json(res, 400, {
|
|
1122
|
+
error: "Project root not found; initialize stageflow.yaml",
|
|
1123
|
+
});
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
const result = await createStage(ctx.projectRoot, parsed);
|
|
1127
|
+
if (!result.ok) {
|
|
1128
|
+
json(res, result.status, { error: result.error });
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
json(res, 201, result.stage);
|
|
460
1132
|
return true;
|
|
461
1133
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1134
|
+
if (method === "POST" && pathname === "/api/pipelines") {
|
|
1135
|
+
let body;
|
|
1136
|
+
try {
|
|
1137
|
+
body = await readJsonBody(req);
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1141
|
+
return true;
|
|
1142
|
+
}
|
|
1143
|
+
const writeRoot = body !== null &&
|
|
1144
|
+
typeof body === "object" &&
|
|
1145
|
+
!Array.isArray(body) &&
|
|
1146
|
+
typeof body.project_root === "string"
|
|
1147
|
+
? body.project_root
|
|
1148
|
+
: undefined;
|
|
1149
|
+
let pipelineWriteRoot;
|
|
1150
|
+
try {
|
|
1151
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1152
|
+
pipelineWriteRoot = selected.path;
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
if (err instanceof CatalogPathError) {
|
|
1156
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
throw err;
|
|
1160
|
+
}
|
|
1161
|
+
const parsed = parseCreatePipelineBody(body);
|
|
1162
|
+
if ("ok" in parsed) {
|
|
1163
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
const ctx = await resolveStageflowContext(pipelineWriteRoot);
|
|
1167
|
+
if (ctx.manifestStatus !== "ok") {
|
|
1168
|
+
json(res, 400, {
|
|
1169
|
+
error: "Project root not found; initialize stageflow.yaml",
|
|
1170
|
+
});
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
const result = await createPipeline(ctx.projectRoot, parsed);
|
|
1174
|
+
if (!result.ok) {
|
|
1175
|
+
json(res, result.status, { error: result.error });
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
json(res, 201, result.pipeline);
|
|
477
1179
|
return true;
|
|
478
1180
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
error:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1181
|
+
if (method === "POST" && pathname === "/api/drafts/validate") {
|
|
1182
|
+
let body;
|
|
1183
|
+
try {
|
|
1184
|
+
body = await readJsonBody(req);
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1191
|
+
if ("ok" in draftParsed) {
|
|
1192
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
const writeRoot = body !== null &&
|
|
1196
|
+
typeof body === "object" &&
|
|
1197
|
+
!Array.isArray(body) &&
|
|
1198
|
+
typeof body.project_root === "string"
|
|
1199
|
+
? body.project_root
|
|
1200
|
+
: undefined;
|
|
1201
|
+
let validateRoot = cwd;
|
|
1202
|
+
if (writeRoot) {
|
|
1203
|
+
try {
|
|
1204
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1205
|
+
validateRoot = selected.path;
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
if (err instanceof CatalogPathError) {
|
|
1209
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1210
|
+
return true;
|
|
1211
|
+
}
|
|
1212
|
+
throw err;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const ctx = await resolveStageflowContext(validateRoot);
|
|
1216
|
+
const result = await validateDraftPackage(draftParsed, {
|
|
1217
|
+
cwd: ctx.projectRoot,
|
|
1218
|
+
projectRoot: ctx.projectRoot,
|
|
1219
|
+
strict: true,
|
|
1220
|
+
});
|
|
1221
|
+
json(res, 200, result);
|
|
1222
|
+
return true;
|
|
521
1223
|
}
|
|
522
|
-
|
|
523
|
-
|
|
1224
|
+
if (method === "POST" && pathname === "/api/drafts/create") {
|
|
1225
|
+
let body;
|
|
1226
|
+
try {
|
|
1227
|
+
body = await readJsonBody(req);
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
const writeRoot = body !== null &&
|
|
1234
|
+
typeof body === "object" &&
|
|
1235
|
+
!Array.isArray(body) &&
|
|
1236
|
+
typeof body.project_root === "string"
|
|
1237
|
+
? body.project_root
|
|
1238
|
+
: undefined;
|
|
1239
|
+
let draftWriteRoot;
|
|
1240
|
+
try {
|
|
1241
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1242
|
+
draftWriteRoot = selected.path;
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
if (err instanceof CatalogPathError) {
|
|
1246
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
throw err;
|
|
1250
|
+
}
|
|
1251
|
+
const parsed = parseCreateDraftPackageBody(body);
|
|
1252
|
+
if ("ok" in parsed) {
|
|
1253
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
const ctx = await resolveStageflowContext(draftWriteRoot);
|
|
1257
|
+
if (!ctx.isGitProject) {
|
|
1258
|
+
json(res, 400, {
|
|
1259
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1260
|
+
});
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
const result = (await publishDraftPackage(ctx.projectRoot, parsed, "create")).write;
|
|
1264
|
+
if (!result.ok) {
|
|
1265
|
+
json(res, result.status, {
|
|
1266
|
+
error: result.error,
|
|
1267
|
+
...(result.findings ? { findings: result.findings } : {}),
|
|
1268
|
+
});
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
json(res, 201, {
|
|
1272
|
+
pipeline: result.pipeline,
|
|
1273
|
+
pipelinePath: result.pipelinePath,
|
|
1274
|
+
stagePaths: result.stagePaths,
|
|
1275
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
1276
|
+
});
|
|
524
1277
|
return true;
|
|
525
1278
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1279
|
+
if (method === "POST" && pathname === "/api/drafts/overwrite") {
|
|
1280
|
+
let body;
|
|
1281
|
+
try {
|
|
1282
|
+
body = await readJsonBody(req);
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
const writeRoot = body !== null &&
|
|
1289
|
+
typeof body === "object" &&
|
|
1290
|
+
!Array.isArray(body) &&
|
|
1291
|
+
typeof body.project_root === "string"
|
|
1292
|
+
? body.project_root
|
|
1293
|
+
: undefined;
|
|
1294
|
+
let draftWriteRoot;
|
|
1295
|
+
try {
|
|
1296
|
+
const { wireRoot: selected } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1297
|
+
draftWriteRoot = selected.path;
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
if (err instanceof CatalogPathError) {
|
|
1301
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
throw err;
|
|
1305
|
+
}
|
|
1306
|
+
const parsed = parseOverwriteDraftPackageBody(body);
|
|
1307
|
+
if ("ok" in parsed) {
|
|
1308
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
const ctx = await resolveStageflowContext(draftWriteRoot);
|
|
1312
|
+
if (!ctx.isGitProject) {
|
|
1313
|
+
json(res, 400, {
|
|
1314
|
+
error: "Project root not found; initialize stageflow.yaml in a git repo",
|
|
1315
|
+
});
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
const result = (await publishDraftPackage(ctx.projectRoot, parsed, "overwrite")).write;
|
|
1319
|
+
if (!result.ok) {
|
|
1320
|
+
json(res, result.status, {
|
|
1321
|
+
error: result.error,
|
|
1322
|
+
...(result.findings ? { findings: result.findings } : {}),
|
|
1323
|
+
});
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
json(res, 200, {
|
|
1327
|
+
pipeline: result.pipeline,
|
|
1328
|
+
pipelinePath: result.pipelinePath,
|
|
1329
|
+
stagePaths: result.stagePaths,
|
|
1330
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
1331
|
+
});
|
|
529
1332
|
return true;
|
|
530
1333
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1334
|
+
if (method === "POST" && pathname === "/api/drafts/open") {
|
|
1335
|
+
let body;
|
|
1336
|
+
try {
|
|
1337
|
+
body = await readJsonBody(req);
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
const parsed = parseOpenDraftPackageBody(body);
|
|
1344
|
+
if ("ok" in parsed) {
|
|
1345
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
const writeRoot = body !== null &&
|
|
1349
|
+
typeof body === "object" &&
|
|
1350
|
+
!Array.isArray(body) &&
|
|
1351
|
+
typeof body.project_root === "string"
|
|
1352
|
+
? body.project_root
|
|
1353
|
+
: undefined;
|
|
1354
|
+
let wireRoot;
|
|
1355
|
+
let roots;
|
|
1356
|
+
try {
|
|
1357
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
if (err instanceof CatalogPathError) {
|
|
1361
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
throw err;
|
|
1365
|
+
}
|
|
1366
|
+
let pipelineRel;
|
|
1367
|
+
let taskRel;
|
|
1368
|
+
try {
|
|
1369
|
+
pipelineRel = resolveCatalogRelativePath({
|
|
1370
|
+
inputPath: parsed.path,
|
|
1371
|
+
projectRoot: writeRoot,
|
|
1372
|
+
roots,
|
|
1373
|
+
fieldName: "path",
|
|
1374
|
+
}).relativePath;
|
|
1375
|
+
if (parsed.taskPath) {
|
|
1376
|
+
taskRel = resolveCatalogRelativePath({
|
|
1377
|
+
inputPath: parsed.taskPath,
|
|
1378
|
+
projectRoot: writeRoot,
|
|
1379
|
+
roots,
|
|
1380
|
+
fieldName: "task",
|
|
1381
|
+
}).relativePath;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch (err) {
|
|
1385
|
+
if (err instanceof CatalogPathError) {
|
|
1386
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
throw err;
|
|
1390
|
+
}
|
|
1391
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1392
|
+
const result = await loadDraftPackage(ctx.projectRoot, pipelineRel, {
|
|
1393
|
+
...(taskRel !== undefined ? { taskPath: taskRel } : {}),
|
|
1394
|
+
});
|
|
1395
|
+
if (!result.ok) {
|
|
1396
|
+
json(res, result.status, { error: result.error });
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
json(res, 200, {
|
|
1400
|
+
draft: result.draft,
|
|
1401
|
+
destination: result.destination,
|
|
1402
|
+
pipelinePath: result.pipelinePath,
|
|
1403
|
+
...(result.taskPath !== undefined ? { taskPath: result.taskPath } : {}),
|
|
535
1404
|
});
|
|
536
1405
|
return true;
|
|
537
1406
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1407
|
+
if (method === "POST" && pathname === "/api/drafts/attach-task") {
|
|
1408
|
+
let body;
|
|
1409
|
+
try {
|
|
1410
|
+
body = await readJsonBody(req);
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
const parsed = parseAttachTaskBody(body);
|
|
1417
|
+
if ("ok" in parsed) {
|
|
1418
|
+
json(res, parsed.status, { error: parsed.error });
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
const writeRoot = body !== null &&
|
|
1422
|
+
typeof body === "object" &&
|
|
1423
|
+
!Array.isArray(body) &&
|
|
1424
|
+
typeof body.project_root === "string"
|
|
1425
|
+
? body.project_root
|
|
1426
|
+
: undefined;
|
|
1427
|
+
let wireRoot;
|
|
1428
|
+
let roots;
|
|
1429
|
+
try {
|
|
1430
|
+
({ wireRoot, roots } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot));
|
|
1431
|
+
}
|
|
1432
|
+
catch (err) {
|
|
1433
|
+
if (err instanceof CatalogPathError) {
|
|
1434
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
throw err;
|
|
1438
|
+
}
|
|
1439
|
+
let taskRel;
|
|
1440
|
+
try {
|
|
1441
|
+
taskRel = resolveCatalogRelativePath({
|
|
1442
|
+
inputPath: parsed.taskPath,
|
|
1443
|
+
projectRoot: writeRoot,
|
|
1444
|
+
roots,
|
|
1445
|
+
fieldName: "task",
|
|
1446
|
+
}).relativePath;
|
|
1447
|
+
}
|
|
1448
|
+
catch (err) {
|
|
1449
|
+
if (err instanceof CatalogPathError) {
|
|
1450
|
+
json(res, 400, catalogPathErrorBody(err));
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
throw err;
|
|
1454
|
+
}
|
|
1455
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1456
|
+
const result = await loadTaskArtifact(ctx.projectRoot, taskRel);
|
|
1457
|
+
if (!result.ok) {
|
|
1458
|
+
json(res, result.status, { error: result.error });
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
json(res, 200, {
|
|
1462
|
+
task: result.task,
|
|
1463
|
+
taskPath: result.taskPath,
|
|
1464
|
+
});
|
|
541
1465
|
return true;
|
|
542
1466
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1467
|
+
if (method === "GET" &&
|
|
1468
|
+
pathname === "/api/workshop/autosave") {
|
|
1469
|
+
const keyParam = url.searchParams.get("key");
|
|
1470
|
+
const key = workshopAutosaveSlotKey(keyParam);
|
|
1471
|
+
const writeRoot = url.searchParams.get("project_root") ?? undefined;
|
|
1472
|
+
let storeRoot;
|
|
1473
|
+
try {
|
|
1474
|
+
if (writeRoot) {
|
|
1475
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
|
|
1476
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1477
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1478
|
+
projectRoot: ctx.projectRoot,
|
|
1479
|
+
isGitProject: ctx.isGitProject,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1484
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1485
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1486
|
+
isGitProject: ctx.isGitProject,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
catch (err) {
|
|
1491
|
+
if (err instanceof CatalogPathError) {
|
|
1492
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
throw err;
|
|
1496
|
+
}
|
|
1497
|
+
const record = readWorkshopAutosave(storeRoot, key);
|
|
1498
|
+
json(res, 200, { key, autosave: record });
|
|
1499
|
+
return true;
|
|
550
1500
|
}
|
|
551
|
-
|
|
552
|
-
|
|
1501
|
+
if (method === "PUT" &&
|
|
1502
|
+
pathname === "/api/workshop/autosave") {
|
|
1503
|
+
let body;
|
|
1504
|
+
try {
|
|
1505
|
+
body = await readJsonBody(req);
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if (!isPlainObject(body)) {
|
|
1512
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
|
|
1516
|
+
const writeRoot = typeof body.project_root === "string"
|
|
1517
|
+
? body.project_root
|
|
1518
|
+
: undefined;
|
|
1519
|
+
let storeRoot;
|
|
1520
|
+
try {
|
|
1521
|
+
if (writeRoot) {
|
|
1522
|
+
const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1523
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1524
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1525
|
+
projectRoot: ctx.projectRoot,
|
|
1526
|
+
isGitProject: ctx.isGitProject,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1531
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1532
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1533
|
+
isGitProject: ctx.isGitProject,
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
catch (err) {
|
|
1538
|
+
if (err instanceof CatalogPathError) {
|
|
1539
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
throw err;
|
|
1543
|
+
}
|
|
1544
|
+
const candidate = {
|
|
1545
|
+
version: 1,
|
|
1546
|
+
key,
|
|
1547
|
+
updatedAt: typeof body.updatedAt === "string" && body.updatedAt
|
|
1548
|
+
? body.updatedAt
|
|
1549
|
+
: new Date().toISOString(),
|
|
1550
|
+
draft: body.draft,
|
|
1551
|
+
messages: body.messages,
|
|
1552
|
+
autoApply: Boolean(body.autoApply),
|
|
1553
|
+
...(body.sessionModelOverride === null ||
|
|
1554
|
+
typeof body.sessionModelOverride === "string"
|
|
1555
|
+
? { sessionModelOverride: body.sessionModelOverride }
|
|
1556
|
+
: {}),
|
|
1557
|
+
...(body.destination !== undefined
|
|
1558
|
+
? {
|
|
1559
|
+
destination: body.destination,
|
|
1560
|
+
}
|
|
1561
|
+
: {}),
|
|
1562
|
+
...(body.savedPath === null || typeof body.savedPath === "string"
|
|
1563
|
+
? { savedPath: body.savedPath }
|
|
1564
|
+
: {}),
|
|
1565
|
+
...(body.savedTaskPath === null ||
|
|
1566
|
+
typeof body.savedTaskPath === "string"
|
|
1567
|
+
? { savedTaskPath: body.savedTaskPath }
|
|
1568
|
+
: {}),
|
|
1569
|
+
...(isPlainObject(body.diskFingerprints)
|
|
1570
|
+
? {
|
|
1571
|
+
diskFingerprints: body.diskFingerprints,
|
|
1572
|
+
}
|
|
1573
|
+
: {}),
|
|
1574
|
+
};
|
|
1575
|
+
const parsed = parseWorkshopAutosaveRecord(candidate);
|
|
1576
|
+
if (!parsed) {
|
|
1577
|
+
json(res, 400, { error: "Invalid workshop autosave payload" });
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
const written = writeWorkshopAutosave(storeRoot, parsed);
|
|
1581
|
+
json(res, 200, { autosave: written });
|
|
553
1582
|
return true;
|
|
554
1583
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1584
|
+
if (method === "DELETE" &&
|
|
1585
|
+
pathname === "/api/workshop/autosave") {
|
|
1586
|
+
let body;
|
|
1587
|
+
try {
|
|
1588
|
+
body = await readJsonBody(req);
|
|
1589
|
+
}
|
|
1590
|
+
catch {
|
|
1591
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
if (!isPlainObject(body)) {
|
|
1595
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1596
|
+
return true;
|
|
1597
|
+
}
|
|
1598
|
+
const key = workshopAutosaveSlotKey(typeof body.key === "string" ? body.key : undefined);
|
|
1599
|
+
const writeRoot = typeof body.project_root === "string"
|
|
1600
|
+
? body.project_root
|
|
1601
|
+
: undefined;
|
|
1602
|
+
let storeRoot;
|
|
1603
|
+
try {
|
|
1604
|
+
if (writeRoot) {
|
|
1605
|
+
const { wireRoot } = await resolveWritableCatalogRoot({ store, bootCwd: cwd }, writeRoot);
|
|
1606
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
1607
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1608
|
+
projectRoot: ctx.projectRoot,
|
|
1609
|
+
isGitProject: ctx.isGitProject,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
const ctx = await resolveStageflowContext(cwd);
|
|
1614
|
+
storeRoot = resolveWorkshopAutosaveStoreRoot({
|
|
1615
|
+
projectRoot: ctx.isGitProject ? ctx.projectRoot : null,
|
|
1616
|
+
isGitProject: ctx.isGitProject,
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
catch (err) {
|
|
1621
|
+
if (err instanceof CatalogPathError) {
|
|
1622
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
throw err;
|
|
1626
|
+
}
|
|
1627
|
+
const cleared = clearWorkshopAutosave(storeRoot, key);
|
|
1628
|
+
json(res, 200, { key, cleared });
|
|
558
1629
|
return true;
|
|
559
1630
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1631
|
+
if (method === "GET" && pathname === "/api/workshop/picker") {
|
|
1632
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
1633
|
+
const result = await listPipelinesMultiProject({
|
|
1634
|
+
store,
|
|
1635
|
+
bootCwd: cwd,
|
|
1636
|
+
projectRootFilter: filter,
|
|
1637
|
+
});
|
|
1638
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
1639
|
+
result.items.length === 0) {
|
|
1640
|
+
json(res, 400, {
|
|
1641
|
+
error: result.root_errors[0].message,
|
|
1642
|
+
code: "unknown_project_root",
|
|
1643
|
+
root_errors: result.root_errors,
|
|
1644
|
+
});
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
json(res, 200, {
|
|
1648
|
+
rows: listWorkshopPickerRows(workshopBuildStoreRoot(), resolvePickerCatalogPipelines(result.items, result.roots), result.roots),
|
|
1649
|
+
root_errors: result.root_errors,
|
|
1650
|
+
...(result.tip !== undefined ? { tip: result.tip } : {}),
|
|
564
1651
|
});
|
|
565
1652
|
return true;
|
|
566
1653
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1654
|
+
if (method === "POST" && pathname === "/api/workshop/focus") {
|
|
1655
|
+
let body;
|
|
1656
|
+
try {
|
|
1657
|
+
body = await readJsonBody(req);
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
if (!isPlainObject(body)) {
|
|
1664
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
if (typeof body.projectRoot !== "string" ||
|
|
1668
|
+
typeof body.relativePath !== "string") {
|
|
1669
|
+
json(res, 400, { error: "project root and path are required" });
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1672
|
+
let projectRoot = body.projectRoot;
|
|
1673
|
+
try {
|
|
1674
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, body.projectRoot);
|
|
1675
|
+
projectRoot = wireRoot.path;
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
if (err instanceof CatalogPathError) {
|
|
1679
|
+
json(res, 400, { error: err.message });
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
throw err;
|
|
1683
|
+
}
|
|
1684
|
+
const focused = await focusUnboundWorkshopBuild(workshopBuildStoreRoot(), {
|
|
1685
|
+
projectRoot,
|
|
1686
|
+
relativePath: body.relativePath,
|
|
1687
|
+
});
|
|
1688
|
+
if (!focused.ok) {
|
|
1689
|
+
json(res, focused.status, { error: focused.error });
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
json(res, focused.created ? 201 : 200, { build: focused.build });
|
|
570
1693
|
return true;
|
|
571
1694
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1695
|
+
if (method === "GET" &&
|
|
1696
|
+
pathname === "/api/workshop/builds") {
|
|
1697
|
+
json(res, 200, {
|
|
1698
|
+
builds: listWorkshopBuilds(workshopBuildStoreRoot()),
|
|
1699
|
+
});
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1702
|
+
if (method === "POST" &&
|
|
1703
|
+
pathname === "/api/workshop/builds") {
|
|
1704
|
+
let body = {};
|
|
1705
|
+
try {
|
|
1706
|
+
body = await readJsonBody(req);
|
|
1707
|
+
}
|
|
1708
|
+
catch {
|
|
1709
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
if (!isPlainObject(body)) {
|
|
1713
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1717
|
+
if ("ok" in draftParsed) {
|
|
1718
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
const projectRoot = readBuildTieField(body, "projectRoot");
|
|
1722
|
+
if (!projectRoot.ok) {
|
|
1723
|
+
json(res, 400, { error: projectRoot.error });
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
const relativePath = readBuildTieField(body, "relativePath");
|
|
1727
|
+
if (!relativePath.ok) {
|
|
1728
|
+
json(res, 400, { error: relativePath.error });
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
const build = createWorkshopBuild(workshopBuildStoreRoot(), {
|
|
1732
|
+
draft: draftParsed,
|
|
1733
|
+
...(typeof body.id === "string" ? { id: body.id } : {}),
|
|
1734
|
+
...(projectRoot.present ? { projectRoot: projectRoot.value } : {}),
|
|
1735
|
+
...(relativePath.present
|
|
1736
|
+
? { relativePath: relativePath.value }
|
|
1737
|
+
: {}),
|
|
1738
|
+
});
|
|
1739
|
+
json(res, 201, { build });
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
const workshopBuildMatch = pathname.match(/^\/api\/workshop\/builds\/([^/]+)$/);
|
|
1743
|
+
if (method === "GET" && workshopBuildMatch) {
|
|
1744
|
+
const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
|
|
1745
|
+
try {
|
|
1746
|
+
const build = getWorkshopBuild(workshopBuildStoreRoot(), buildId);
|
|
1747
|
+
json(res, 200, { build });
|
|
1748
|
+
}
|
|
1749
|
+
catch (err) {
|
|
1750
|
+
if (err instanceof WorkshopBuildStoreError &&
|
|
1751
|
+
err.code === "workshop_build_not_found") {
|
|
1752
|
+
json(res, 404, {
|
|
1753
|
+
error: err.message,
|
|
1754
|
+
code: err.code,
|
|
1755
|
+
buildId: err.buildId,
|
|
1756
|
+
});
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
throw err;
|
|
1760
|
+
}
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
if (method === "PUT" && workshopBuildMatch) {
|
|
1764
|
+
const buildId = decodeURIComponent(workshopBuildMatch[1] ?? "");
|
|
1765
|
+
let body = {};
|
|
1766
|
+
try {
|
|
1767
|
+
body = await readJsonBody(req);
|
|
1768
|
+
}
|
|
1769
|
+
catch {
|
|
1770
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
if (!isPlainObject(body)) {
|
|
1774
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
let draft;
|
|
1778
|
+
if (body.draft !== undefined) {
|
|
1779
|
+
const draftParsed = parseDraftPackageBody({ draft: body.draft });
|
|
1780
|
+
if ("ok" in draftParsed) {
|
|
1781
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
draft = draftParsed;
|
|
1785
|
+
}
|
|
1786
|
+
const projectRoot = readBuildTieField(body, "projectRoot");
|
|
1787
|
+
if (!projectRoot.ok) {
|
|
1788
|
+
json(res, 400, { error: projectRoot.error });
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
const relativePath = readBuildTieField(body, "relativePath");
|
|
1792
|
+
if (!relativePath.ok) {
|
|
1793
|
+
json(res, 400, { error: relativePath.error });
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
try {
|
|
1797
|
+
const build = updateWorkshopBuild(workshopBuildStoreRoot(), buildId, {
|
|
1798
|
+
...(draft ? { draft } : {}),
|
|
1799
|
+
...(projectRoot.present
|
|
1800
|
+
? { projectRoot: projectRoot.value }
|
|
1801
|
+
: {}),
|
|
1802
|
+
...(relativePath.present
|
|
1803
|
+
? { relativePath: relativePath.value }
|
|
1804
|
+
: {}),
|
|
1805
|
+
});
|
|
1806
|
+
json(res, 200, { build });
|
|
1807
|
+
}
|
|
1808
|
+
catch (err) {
|
|
1809
|
+
if (err instanceof WorkshopBuildStoreError &&
|
|
1810
|
+
err.code === "workshop_build_not_found") {
|
|
1811
|
+
json(res, 404, {
|
|
1812
|
+
error: err.message,
|
|
1813
|
+
code: err.code,
|
|
1814
|
+
buildId: err.buildId,
|
|
1815
|
+
});
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
619
1821
|
}
|
|
620
|
-
|
|
621
|
-
|
|
1822
|
+
if (method === "GET" &&
|
|
1823
|
+
pathname === "/api/workshop/sessions") {
|
|
1824
|
+
const sessions = listWorkshopSessions(workshopSessionStoreRoot());
|
|
1825
|
+
json(res, 200, { sessions });
|
|
622
1826
|
return true;
|
|
623
1827
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
error: "
|
|
1828
|
+
if (method === "POST" &&
|
|
1829
|
+
pathname === "/api/workshop/sessions") {
|
|
1830
|
+
let body = {};
|
|
1831
|
+
try {
|
|
1832
|
+
body = await readJsonBody(req);
|
|
1833
|
+
}
|
|
1834
|
+
catch {
|
|
1835
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
if (!isPlainObject(body)) {
|
|
1839
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
const record = createWorkshopSession(workshopSessionStoreRoot(), {
|
|
1843
|
+
...(typeof body.id === "string" ? { id: body.id } : {}),
|
|
632
1844
|
});
|
|
1845
|
+
json(res, 201, { session: record });
|
|
633
1846
|
return true;
|
|
634
1847
|
}
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
1848
|
+
const workshopSessionGetMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
|
|
1849
|
+
if (method === "GET" && workshopSessionGetMatch) {
|
|
1850
|
+
const sessionId = decodeURIComponent(workshopSessionGetMatch[1] ?? "");
|
|
1851
|
+
try {
|
|
1852
|
+
const session = getWorkshopSession(workshopSessionStoreRoot(), sessionId);
|
|
1853
|
+
json(res, 200, { session });
|
|
1854
|
+
}
|
|
1855
|
+
catch (err) {
|
|
1856
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1857
|
+
err.code === "workshop_session_not_found") {
|
|
1858
|
+
json(res, 404, {
|
|
1859
|
+
error: err.message,
|
|
1860
|
+
code: err.code,
|
|
1861
|
+
sessionId: err.sessionId,
|
|
1862
|
+
});
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
throw err;
|
|
1866
|
+
}
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
const workshopSessionPatchMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)$/);
|
|
1870
|
+
if (method === "PATCH" && workshopSessionPatchMatch) {
|
|
1871
|
+
const sessionId = decodeURIComponent(workshopSessionPatchMatch[1] ?? "");
|
|
1872
|
+
let body = {};
|
|
1873
|
+
try {
|
|
1874
|
+
body = await readJsonBody(req);
|
|
1875
|
+
}
|
|
1876
|
+
catch {
|
|
1877
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
if (!isPlainObject(body)) {
|
|
1881
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
if (!Object.prototype.hasOwnProperty.call(body, "activeBuildId")) {
|
|
1885
|
+
json(res, 400, { error: "activeBuildId is required" });
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
const activeBuildId = body.activeBuildId;
|
|
1889
|
+
if (activeBuildId !== null && typeof activeBuildId !== "string") {
|
|
1890
|
+
json(res, 400, { error: "activeBuildId must be a string or null" });
|
|
640
1891
|
return true;
|
|
641
1892
|
}
|
|
642
|
-
|
|
1893
|
+
try {
|
|
1894
|
+
const session = updateWorkshopSessionActiveBuildId(workshopSessionStoreRoot(), sessionId, activeBuildId);
|
|
1895
|
+
json(res, 200, { session });
|
|
1896
|
+
}
|
|
1897
|
+
catch (err) {
|
|
1898
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1899
|
+
err.code === "workshop_session_not_found") {
|
|
1900
|
+
json(res, 404, {
|
|
1901
|
+
error: err.message,
|
|
1902
|
+
code: err.code,
|
|
1903
|
+
sessionId: err.sessionId,
|
|
1904
|
+
});
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
throw err;
|
|
1908
|
+
}
|
|
1909
|
+
return true;
|
|
643
1910
|
}
|
|
644
|
-
|
|
645
|
-
if (
|
|
1911
|
+
const workshopSessionUndoMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/undo$/);
|
|
1912
|
+
if (method === "POST" && workshopSessionUndoMatch) {
|
|
1913
|
+
const sessionId = decodeURIComponent(workshopSessionUndoMatch[1] ?? "");
|
|
1914
|
+
let body = {};
|
|
646
1915
|
try {
|
|
647
|
-
|
|
1916
|
+
body = await readJsonBody(req);
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
if (!isPlainObject(body)) {
|
|
1923
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1927
|
+
if ("ok" in draftParsed) {
|
|
1928
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
const result = await undoWorkshopSessionMutation({
|
|
1933
|
+
sessionId,
|
|
1934
|
+
draft: draftParsed,
|
|
1935
|
+
...(typeof body.mutationId === "string"
|
|
1936
|
+
? { mutationId: body.mutationId }
|
|
1937
|
+
: {}),
|
|
1938
|
+
registry: workshopChatRegistry,
|
|
1939
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
1940
|
+
});
|
|
1941
|
+
json(res, result.ok ? 200 : 409, result);
|
|
648
1942
|
}
|
|
649
1943
|
catch (err) {
|
|
650
|
-
|
|
651
|
-
|
|
1944
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1945
|
+
err.code === "workshop_session_not_found") {
|
|
1946
|
+
json(res, 404, {
|
|
1947
|
+
error: err.message,
|
|
1948
|
+
code: err.code,
|
|
1949
|
+
sessionId: err.sessionId,
|
|
1950
|
+
});
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
json(res, 400, {
|
|
1954
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
const workshopSessionAcceptMatch = pathname.match(/^\/api\/workshop\/sessions\/([^/]+)\/accept$/);
|
|
1960
|
+
if (method === "POST" && workshopSessionAcceptMatch) {
|
|
1961
|
+
const sessionId = decodeURIComponent(workshopSessionAcceptMatch[1] ?? "");
|
|
1962
|
+
let body = {};
|
|
1963
|
+
try {
|
|
1964
|
+
body = await readJsonBody(req);
|
|
1965
|
+
}
|
|
1966
|
+
catch {
|
|
1967
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
652
1968
|
return true;
|
|
653
1969
|
}
|
|
1970
|
+
if (!isPlainObject(body)) {
|
|
1971
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
1975
|
+
if ("ok" in draftParsed) {
|
|
1976
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const result = await acceptWorkshopSessionMutation({
|
|
1981
|
+
sessionId,
|
|
1982
|
+
draft: draftParsed,
|
|
1983
|
+
...(typeof body.mutationId === "string"
|
|
1984
|
+
? { mutationId: body.mutationId }
|
|
1985
|
+
: {}),
|
|
1986
|
+
registry: workshopChatRegistry,
|
|
1987
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
1988
|
+
});
|
|
1989
|
+
json(res, result.ok ? 200 : 409, result);
|
|
1990
|
+
}
|
|
1991
|
+
catch (err) {
|
|
1992
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
1993
|
+
err.code === "workshop_session_not_found") {
|
|
1994
|
+
json(res, 404, {
|
|
1995
|
+
error: err.message,
|
|
1996
|
+
code: err.code,
|
|
1997
|
+
sessionId: err.sessionId,
|
|
1998
|
+
});
|
|
1999
|
+
return true;
|
|
2000
|
+
}
|
|
2001
|
+
json(res, 400, {
|
|
2002
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
return true;
|
|
654
2006
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
2007
|
+
if (method === "POST" && pathname === "/api/workshop/chat/stop") {
|
|
2008
|
+
let body;
|
|
2009
|
+
try {
|
|
2010
|
+
body = await readJsonBody(req);
|
|
2011
|
+
}
|
|
2012
|
+
catch {
|
|
2013
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
if (!isPlainObject(body)) {
|
|
2017
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
|
|
2021
|
+
json(res, 400, { error: "sessionId is required" });
|
|
2022
|
+
return true;
|
|
2023
|
+
}
|
|
2024
|
+
const stopped = await stopWorkshopChatTurn(workshopChatRegistry, body.sessionId);
|
|
2025
|
+
json(res, 200, { ok: true, ...stopped });
|
|
664
2026
|
return true;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
2027
|
+
}
|
|
2028
|
+
if (method === "POST" && pathname === "/api/workshop/chat") {
|
|
2029
|
+
let body;
|
|
2030
|
+
try {
|
|
2031
|
+
body = await readJsonBody(req);
|
|
2032
|
+
}
|
|
2033
|
+
catch {
|
|
2034
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2035
|
+
return true;
|
|
2036
|
+
}
|
|
2037
|
+
if (!isPlainObject(body)) {
|
|
2038
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
|
|
2042
|
+
json(res, 400, { error: "sessionId is required" });
|
|
2043
|
+
return true;
|
|
2044
|
+
}
|
|
2045
|
+
if (typeof body.message !== "string" || !body.message.trim()) {
|
|
2046
|
+
json(res, 400, { error: "message is required" });
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
const sessionId = body.sessionId;
|
|
2050
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
2051
|
+
if ("ok" in draftParsed) {
|
|
2052
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
2053
|
+
return true;
|
|
2054
|
+
}
|
|
2055
|
+
const settingsDefault = await readWorkshopOpenModel(cwd);
|
|
2056
|
+
const accept = String(req.headers.accept ?? "");
|
|
2057
|
+
const wantsStream = body.stream === true ||
|
|
2058
|
+
accept.includes("application/x-ndjson") ||
|
|
2059
|
+
accept.includes("text/event-stream");
|
|
2060
|
+
const chatTurnBase = {
|
|
2061
|
+
sessionId: body.sessionId,
|
|
2062
|
+
draft: draftParsed,
|
|
2063
|
+
message: body.message,
|
|
2064
|
+
autoApply: body.autoApply === true,
|
|
2065
|
+
model: typeof body.model === "string" || body.model === null
|
|
2066
|
+
? body.model
|
|
2067
|
+
: undefined,
|
|
2068
|
+
settingsDefault: settingsDefault ?? null,
|
|
2069
|
+
registry: workshopChatRegistry,
|
|
2070
|
+
storeRoot: workshopSessionStoreRoot(),
|
|
2071
|
+
};
|
|
2072
|
+
// Fail closed on missing sessions before opening an NDJSON body.
|
|
2073
|
+
try {
|
|
2074
|
+
getWorkshopSession(workshopSessionStoreRoot(), body.sessionId);
|
|
2075
|
+
}
|
|
2076
|
+
catch (err) {
|
|
2077
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
2078
|
+
err.code === "workshop_session_not_found") {
|
|
2079
|
+
json(res, 404, {
|
|
2080
|
+
error: err.message,
|
|
2081
|
+
code: err.code,
|
|
2082
|
+
sessionId: err.sessionId,
|
|
2083
|
+
});
|
|
2084
|
+
return true;
|
|
2085
|
+
}
|
|
2086
|
+
throw err;
|
|
2087
|
+
}
|
|
2088
|
+
if (wantsStream) {
|
|
2089
|
+
res.writeHead(200, {
|
|
2090
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
2091
|
+
"Cache-Control": "no-store",
|
|
2092
|
+
});
|
|
2093
|
+
const resolvedModel = resolveWorkshopModel({
|
|
2094
|
+
sessionOverride: typeof body.model === "string" || body.model === null
|
|
2095
|
+
? body.model
|
|
2096
|
+
: undefined,
|
|
2097
|
+
settingsDefault: settingsDefault ?? null,
|
|
2098
|
+
});
|
|
2099
|
+
const writeFrame = async (frame) => {
|
|
2100
|
+
if (req.aborted || res.writableEnded || res.destroyed)
|
|
2101
|
+
return;
|
|
2102
|
+
const line = `${JSON.stringify(frame)}\n`;
|
|
2103
|
+
if (!res.write(line)) {
|
|
2104
|
+
await new Promise((resolve) => {
|
|
2105
|
+
const onDrain = () => {
|
|
2106
|
+
cleanup();
|
|
2107
|
+
resolve();
|
|
2108
|
+
};
|
|
2109
|
+
const onAbort = () => {
|
|
2110
|
+
cleanup();
|
|
2111
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2112
|
+
res.destroy();
|
|
2113
|
+
}
|
|
2114
|
+
resolve();
|
|
2115
|
+
};
|
|
2116
|
+
const cleanup = () => {
|
|
2117
|
+
res.off("drain", onDrain);
|
|
2118
|
+
req.off("aborted", onAbort);
|
|
2119
|
+
req.off("close", onAbort);
|
|
2120
|
+
res.off("close", onAbort);
|
|
2121
|
+
res.off("error", onAbort);
|
|
2122
|
+
};
|
|
2123
|
+
res.once("drain", onDrain);
|
|
2124
|
+
req.once("aborted", onAbort);
|
|
2125
|
+
req.once("close", onAbort);
|
|
2126
|
+
res.once("close", onAbort);
|
|
2127
|
+
res.once("error", onAbort);
|
|
2128
|
+
if (req.aborted || res.writableEnded || res.destroyed) {
|
|
2129
|
+
onAbort();
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
let streamedDelta = false;
|
|
2135
|
+
let writeChain = Promise.resolve();
|
|
2136
|
+
let turnFinished = false;
|
|
2137
|
+
const enqueueFrame = (frame) => {
|
|
2138
|
+
writeChain = writeChain.then(() => writeFrame(frame));
|
|
2139
|
+
};
|
|
2140
|
+
const stopIfClientLeft = () => {
|
|
2141
|
+
if (turnFinished || res.writableEnded)
|
|
2142
|
+
return;
|
|
2143
|
+
void workshopChatRegistry.abortTurn(sessionId);
|
|
2144
|
+
};
|
|
2145
|
+
res.once("close", stopIfClientLeft);
|
|
2146
|
+
try {
|
|
2147
|
+
const turn = await runWorkshopChatTurn({
|
|
2148
|
+
...chatTurnBase,
|
|
2149
|
+
onDelta: (text) => {
|
|
2150
|
+
streamedDelta = true;
|
|
2151
|
+
enqueueFrame({ type: "delta", text });
|
|
2152
|
+
},
|
|
2153
|
+
onActivity: (update) => {
|
|
2154
|
+
enqueueFrame({ type: "activity", ...update });
|
|
2155
|
+
},
|
|
2156
|
+
onPointerChange: (frame) => {
|
|
2157
|
+
enqueueFrame({
|
|
2158
|
+
type: "pointer-change",
|
|
2159
|
+
buildId: frame.buildId,
|
|
2160
|
+
draft: frame.draft,
|
|
2161
|
+
});
|
|
2162
|
+
},
|
|
2163
|
+
});
|
|
2164
|
+
turnFinished = true;
|
|
2165
|
+
await writeChain;
|
|
2166
|
+
for (const frame of iterateWorkshopChatStreamFrames(turn, {
|
|
2167
|
+
chunkAssistantText: !streamedDelta,
|
|
2168
|
+
})) {
|
|
2169
|
+
await writeFrame(frame);
|
|
2170
|
+
}
|
|
2171
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2172
|
+
res.end();
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
catch (err) {
|
|
2176
|
+
turnFinished = true;
|
|
2177
|
+
await writeChain.catch(() => undefined);
|
|
2178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2179
|
+
const errorEvent = { type: "error", message };
|
|
2180
|
+
await writeFrame({
|
|
2181
|
+
type: "event",
|
|
2182
|
+
event: errorEvent,
|
|
2183
|
+
});
|
|
2184
|
+
await writeFrame({
|
|
2185
|
+
type: "done",
|
|
2186
|
+
sessionId: body.sessionId,
|
|
2187
|
+
events: [errorEvent],
|
|
2188
|
+
draft: draftParsed,
|
|
2189
|
+
pending: null,
|
|
2190
|
+
autoApply: false,
|
|
2191
|
+
model: resolvedModel,
|
|
2192
|
+
buildId: workshopChatRegistry.getPinnedBuildId(body.sessionId),
|
|
2193
|
+
});
|
|
2194
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
2195
|
+
res.end();
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
let turn;
|
|
2201
|
+
try {
|
|
2202
|
+
turn = await runWorkshopChatTurn(chatTurnBase);
|
|
2203
|
+
}
|
|
2204
|
+
catch (err) {
|
|
2205
|
+
if (err instanceof WorkshopSessionStoreError &&
|
|
2206
|
+
err.code === "workshop_session_not_found") {
|
|
2207
|
+
json(res, 404, {
|
|
2208
|
+
error: err.message,
|
|
2209
|
+
code: err.code,
|
|
2210
|
+
sessionId: err.sessionId,
|
|
2211
|
+
});
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
2214
|
+
json(res, 400, {
|
|
2215
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2216
|
+
});
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
json(res, 200, turn);
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
if (method === "POST" &&
|
|
2223
|
+
pathname === "/api/workshop/disk-change") {
|
|
2224
|
+
let body;
|
|
2225
|
+
try {
|
|
2226
|
+
body = await readJsonBody(req);
|
|
2227
|
+
}
|
|
2228
|
+
catch {
|
|
2229
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
if (!isPlainObject(body)) {
|
|
2233
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
if (typeof body.pipelinePath !== "string" || !body.pipelinePath.trim()) {
|
|
2237
|
+
json(res, 400, { error: "pipelinePath is required" });
|
|
2238
|
+
return true;
|
|
2239
|
+
}
|
|
2240
|
+
const draftParsed = parseDraftPackageBody(body);
|
|
2241
|
+
if ("ok" in draftParsed) {
|
|
2242
|
+
json(res, draftParsed.status, { error: draftParsed.error });
|
|
2243
|
+
return true;
|
|
2244
|
+
}
|
|
2245
|
+
const writeRoot = typeof body.project_root === "string"
|
|
2246
|
+
? body.project_root
|
|
2247
|
+
: undefined;
|
|
2248
|
+
let projectRoot;
|
|
2249
|
+
try {
|
|
2250
|
+
const { wireRoot } = await resolveCatalogStartInput({ store, bootCwd: cwd }, writeRoot);
|
|
2251
|
+
const ctx = await resolveStageflowContext(wireRoot.path);
|
|
2252
|
+
projectRoot = ctx.projectRoot;
|
|
2253
|
+
}
|
|
2254
|
+
catch (err) {
|
|
2255
|
+
if (err instanceof CatalogPathError) {
|
|
2256
|
+
json(res, err.code === "catalog_root_read_only" ? 403 : 400, catalogPathErrorBody(err));
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
throw err;
|
|
2260
|
+
}
|
|
2261
|
+
const relativePaths = draftPackageDiskRelativePaths({
|
|
2262
|
+
pipelinePath: body.pipelinePath,
|
|
2263
|
+
draft: draftParsed,
|
|
2264
|
+
taskPath: typeof body.taskPath === "string" ? body.taskPath : null,
|
|
2265
|
+
});
|
|
2266
|
+
const current = fingerprintPackageFiles(projectRoot, relativePaths);
|
|
2267
|
+
const baseline = isPlainObject(body.baseline) &&
|
|
2268
|
+
Object.values(body.baseline).every((v) => typeof v === "string")
|
|
2269
|
+
? body.baseline
|
|
2270
|
+
: null;
|
|
2271
|
+
const detection = detectDiskChange({ baseline, current });
|
|
2272
|
+
json(res, 200, {
|
|
2273
|
+
fingerprints: current,
|
|
2274
|
+
changed: detection.changed,
|
|
2275
|
+
changedPaths: detection.changedPaths,
|
|
2276
|
+
});
|
|
2277
|
+
return true;
|
|
2278
|
+
}
|
|
2279
|
+
if (method === "GET" && pathname === "/api/models") {
|
|
2280
|
+
const filter = url.searchParams.get("project_root") ?? undefined;
|
|
2281
|
+
const result = await listModelsMultiProject({
|
|
2282
|
+
store,
|
|
2283
|
+
bootCwd: cwd,
|
|
2284
|
+
projectRootFilter: filter,
|
|
2285
|
+
});
|
|
2286
|
+
if (result.root_errors.some((e) => e.code === "unknown_project_root") &&
|
|
2287
|
+
result.items.length === 0) {
|
|
2288
|
+
json(res, 400, {
|
|
2289
|
+
error: result.root_errors[0].message,
|
|
2290
|
+
code: "unknown_project_root",
|
|
2291
|
+
root_errors: result.root_errors,
|
|
2292
|
+
});
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
json(res, 200, {
|
|
2296
|
+
models: result.models,
|
|
2297
|
+
entries: result.items,
|
|
2298
|
+
root_errors: result.root_errors,
|
|
2299
|
+
});
|
|
2300
|
+
return true;
|
|
2301
|
+
}
|
|
2302
|
+
if (await handleProviderRoutes(req, res, {
|
|
2303
|
+
cwd,
|
|
2304
|
+
readJsonBody,
|
|
2305
|
+
json,
|
|
2306
|
+
providerAuthContext,
|
|
2307
|
+
})) {
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
if (await handleProjectMcpRoutes(req, res, {
|
|
2311
|
+
projectRoot: rootDir,
|
|
2312
|
+
json,
|
|
2313
|
+
})) {
|
|
2314
|
+
return true;
|
|
2315
|
+
}
|
|
2316
|
+
if (await handleTriggerRoutes(req, res, {
|
|
2317
|
+
cwd,
|
|
2318
|
+
manager,
|
|
2319
|
+
store,
|
|
2320
|
+
json,
|
|
2321
|
+
readJsonBody,
|
|
2322
|
+
readRawBody,
|
|
2323
|
+
auditLog,
|
|
2324
|
+
})) {
|
|
2325
|
+
return true;
|
|
2326
|
+
}
|
|
2327
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
2328
|
+
json(res, 200, await listSkills({ cwd, agentDir }));
|
|
2329
|
+
return true;
|
|
2330
|
+
}
|
|
2331
|
+
if (method === "GET" && pathname === "/api/extensions") {
|
|
2332
|
+
json(res, 200, await listExtensions({ cwd, agentDir }));
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
if (method === "GET" && pathname === "/api/health") {
|
|
2336
|
+
await handleApiHealth(req, res, boot);
|
|
2337
|
+
return true;
|
|
2338
|
+
}
|
|
2339
|
+
if (method === "GET" && pathname === "/api/settings") {
|
|
2340
|
+
const health = manager.getHealth();
|
|
2341
|
+
const credential = getCredentialSourceSettings(cwd);
|
|
2342
|
+
const factory = readFactorySettings(cwd);
|
|
2343
|
+
const defaultModel = await readWorkshopOpenModel(cwd);
|
|
2344
|
+
json(res, 200, {
|
|
2345
|
+
maxConcurrent: health.maxConcurrent,
|
|
2346
|
+
...credential,
|
|
2347
|
+
...(factory.workshopModel !== undefined
|
|
2348
|
+
? { workshopModel: factory.workshopModel }
|
|
2349
|
+
: {}),
|
|
2350
|
+
defaultModel,
|
|
2351
|
+
});
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
if (method === "POST" && pathname === "/api/settings") {
|
|
2355
|
+
let body;
|
|
2356
|
+
try {
|
|
2357
|
+
body = await readJsonBody(req);
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
2361
|
+
return true;
|
|
2362
|
+
}
|
|
2363
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
2364
|
+
? body
|
|
2365
|
+
: {};
|
|
2366
|
+
const hasMax = Object.prototype.hasOwnProperty.call(record, "maxConcurrent");
|
|
2367
|
+
const hasCredentialSource = Object.prototype.hasOwnProperty.call(record, "credentialSource");
|
|
2368
|
+
const hasWorkshopModel = Object.prototype.hasOwnProperty.call(record, "workshopModel");
|
|
2369
|
+
if (!hasMax && !hasCredentialSource && !hasWorkshopModel) {
|
|
2370
|
+
json(res, 400, {
|
|
2371
|
+
error: "maxConcurrent, credentialSource, or workshopModel is required",
|
|
2372
|
+
});
|
|
2373
|
+
return true;
|
|
2374
|
+
}
|
|
2375
|
+
let health = manager.getHealth();
|
|
2376
|
+
if (hasMax) {
|
|
2377
|
+
const n = parseSlotCount(record.maxConcurrent);
|
|
2378
|
+
if (n === undefined) {
|
|
2379
|
+
json(res, 400, { error: INVALID_SLOT_COUNT_MESSAGE });
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
health = manager.setMaxConcurrent(n);
|
|
2383
|
+
}
|
|
2384
|
+
let credential = getCredentialSourceSettings(cwd);
|
|
2385
|
+
if (hasCredentialSource) {
|
|
2386
|
+
try {
|
|
2387
|
+
credential = setCredentialSource(cwd, record.credentialSource);
|
|
2388
|
+
}
|
|
2389
|
+
catch (err) {
|
|
2390
|
+
const mapped = mapProviderAuthError(err);
|
|
2391
|
+
json(res, mapped.status, mapped.body);
|
|
2392
|
+
return true;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
let workshopModel = readFactorySettings(cwd).workshopModel;
|
|
2396
|
+
if (hasWorkshopModel) {
|
|
2397
|
+
const parsed = parseWorkshopModelSetting(record.workshopModel);
|
|
2398
|
+
if (parsed === undefined) {
|
|
2399
|
+
json(res, 400, { error: INVALID_WORKSHOP_MODEL_MESSAGE });
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
writeFactorySettings(cwd, { workshopModel: parsed });
|
|
2403
|
+
workshopModel = parsed;
|
|
2404
|
+
}
|
|
2405
|
+
json(res, 200, {
|
|
2406
|
+
...health,
|
|
2407
|
+
...credential,
|
|
2408
|
+
...(workshopModel !== undefined ? { workshopModel } : {}),
|
|
2409
|
+
defaultModel: await readWorkshopOpenModel(cwd),
|
|
2410
|
+
});
|
|
2411
|
+
return true;
|
|
2412
|
+
}
|
|
2413
|
+
if (method === "GET" && uiDistDir !== undefined) {
|
|
2414
|
+
const served = await serveStatic(res, uiDistDir, pathname);
|
|
2415
|
+
if (served)
|
|
2416
|
+
return true;
|
|
2417
|
+
json(res, 404, {
|
|
2418
|
+
error: "UI not built. Run npm run ui:build, or use Vite dev proxy.",
|
|
2419
|
+
});
|
|
2420
|
+
return true;
|
|
2421
|
+
}
|
|
2422
|
+
json(res, 404, { error: "Not found" });
|
|
668
2423
|
return true;
|
|
669
2424
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
});
|
|
2425
|
+
catch (err) {
|
|
2426
|
+
if (!res.headersSent) {
|
|
2427
|
+
json(res, 500, {
|
|
2428
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
return true;
|
|
678
2432
|
}
|
|
679
|
-
return true;
|
|
680
2433
|
}
|
|
681
2434
|
};
|
|
682
2435
|
}
|
|
@@ -685,21 +2438,45 @@ export async function startUiServer(options) {
|
|
|
685
2438
|
const port = options.port ?? DEFAULT_PORT;
|
|
686
2439
|
const uiDistDir = options.uiDistDir ?? defaultUiDistDir();
|
|
687
2440
|
const boot = await bootstrapStageflowHost(options);
|
|
688
|
-
const {
|
|
2441
|
+
const { cwd, agentDir, rootDir } = boot;
|
|
689
2442
|
const providerAuthContext = boot.providerAuthContext;
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
2443
|
+
const allowedHosts = options.allowedHosts ?? resolveAllowedHosts();
|
|
2444
|
+
const controlTokens = options.controlTokens ?? loadControlTokens();
|
|
2445
|
+
let shutdown;
|
|
2446
|
+
const routes = boot.serveBlocked !== undefined ||
|
|
2447
|
+
boot.manager === undefined ||
|
|
2448
|
+
boot.store === undefined
|
|
2449
|
+
? async () => false
|
|
2450
|
+
: createOperatorRoutes({
|
|
2451
|
+
manager: boot.manager,
|
|
2452
|
+
store: boot.store,
|
|
697
2453
|
cwd,
|
|
698
2454
|
agentDir,
|
|
699
2455
|
rootDir,
|
|
700
2456
|
providerAuthContext,
|
|
701
2457
|
uiDistDir,
|
|
702
|
-
|
|
2458
|
+
allowedHosts,
|
|
2459
|
+
controlTokens,
|
|
2460
|
+
getShutdown: () => shutdown,
|
|
2461
|
+
...(options.workshopOperatorHost
|
|
2462
|
+
? { workshopOperatorHost: options.workshopOperatorHost }
|
|
2463
|
+
: {}),
|
|
2464
|
+
});
|
|
2465
|
+
const envelope = await createHttpHost({
|
|
2466
|
+
boot,
|
|
2467
|
+
host,
|
|
2468
|
+
port,
|
|
2469
|
+
allowedHosts,
|
|
2470
|
+
controlTokens,
|
|
2471
|
+
routes,
|
|
2472
|
+
});
|
|
2473
|
+
shutdown = installShutdownController({
|
|
2474
|
+
server: envelope.server,
|
|
2475
|
+
host: makeDrainableHostFromOptional(envelope.manager, envelope.store),
|
|
2476
|
+
});
|
|
2477
|
+
envelope.server.on("close", () => {
|
|
2478
|
+
shutdown?.uninstall();
|
|
703
2479
|
});
|
|
2480
|
+
return { ...envelope, shutdown };
|
|
704
2481
|
}
|
|
705
2482
|
export { DEFAULT_PORT };
|