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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { shortHash, } from "./browserHost.js";
|
|
2
|
+
export function createFakeRemoteBrowserHost(address) {
|
|
3
|
+
return {
|
|
4
|
+
async ensureProfileBrowser(request) {
|
|
5
|
+
return {
|
|
6
|
+
cdpAddress: address,
|
|
7
|
+
anchorEnv: {
|
|
8
|
+
AGENT_BROWSER_CDP: address,
|
|
9
|
+
AGENT_BROWSER_SESSION: `sf-r-${shortHash(`${request.profile.key.scope}/${request.profile.key.name}`, 12)}`,
|
|
10
|
+
AGENT_BROWSER_IDLE_TIMEOUT_MS: "0",
|
|
11
|
+
},
|
|
12
|
+
restarted: false,
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
async profileBrowserEnv(request) {
|
|
16
|
+
return {
|
|
17
|
+
AGENT_BROWSER_CDP: address,
|
|
18
|
+
AGENT_BROWSER_SESSION: `sf-r-${shortHash(`${request.profile.key.scope}/${request.profile.key.name}`, 12)}`,
|
|
19
|
+
AGENT_BROWSER_IDLE_TIMEOUT_MS: "0",
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
async stageEnv(request) {
|
|
23
|
+
const identity = `${request.runId}/${request.stageId}`;
|
|
24
|
+
const env = {
|
|
25
|
+
AGENT_BROWSER_CDP: request.cdpAddress ?? address,
|
|
26
|
+
AGENT_BROWSER_SESSION: `sf-r-${shortHash(identity, 12)}`,
|
|
27
|
+
AGENT_BROWSER_IDLE_TIMEOUT_MS: "0",
|
|
28
|
+
};
|
|
29
|
+
if (request.profile !== undefined)
|
|
30
|
+
env.AGENT_BROWSER_PIN_TAB = "1";
|
|
31
|
+
return env;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
2
|
+
export type GateHandoff = {
|
|
3
|
+
kind: "local_window";
|
|
4
|
+
} | {
|
|
5
|
+
kind: "live_view";
|
|
6
|
+
url: string;
|
|
7
|
+
};
|
|
8
|
+
/** Host-owned fields on a gate prompt for stages with a `browser`. Never a path. */
|
|
9
|
+
export type HostGateContext = {
|
|
10
|
+
handoff?: GateHandoff;
|
|
11
|
+
site?: string;
|
|
12
|
+
profile?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const HOST_GATE_KEYS: readonly ["handoff", "site", "profile"];
|
|
15
|
+
export declare function parseGateHandoff(value: unknown): GateHandoff | undefined;
|
|
16
|
+
/** Reads the Host-owned fields off a stored or live wait request; unknown shapes yield nothing. */
|
|
17
|
+
export declare function parseHostGateContext(value: unknown): HostGateContext;
|
|
18
|
+
export declare function browserSite(browser: StageBrowserConfig): string | undefined;
|
|
19
|
+
/** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
|
|
20
|
+
export declare function hostGateContextFor(browser: StageBrowserConfig | undefined): HostGateContext | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Overwrites (or, for stages without a browser, strips) the Host-owned keys of
|
|
23
|
+
* a wait request. Non-object requests pass through unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stampGateRequest(request: unknown, context: HostGateContext | undefined): unknown;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const HOST_GATE_KEYS = ["handoff", "site", "profile"];
|
|
2
|
+
export function parseGateHandoff(value) {
|
|
3
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const record = value;
|
|
7
|
+
if (record.kind === "local_window")
|
|
8
|
+
return { kind: "local_window" };
|
|
9
|
+
if (record.kind === "live_view" && typeof record.url === "string") {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(record.url);
|
|
12
|
+
if (url.protocol === "https:" || url.protocol === "http:") {
|
|
13
|
+
return { kind: "live_view", url: record.url };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Reads the Host-owned fields off a stored or live wait request; unknown shapes yield nothing. */
|
|
23
|
+
export function parseHostGateContext(value) {
|
|
24
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const record = value;
|
|
28
|
+
const handoff = parseGateHandoff(record.handoff);
|
|
29
|
+
return {
|
|
30
|
+
...(handoff !== undefined ? { handoff } : {}),
|
|
31
|
+
...(typeof record.site === "string" && record.site !== ""
|
|
32
|
+
? { site: record.site }
|
|
33
|
+
: {}),
|
|
34
|
+
...(typeof record.profile === "string" && record.profile !== ""
|
|
35
|
+
? { profile: record.profile }
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function hostOf(url) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(url).hostname || undefined;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function browserSite(browser) {
|
|
48
|
+
if (browser.check !== undefined) {
|
|
49
|
+
const host = hostOf(browser.check.url);
|
|
50
|
+
if (host !== undefined)
|
|
51
|
+
return host;
|
|
52
|
+
}
|
|
53
|
+
const first = browser.allow_domains?.[0];
|
|
54
|
+
if (first !== undefined)
|
|
55
|
+
return first.replace(/^\*\./, "");
|
|
56
|
+
if (browser.login_url !== undefined)
|
|
57
|
+
return hostOf(browser.login_url);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
|
|
61
|
+
export function hostGateContextFor(browser) {
|
|
62
|
+
if (browser === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const site = browserSite(browser);
|
|
65
|
+
return {
|
|
66
|
+
handoff: { kind: "local_window" },
|
|
67
|
+
...(site !== undefined ? { site } : {}),
|
|
68
|
+
...(browser.profile !== undefined ? { profile: browser.profile } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Overwrites (or, for stages without a browser, strips) the Host-owned keys of
|
|
73
|
+
* a wait request. Non-object requests pass through unchanged.
|
|
74
|
+
*/
|
|
75
|
+
export function stampGateRequest(request, context) {
|
|
76
|
+
if (request === null || typeof request !== "object" || Array.isArray(request)) {
|
|
77
|
+
return request;
|
|
78
|
+
}
|
|
79
|
+
const next = { ...request };
|
|
80
|
+
for (const key of HOST_GATE_KEYS)
|
|
81
|
+
delete next[key];
|
|
82
|
+
return context === undefined ? next : { ...next, ...context };
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
2
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
3
|
+
export type DisplayProbe = () => {
|
|
4
|
+
hasDisplay: boolean;
|
|
5
|
+
docker: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare const NO_SCREEN_MESSAGE = "A visible browser is needed for login, but this Host has no screen. A live view handoff is not available yet.";
|
|
8
|
+
export declare const DOCKER_HINT = " This Host runs in Docker: run the login stage on a machine with a screen, or log in there first and reuse the profile.";
|
|
9
|
+
/**
|
|
10
|
+
* A human login stage is a stage with a `browser` whose after-phase verify
|
|
11
|
+
* contains a `browser_login` item. The Host opens the login page in a visible
|
|
12
|
+
* window and the agent only asks the operator to confirm.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isHumanLoginStage(dag: Pick<ResolvedPipelineDag, "nodes"> | undefined, stageId: string, browser: StageBrowserConfig | undefined): boolean;
|
|
15
|
+
export declare function loginPageUrl(browser: StageBrowserConfig): string | undefined;
|
|
16
|
+
export declare function defaultDisplayProbe(platform?: NodeJS.Platform, env?: Record<string, string | undefined>): ReturnType<DisplayProbe>;
|
|
17
|
+
export declare function noScreenError(docker: boolean): Error;
|
|
18
|
+
export declare function humanLoginPromptBlock(url: string | undefined): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
export const NO_SCREEN_MESSAGE = "A visible browser is needed for login, but this Host has no screen. A live view handoff is not available yet.";
|
|
3
|
+
export const DOCKER_HINT = " This Host runs in Docker: run the login stage on a machine with a screen, or log in there first and reuse the profile.";
|
|
4
|
+
/**
|
|
5
|
+
* A human login stage is a stage with a `browser` whose after-phase verify
|
|
6
|
+
* contains a `browser_login` item. The Host opens the login page in a visible
|
|
7
|
+
* window and the agent only asks the operator to confirm.
|
|
8
|
+
*/
|
|
9
|
+
export function isHumanLoginStage(dag, stageId, browser) {
|
|
10
|
+
if (browser === undefined || dag === undefined)
|
|
11
|
+
return false;
|
|
12
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
13
|
+
return node?.completion?.checks.some((c) => c.type === "browser_login") === true;
|
|
14
|
+
}
|
|
15
|
+
export function loginPageUrl(browser) {
|
|
16
|
+
return browser.login_url ?? browser.check?.url;
|
|
17
|
+
}
|
|
18
|
+
export function defaultDisplayProbe(platform = process.platform, env = process.env) {
|
|
19
|
+
const docker = existsSync("/.dockerenv") || Boolean(env.container) || env.STAGEFLOW_IN_DOCKER === "1";
|
|
20
|
+
const hasDisplay = platform !== "linux" || Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
21
|
+
return { hasDisplay, docker };
|
|
22
|
+
}
|
|
23
|
+
export function noScreenError(docker) {
|
|
24
|
+
return new Error(`${NO_SCREEN_MESSAGE}${docker ? DOCKER_HINT : ""}`);
|
|
25
|
+
}
|
|
26
|
+
export function humanLoginPromptBlock(url) {
|
|
27
|
+
return [
|
|
28
|
+
"## Human login",
|
|
29
|
+
"",
|
|
30
|
+
`The Host has opened ${url !== undefined ? JSON.stringify(url) : "the login page"} in a visible browser window on the operator's screen.`,
|
|
31
|
+
"Do not type credentials and do not drive the browser. Call ask_operator with kind confirm: ask the operator to log in in that window and confirm when done. Leave the browser open.",
|
|
32
|
+
"After the operator accepts, emit the envelope. The Host re-checks the login itself; if it still sees a logged-out session, this stage runs again.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supplies a Host-held secret for protecting browser state at rest. Not wired
|
|
3
|
+
* to agent-browser: its key only covers its own restore/state files, never the
|
|
4
|
+
* Chrome profile directory.
|
|
5
|
+
*/
|
|
6
|
+
export interface KeyProvider {
|
|
7
|
+
getKey(): Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function localKeyFilePath(): string;
|
|
10
|
+
export declare function createLocalKeyProvider(file?: string): KeyProvider;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
export function localKeyFilePath() {
|
|
6
|
+
return path.join(globalStageflowHome(), "browser", "browser.key");
|
|
7
|
+
}
|
|
8
|
+
// TODO(multi-tenant): nothing in the runtime calls a KeyProvider yet, so browser.key is never created outside tests.
|
|
9
|
+
// A service needs a per-tenant key (KMS) and a decision on what it encrypts; the agent-browser key does not protect the Chrome profile.
|
|
10
|
+
export function createLocalKeyProvider(file) {
|
|
11
|
+
return {
|
|
12
|
+
async getKey() {
|
|
13
|
+
const target = file ?? localKeyFilePath();
|
|
14
|
+
try {
|
|
15
|
+
const existing = (await readFile(target, "utf8")).trim();
|
|
16
|
+
if (existing.length > 0) {
|
|
17
|
+
await chmod(target, 0o600);
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err.code !== "ENOENT")
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
26
|
+
const key = randomBytes(32).toString("hex");
|
|
27
|
+
try {
|
|
28
|
+
await writeFile(target, `${key}\n`, { mode: 0o600, flag: "wx" });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err.code === "EEXIST") {
|
|
32
|
+
return (await readFile(target, "utf8")).trim();
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
return key;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BrowserHost } from "./browserHost.js";
|
|
2
|
+
export type LocalBrowserHostOptions = {
|
|
3
|
+
platform?: NodeJS.Platform;
|
|
4
|
+
/** Read only for display detection; never for AGENT_BROWSER_* settings. */
|
|
5
|
+
hostEnv?: Record<string, string | undefined>;
|
|
6
|
+
socketRoot?: string;
|
|
7
|
+
emptyConfigPath?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function defaultSocketRoot(platform: NodeJS.Platform): string;
|
|
10
|
+
export declare function createLocalBrowserHost(options?: LocalBrowserHostOptions): BrowserHost;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
import { shortHash, } from "./browserHost.js";
|
|
6
|
+
const MAX_SOCKET_PATH_BYTES = 103;
|
|
7
|
+
const MAX_SESSION_NAME_LENGTH = 48;
|
|
8
|
+
const DISPLAY_VARS = ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY"];
|
|
9
|
+
export function defaultSocketRoot(platform) {
|
|
10
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "u";
|
|
11
|
+
const base = platform === "win32" ? os.tmpdir() : "/tmp";
|
|
12
|
+
return path.join(base, `sfb-${uid}`);
|
|
13
|
+
}
|
|
14
|
+
function sessionNameForProfile(scope, name) {
|
|
15
|
+
const plain = `sf-${name}`;
|
|
16
|
+
if (plain.length <= MAX_SESSION_NAME_LENGTH)
|
|
17
|
+
return plain;
|
|
18
|
+
return `${plain.slice(0, MAX_SESSION_NAME_LENGTH - 7)}-${shortHash(`${scope}/${name}`, 6)}`;
|
|
19
|
+
}
|
|
20
|
+
function sessionNameForProfileStage(profile, runId, stageId) {
|
|
21
|
+
const suffix = shortHash(`${runId}/${stageId}`, 6);
|
|
22
|
+
const plain = `sf-${profile.key.name}`;
|
|
23
|
+
const room = MAX_SESSION_NAME_LENGTH - suffix.length - 1;
|
|
24
|
+
return `${plain.slice(0, room)}-${suffix}`;
|
|
25
|
+
}
|
|
26
|
+
const OPEN_ANCHOR_TIMEOUT_MS = 60_000;
|
|
27
|
+
function hasDisplay(platform, hostEnv) {
|
|
28
|
+
if (platform !== "linux")
|
|
29
|
+
return true;
|
|
30
|
+
return Boolean(hostEnv.DISPLAY || hostEnv.WAYLAND_DISPLAY);
|
|
31
|
+
}
|
|
32
|
+
export function createLocalBrowserHost(options = {}) {
|
|
33
|
+
const platform = options.platform ?? process.platform;
|
|
34
|
+
const hostEnv = options.hostEnv ?? process.env;
|
|
35
|
+
async function build(input) {
|
|
36
|
+
const socketRoot = options.socketRoot ?? defaultSocketRoot(platform);
|
|
37
|
+
const socketDir = path.join(socketRoot, shortHash(input.identity, 8));
|
|
38
|
+
const socketPathBytes = Buffer.byteLength(path.join(socketDir, `${input.session}.sock`));
|
|
39
|
+
if (socketPathBytes > MAX_SOCKET_PATH_BYTES) {
|
|
40
|
+
throw new Error(`browser socket path is ${socketPathBytes} bytes (limit ${MAX_SOCKET_PATH_BYTES})`);
|
|
41
|
+
}
|
|
42
|
+
await mkdir(socketDir, { recursive: true, mode: 0o700 });
|
|
43
|
+
const emptyConfigPath = options.emptyConfigPath ??
|
|
44
|
+
path.join(globalStageflowHome(), "browser", "agent-browser.empty.json");
|
|
45
|
+
await mkdir(path.dirname(emptyConfigPath), { recursive: true, mode: 0o700 });
|
|
46
|
+
await writeFile(emptyConfigPath, "{}\n", { mode: 0o600 });
|
|
47
|
+
const env = {
|
|
48
|
+
AGENT_BROWSER_SESSION: input.session,
|
|
49
|
+
AGENT_BROWSER_SOCKET_DIR: socketDir,
|
|
50
|
+
AGENT_BROWSER_HEADED: input.headed ? "1" : "0",
|
|
51
|
+
AGENT_BROWSER_IDLE_TIMEOUT_MS: "0",
|
|
52
|
+
AGENT_BROWSER_CONFIG: emptyConfigPath,
|
|
53
|
+
};
|
|
54
|
+
input.set(env);
|
|
55
|
+
if (input.headed && platform === "linux") {
|
|
56
|
+
for (const name of DISPLAY_VARS) {
|
|
57
|
+
const value = hostEnv[name];
|
|
58
|
+
if (value)
|
|
59
|
+
env[name] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return env;
|
|
63
|
+
}
|
|
64
|
+
function headedFor(request) {
|
|
65
|
+
return (request.humanLogin === true ||
|
|
66
|
+
((request.browser.headed ?? true) && hasDisplay(platform, hostEnv)));
|
|
67
|
+
}
|
|
68
|
+
async function profileBrowserEnv(request) {
|
|
69
|
+
const { profile } = request;
|
|
70
|
+
return build({
|
|
71
|
+
identity: `profile:${profile.key.scope}/${profile.key.name}:${profile.profileDir}`,
|
|
72
|
+
session: sessionNameForProfile(profile.key.scope, profile.key.name),
|
|
73
|
+
headed: headedFor(request),
|
|
74
|
+
set: (env) => {
|
|
75
|
+
env.AGENT_BROWSER_PROFILE = profile.profileDir;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function readCdpAddress(request, env) {
|
|
80
|
+
const got = await request.runner(["get", "cdp-url"], env, {
|
|
81
|
+
timeoutMs: OPEN_ANCHOR_TIMEOUT_MS,
|
|
82
|
+
});
|
|
83
|
+
const address = (got.stdout ?? "").trim();
|
|
84
|
+
if (got.code !== 0 || !/^wss?:\/\//.test(address)) {
|
|
85
|
+
throw new Error("could not read the shared browser address (agent-browser get cdp-url)");
|
|
86
|
+
}
|
|
87
|
+
return address;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
async ensureProfileBrowser(request) {
|
|
91
|
+
const wantHeaded = headedFor(request);
|
|
92
|
+
let previous = request.previous;
|
|
93
|
+
if (previous !== undefined &&
|
|
94
|
+
wantHeaded &&
|
|
95
|
+
previous.anchorEnv.AGENT_BROWSER_HEADED !== "1") {
|
|
96
|
+
await request.runner(["close"], previous.anchorEnv).catch(() => undefined);
|
|
97
|
+
previous = undefined;
|
|
98
|
+
}
|
|
99
|
+
if (previous !== undefined) {
|
|
100
|
+
// Any command relaunches a dead anchor, so reading the address is the liveness probe.
|
|
101
|
+
const cdpAddress = await readCdpAddress(request, previous.anchorEnv);
|
|
102
|
+
return {
|
|
103
|
+
cdpAddress,
|
|
104
|
+
anchorEnv: previous.anchorEnv,
|
|
105
|
+
restarted: cdpAddress !== previous.cdpAddress,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const anchorEnv = await profileBrowserEnv(request);
|
|
109
|
+
const opened = await request.runner(["open", "about:blank"], anchorEnv, {
|
|
110
|
+
timeoutMs: OPEN_ANCHOR_TIMEOUT_MS,
|
|
111
|
+
});
|
|
112
|
+
if (opened.code !== 0)
|
|
113
|
+
throw new Error("could not start the shared browser");
|
|
114
|
+
const cdpAddress = await readCdpAddress(request, anchorEnv);
|
|
115
|
+
return {
|
|
116
|
+
cdpAddress,
|
|
117
|
+
anchorEnv,
|
|
118
|
+
restarted: request.previous !== undefined,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
profileBrowserEnv,
|
|
122
|
+
async stageEnv(request) {
|
|
123
|
+
const { browser, profile } = request;
|
|
124
|
+
if (profile !== undefined) {
|
|
125
|
+
if (request.cdpAddress === undefined) {
|
|
126
|
+
throw new Error("a stage with a browser profile needs the shared browser address");
|
|
127
|
+
}
|
|
128
|
+
const cdp = request.cdpAddress;
|
|
129
|
+
return build({
|
|
130
|
+
identity: `stage:${request.runId}/${request.stageId}`,
|
|
131
|
+
session: sessionNameForProfileStage(profile, request.runId, request.stageId),
|
|
132
|
+
headed: headedFor(request),
|
|
133
|
+
set: (env) => {
|
|
134
|
+
env.AGENT_BROWSER_CDP = cdp;
|
|
135
|
+
env.AGENT_BROWSER_PIN_TAB = "1";
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const identity = `stage:${request.runId}/${request.stageId}`;
|
|
140
|
+
return build({
|
|
141
|
+
identity,
|
|
142
|
+
session: `sf-t-${shortHash(identity, 12)}`,
|
|
143
|
+
headed: headedFor(request),
|
|
144
|
+
set: (env) => {
|
|
145
|
+
if (browser.allow_domains && browser.allow_domains.length > 0) {
|
|
146
|
+
env.AGENT_BROWSER_ALLOWED_DOMAINS = browser.allow_domains.join(",");
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
5
|
+
import { lockKey, ownerMatches, sameOwner, } from "./profileLock.js";
|
|
6
|
+
function locksRoot() {
|
|
7
|
+
return path.join(globalStageflowHome(), "browser", "locks");
|
|
8
|
+
}
|
|
9
|
+
function lockFile(key) {
|
|
10
|
+
return path.join(locksRoot(), key.scope, `${key.name}.lock`);
|
|
11
|
+
}
|
|
12
|
+
async function readHolder(file) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
15
|
+
if (typeof parsed.runId === "string") {
|
|
16
|
+
return typeof parsed.stageId === "string"
|
|
17
|
+
? { runId: parsed.runId, stageId: parsed.stageId }
|
|
18
|
+
: { runId: parsed.runId };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// missing or unreadable: treated as free by callers that re-check
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/** A lock file that exists but cannot be parsed (crash, disk full, manual edit). */
|
|
27
|
+
async function isCorrupt(file) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = await readFile(file, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return readHolderFrom(raw) === undefined;
|
|
36
|
+
}
|
|
37
|
+
function readHolderFrom(raw) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (typeof parsed.runId === "string") {
|
|
41
|
+
return typeof parsed.stageId === "string"
|
|
42
|
+
? { runId: parsed.runId, stageId: parsed.stageId }
|
|
43
|
+
: { runId: parsed.runId };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// unparseable
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Removes a corrupt lock file via rename-away; restores it if it turned out valid. */
|
|
52
|
+
async function dropCorrupt(file) {
|
|
53
|
+
const tomb = `${file}.${process.pid}.${randomUUID()}.stale`;
|
|
54
|
+
try {
|
|
55
|
+
await rename(file, tomb);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if ((await readHolder(tomb)) !== undefined) {
|
|
61
|
+
await link(tomb, file).catch(() => undefined);
|
|
62
|
+
await rm(tomb, { force: true });
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
await rm(tomb, { force: true });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/** `link` fails with EEXIST when the lock exists, and the content is complete at creation. */
|
|
69
|
+
async function createExclusive(file, owner) {
|
|
70
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
71
|
+
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
72
|
+
await writeFile(tmp, `${JSON.stringify(owner)}\n`, { mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
await link(tmp, file);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err.code === "EEXIST")
|
|
79
|
+
return false;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await rm(tmp, { force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Removes a lock judged stale. The file is renamed away first and re-read, so
|
|
88
|
+
* a lock another process re-created in the meantime is not dropped.
|
|
89
|
+
*/
|
|
90
|
+
async function dropStale(file, stale) {
|
|
91
|
+
const tomb = `${file}.${process.pid}.${randomUUID()}.stale`;
|
|
92
|
+
try {
|
|
93
|
+
await rename(file, tomb);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
const moved = await readHolder(tomb);
|
|
99
|
+
if (moved !== undefined && !sameOwner(moved, stale)) {
|
|
100
|
+
await link(tomb, file).catch(() => undefined);
|
|
101
|
+
await rm(tomb, { force: true });
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
await rm(tomb, { force: true });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
async function lockFiles() {
|
|
108
|
+
const files = [];
|
|
109
|
+
let scopes;
|
|
110
|
+
try {
|
|
111
|
+
scopes = await readdir(locksRoot());
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return files;
|
|
115
|
+
}
|
|
116
|
+
for (const scope of scopes) {
|
|
117
|
+
let names;
|
|
118
|
+
try {
|
|
119
|
+
names = await readdir(path.join(locksRoot(), scope));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (name.endsWith(".lock"))
|
|
126
|
+
files.push(path.join(locksRoot(), scope, name));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
export function createLocalProfileLock(options = {}) {
|
|
132
|
+
// Serializes this process's operations per lock file; other processes rely on link/rename atomicity.
|
|
133
|
+
const chains = new Map();
|
|
134
|
+
const serialized = (file, task) => {
|
|
135
|
+
const next = (chains.get(file) ?? Promise.resolve())
|
|
136
|
+
.catch(() => undefined)
|
|
137
|
+
.then(task);
|
|
138
|
+
chains.set(file, next);
|
|
139
|
+
void next.finally(() => {
|
|
140
|
+
if (chains.get(file) === next)
|
|
141
|
+
chains.delete(file);
|
|
142
|
+
});
|
|
143
|
+
return next;
|
|
144
|
+
};
|
|
145
|
+
const releaseFile = (file, owner) => serialized(file, async () => {
|
|
146
|
+
const current = await readHolder(file);
|
|
147
|
+
if (current !== undefined && sameOwner(current, owner)) {
|
|
148
|
+
await rm(file, { force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
async acquire(rawKey, owner) {
|
|
153
|
+
const file = lockFile(lockKey(rawKey));
|
|
154
|
+
return serialized(file, async () => {
|
|
155
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
156
|
+
if (await createExclusive(file, owner)) {
|
|
157
|
+
return {
|
|
158
|
+
status: "acquired",
|
|
159
|
+
release: () => releaseFile(file, owner),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const current = await readHolder(file);
|
|
163
|
+
if (current === undefined) {
|
|
164
|
+
if (await isCorrupt(file))
|
|
165
|
+
await dropCorrupt(file);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (sameOwner(current, owner)) {
|
|
169
|
+
return {
|
|
170
|
+
status: "acquired",
|
|
171
|
+
release: () => releaseFile(file, owner),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (options.isRunLive !== undefined &&
|
|
175
|
+
!(await options.isRunLive(current.runId))) {
|
|
176
|
+
if (await dropStale(file, current))
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
return { status: "queued", holder: current };
|
|
180
|
+
}
|
|
181
|
+
const holder = await readHolder(file);
|
|
182
|
+
return {
|
|
183
|
+
status: "queued",
|
|
184
|
+
holder: holder ?? { runId: "unknown" },
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
async holder(rawKey) {
|
|
189
|
+
return readHolder(lockFile(lockKey(rawKey)));
|
|
190
|
+
},
|
|
191
|
+
async releaseOwner(target) {
|
|
192
|
+
for (const file of await lockFiles()) {
|
|
193
|
+
const current = await readHolder(file);
|
|
194
|
+
if (current !== undefined && ownerMatches(current, target)) {
|
|
195
|
+
await releaseFile(file, current);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async reclaimStale(isRunLive) {
|
|
200
|
+
let dropped = 0;
|
|
201
|
+
for (const file of await lockFiles()) {
|
|
202
|
+
const current = await readHolder(file);
|
|
203
|
+
if (current === undefined) {
|
|
204
|
+
if (await isCorrupt(file)) {
|
|
205
|
+
if (await serialized(file, () => dropCorrupt(file)))
|
|
206
|
+
dropped += 1;
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (await isRunLive(current.runId))
|
|
211
|
+
continue;
|
|
212
|
+
if (await serialized(file, () => dropStale(file, current)))
|
|
213
|
+
dropped += 1;
|
|
214
|
+
}
|
|
215
|
+
return dropped;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|