stageflow 0.33.0 → 0.36.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 +3 -3
- package/dist/agent/adapterSpill.d.ts +14 -0
- package/dist/agent/adapterSpill.js +83 -0
- package/dist/agent/agentBackend.d.ts +2 -2
- package/dist/agent/agentBackend.js +2 -2
- package/dist/agent/claudeAdapter.d.ts +13 -2
- package/dist/agent/claudeAdapter.js +56 -6
- package/dist/agent/cursorProvider.d.ts +3 -0
- package/dist/agent/cursorProvider.js +23 -6
- package/dist/agent/fakeAgent.d.ts +3 -3
- package/dist/agent/piAdapter.d.ts +2 -2
- package/dist/agent/piAdapter.js +125 -15
- package/dist/agent/piIsolatedMcp.d.ts +1 -0
- package/dist/agent/piIsolatedMcp.js +19 -1
- package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
- package/dist/agent/piIsolatedMcpProbe.js +20 -0
- package/dist/agent/port.d.ts +4 -2
- package/dist/agent/providerAuth.js +36 -3
- package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -8
- package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +11 -11
- package/dist/agent/stageTurnDriver.d.ts +33 -0
- package/dist/agent/stageTurnDriver.js +64 -0
- package/dist/agent/toolOutputBudget.d.ts +46 -0
- package/dist/agent/toolOutputBudget.js +97 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
- package/dist/agent/toolOutputBudgetExtension.js +141 -0
- package/dist/agent/toolOutputSpill.d.ts +34 -0
- package/dist/agent/toolOutputSpill.js +85 -0
- package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
- package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
- package/dist/browser/anchor.d.ts +2 -0
- package/dist/browser/anchor.js +2 -1
- package/dist/browser/auditSink.d.ts +8 -0
- package/dist/browser/browserHost.d.ts +29 -0
- package/dist/browser/browserSweep.d.ts +4 -1
- package/dist/browser/browserSweep.js +7 -0
- package/dist/browser/browserTeardown.d.ts +1 -0
- package/dist/browser/browserTeardown.js +32 -2
- package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
- package/dist/browser/containerBrowserEndpoint.js +57 -0
- package/dist/browser/containerBrowserHost.d.ts +24 -0
- package/dist/browser/containerBrowserHost.js +147 -0
- package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
- package/dist/browser/dockerSandboxOrchestrator.js +142 -0
- package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
- package/dist/browser/fakeLiveViewRelay.js +94 -0
- package/dist/browser/fakeLiveViewSource.d.ts +6 -0
- package/dist/browser/fakeLiveViewSource.js +16 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
- package/dist/browser/fakeRemoteBrowserHost.js +2 -1
- package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
- package/dist/browser/fakeSandboxOrchestrator.js +44 -0
- package/dist/browser/gateHandoff.d.ts +0 -2
- package/dist/browser/gateHandoff.js +5 -11
- package/dist/browser/hostCapabilities.d.ts +23 -0
- package/dist/browser/hostCapabilities.js +23 -0
- package/dist/browser/humanLogin.d.ts +2 -2
- package/dist/browser/humanLogin.js +11 -2
- package/dist/browser/liveViewPersisted.d.ts +11 -0
- package/dist/browser/liveViewPersisted.js +22 -0
- package/dist/browser/liveViewRelay.d.ts +76 -0
- package/dist/browser/liveViewRelay.js +1 -0
- package/dist/browser/liveViewSessions.d.ts +25 -0
- package/dist/browser/liveViewSessions.js +139 -0
- package/dist/browser/liveViewSource.d.ts +25 -0
- package/dist/browser/liveViewSource.js +7 -0
- package/dist/browser/liveViewTickets.d.ts +46 -0
- package/dist/browser/liveViewTickets.js +122 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +53 -7
- package/dist/browser/ownerScope.d.ts +10 -0
- package/dist/browser/ownerScope.js +6 -0
- package/dist/browser/profileStore.d.ts +5 -2
- package/dist/browser/profileStore.js +3 -5
- package/dist/browser/sandboxOrchestrator.d.ts +60 -0
- package/dist/browser/sandboxOrchestrator.js +8 -0
- package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
- package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
- package/dist/browser/stageBrowserEnv.d.ts +7 -0
- package/dist/browser/stageBrowserEnv.js +53 -15
- package/dist/browser/stageHandoff.d.ts +47 -0
- package/dist/browser/stageHandoff.js +101 -0
- package/dist/browser/stageProfileLock.d.ts +1 -0
- package/dist/browser/stageProfileLock.js +1 -3
- package/dist/browser/volumeProfileStore.d.ts +7 -0
- package/dist/browser/volumeProfileStore.js +35 -0
- package/dist/channels/config.d.ts +11 -0
- package/dist/channels/config.js +95 -0
- package/dist/channels/configWriter.d.ts +40 -0
- package/dist/channels/configWriter.js +148 -0
- package/dist/channels/connectionEdits.d.ts +34 -0
- package/dist/channels/connectionEdits.js +53 -0
- package/dist/channels/errorInfo.d.ts +18 -0
- package/dist/channels/errorInfo.js +71 -0
- package/dist/channels/errors.d.ts +12 -0
- package/dist/channels/errors.js +13 -0
- package/dist/channels/health.d.ts +41 -0
- package/dist/channels/health.js +99 -0
- package/dist/channels/host.d.ts +55 -0
- package/dist/channels/host.js +185 -0
- package/dist/channels/http.d.ts +31 -0
- package/dist/channels/http.js +79 -0
- package/dist/channels/index.d.ts +10 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/ledger.d.ts +37 -0
- package/dist/channels/ledger.js +95 -0
- package/dist/channels/providers/agentmail.d.ts +26 -0
- package/dist/channels/providers/agentmail.js +81 -0
- package/dist/channels/providers/kit.d.ts +40 -0
- package/dist/channels/providers/kit.js +78 -0
- package/dist/channels/providers/photonImessage.d.ts +45 -0
- package/dist/channels/providers/photonImessage.js +152 -0
- package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
- package/dist/channels/providers/photonWhatsapp.js +146 -0
- package/dist/channels/providers/slack.d.ts +25 -0
- package/dist/channels/providers/slack.js +101 -0
- package/dist/channels/providers/telegram.d.ts +21 -0
- package/dist/channels/providers/telegram.js +100 -0
- package/dist/channels/providers.d.ts +5 -0
- package/dist/channels/providers.js +22 -0
- package/dist/channels/registry.d.ts +54 -0
- package/dist/channels/registry.js +186 -0
- package/dist/channels/secretStore.d.ts +21 -0
- package/dist/channels/secretStore.js +75 -0
- package/dist/channels/secrets.d.ts +15 -0
- package/dist/channels/secrets.js +50 -0
- package/dist/channels/service.d.ts +54 -0
- package/dist/channels/service.js +307 -0
- package/dist/channels/stageChannels.d.ts +45 -0
- package/dist/channels/stageChannels.js +110 -0
- package/dist/channels/types.d.ts +142 -0
- package/dist/channels/types.js +1 -0
- package/dist/cli/browserCommand.d.ts +3 -1
- package/dist/cli/browserCommand.js +25 -20
- package/dist/cli/doctorCommand.js +3 -1
- package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
- package/dist/cli/resolveCliProjectRoot.js +27 -0
- package/dist/cli/runCommand.d.ts +1 -1
- package/dist/cli/runCommand.js +59 -17
- package/dist/cli/skillsCommand.d.ts +20 -0
- package/dist/cli/skillsCommand.js +74 -31
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +22 -4
- package/dist/cli.js +4 -4
- package/dist/config/browseCatalog.d.ts +2 -0
- package/dist/config/browseCatalog.js +6 -0
- package/dist/config/catalogRelativePath.d.ts +4 -0
- package/dist/config/catalogRelativePath.js +37 -9
- package/dist/config/createStage.d.ts +1 -0
- package/dist/config/createStage.js +11 -2
- package/dist/config/createTask.d.ts +30 -0
- package/dist/config/createTask.js +161 -0
- package/dist/config/createTrigger.d.ts +15 -0
- package/dist/config/createTrigger.js +32 -25
- package/dist/config/draftPackage.d.ts +42 -0
- package/dist/config/draftPackage.js +220 -23
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
- package/dist/config/ensureCatalogScanRoot.js +6 -2
- package/dist/config/findTaskInCatalog.d.ts +33 -0
- package/dist/config/findTaskInCatalog.js +69 -0
- package/dist/config/hostConfig.d.ts +21 -0
- package/dist/config/hostConfig.js +114 -1
- package/dist/config/importTask.d.ts +19 -0
- package/dist/config/importTask.js +133 -0
- package/dist/config/installSkillRequest.d.ts +40 -0
- package/dist/config/installSkillRequest.js +286 -0
- package/dist/config/loadOutcome.d.ts +11 -6
- package/dist/config/loadPipeline.js +27 -4
- package/dist/config/loadStage.js +17 -0
- package/dist/config/loadStageflowManifest.js +4 -1
- package/dist/config/loadTask.js +2 -0
- package/dist/config/loadTrigger.js +2 -0
- package/dist/config/mergePipelineIncludes.d.ts +3 -0
- package/dist/config/mergePipelineIncludes.js +30 -0
- package/dist/config/normalizePipelineStageEntry.js +10 -1
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/printTargetYaml.js +0 -1
- package/dist/config/publishDraftPackage.d.ts +5 -1
- package/dist/config/publishDraftPackage.js +4 -1
- package/dist/config/resolveCatalogRoots.d.ts +4 -1
- package/dist/config/resolveCatalogRoots.js +18 -1
- package/dist/config/skillUsageIndex.d.ts +6 -0
- package/dist/config/skillUsageIndex.js +39 -0
- package/dist/config/stageBrowser.js +13 -0
- package/dist/config/triggerAdapterStatus.d.ts +11 -0
- package/dist/config/triggerAdapterStatus.js +90 -0
- package/dist/config/triggerCatalog.d.ts +41 -2
- package/dist/config/triggerCatalog.js +107 -15
- package/dist/config/updateTask.d.ts +30 -0
- package/dist/config/updateTask.js +119 -0
- package/dist/config/updateTrigger.d.ts +38 -0
- package/dist/config/updateTrigger.js +220 -0
- package/dist/config/validateCatalog.d.ts +9 -2
- package/dist/config/validateCatalog.js +71 -7
- package/dist/config/workshopCatalogFilenames.d.ts +37 -0
- package/dist/config/workshopCatalogFilenames.js +125 -0
- package/dist/config/workshopCatalogRepair.d.ts +7 -0
- package/dist/config/workshopCatalogRepair.js +144 -0
- package/dist/config/yamlParsePosition.d.ts +4 -0
- package/dist/config/yamlParsePosition.js +9 -0
- package/dist/errors/codes.d.ts +1 -1
- package/dist/errors/codes.js +1 -0
- package/dist/hosted/runtime.d.ts +2 -2
- package/dist/hosted/types.d.ts +1 -1
- package/dist/hosted/types.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/mcp/catalogTools.js +1 -0
- package/dist/mcp/getStartedTool.js +15 -6
- package/dist/mcp/triggerTools.js +2 -8
- package/dist/operatorAgent/agentPort.d.ts +11 -0
- package/dist/operatorAgent/agentPort.js +12 -0
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/piModel.js +26 -6
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/channelChecks.d.ts +17 -0
- package/dist/preflight/channelChecks.js +106 -0
- package/dist/preflight/pipelinePreflight.d.ts +9 -2
- package/dist/preflight/pipelinePreflight.js +9 -1
- package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
- package/dist/project/ensureProjectRootsFromEnv.js +22 -0
- package/dist/project/globalHome.d.ts +1 -0
- package/dist/project/globalHome.js +18 -0
- package/dist/project/workspaceCatalog.d.ts +10 -0
- package/dist/project/workspaceCatalog.js +91 -0
- package/dist/runstore/backup.js +14 -4
- package/dist/runstore/port.d.ts +11 -1
- package/dist/runstore/restore.js +5 -2
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
- package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/runstore/stageSnapshot.d.ts +3 -1
- package/dist/runstore/stageSnapshot.js +19 -0
- package/dist/runstore/workspaceLayout.js +27 -0
- package/dist/runstore/writeContainedArtifact.d.ts +1 -0
- package/dist/runstore/writeContainedArtifact.js +52 -0
- package/dist/runtime/credentialBinding.js +14 -6
- package/dist/runtime/emailSource.js +20 -28
- package/dist/runtime/githubPollSource.js +20 -28
- package/dist/runtime/pipelineRunner.d.ts +4 -4
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/pipelineScheduler.d.ts +2 -2
- package/dist/runtime/pipelineScheduler.js +3 -0
- package/dist/runtime/repositoryMaterialize.d.ts +1 -1
- package/dist/runtime/resumeReconstruct.d.ts +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +2 -2
- package/dist/runtime/runManager.d.ts +18 -2
- package/dist/runtime/runManager.js +99 -31
- package/dist/runtime/runRetryCoordinator.d.ts +2 -2
- package/dist/runtime/scheduleSource.js +15 -24
- package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
- package/dist/runtime/stageAttemptBootstrap.js +26 -1
- package/dist/runtime/stageProcessLauncher.js +33 -3
- package/dist/runtime/stageRoots.js +8 -1
- package/dist/runtime/stageRunner.d.ts +2 -2
- package/dist/runtime/stageRunner.js +8 -2
- package/dist/runtime/stageWorker.js +2 -2
- package/dist/runtime/triggerPort.d.ts +5 -3
- package/dist/runtime/triggerRunner.d.ts +1 -0
- package/dist/runtime/triggerRunner.js +27 -20
- package/dist/runtime/webhookAccept.d.ts +25 -0
- package/dist/runtime/webhookAccept.js +56 -0
- package/dist/server/bootstrap.d.ts +10 -2
- package/dist/server/bootstrap.js +19 -1
- package/dist/server/channelRoutes.d.ts +9 -0
- package/dist/server/channelRoutes.js +88 -0
- package/dist/server/controlToken.d.ts +1 -1
- package/dist/server/controlToken.js +18 -2
- package/dist/server/healthSurfaces.d.ts +12 -0
- package/dist/server/healthSurfaces.js +27 -6
- package/dist/server/http.d.ts +19 -10
- package/dist/server/http.js +616 -85
- package/dist/server/liveViewPath.d.ts +9 -0
- package/dist/server/liveViewPath.js +8 -0
- package/dist/server/liveViewRoutes.d.ts +58 -0
- package/dist/server/liveViewRoutes.js +565 -0
- package/dist/server/mcpHost.d.ts +4 -2
- package/dist/server/mcpHost.js +2 -0
- package/dist/server/readCatalogFile.d.ts +29 -0
- package/dist/server/readCatalogFile.js +215 -0
- package/dist/server/triggerRoutes.d.ts +1 -0
- package/dist/server/triggerRoutes.js +200 -73
- package/dist/tools/sendMessage.d.ts +21 -0
- package/dist/tools/sendMessage.js +25 -0
- package/dist/tools/writeStageArtifact.d.ts +9 -0
- package/dist/tools/writeStageArtifact.js +2 -47
- package/dist/types/pipeline.d.ts +5 -2
- package/dist/types/stage.d.ts +14 -1
- package/dist/types/stageflowManifest.d.ts +1 -1
- package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/assets/index-BOhqnPNg.js +260 -0
- package/dist/ui/assets/{index-Bv4p1aPK.css → index-Clv4sJpU.css} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/workshop/autosave.d.ts +2 -2
- package/dist/workshop/autosave.js +4 -6
- package/dist/workshop/buildStore.d.ts +2 -0
- package/dist/workshop/chatAttachments.d.ts +27 -0
- package/dist/workshop/chatAttachments.js +98 -0
- package/dist/workshop/chatTurn.d.ts +5 -2
- package/dist/workshop/chatTurn.js +22 -18
- package/dist/workshop/sessionMutations.d.ts +17 -0
- package/dist/workshop/sessionMutations.js +107 -0
- package/dist/workshop/sessionStore.d.ts +10 -0
- package/dist/workshop/sessionStore.js +52 -0
- package/dist/workshop/workshopPin.d.ts +4 -3
- package/dist/workshop/workshopPin.js +26 -6
- package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
- package/dist/workshop/workshopTranscriptTurn.js +46 -0
- package/examples/README.md +1 -1
- package/examples/archify-on-pr/README.md +1 -1
- package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
- package/examples/archify-on-pr/author-diagrams.yaml +1 -0
- package/examples/channels-smoke/README.md +12 -0
- package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
- package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
- package/examples/channels-smoke/send-email.yaml +32 -0
- package/examples/channels-smoke/send-text.yaml +31 -0
- package/examples/conditional-fork/branch-a.yaml +1 -1
- package/examples/conditional-fork/decide.yaml +2 -1
- package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
- package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
- package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
- package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
- package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
- package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
- package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
- package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
- package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
- package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
- package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
- package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
- package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
- package/examples/hello-world/hello.pipeline.yaml +2 -2
- package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
- package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
- package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
- package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
- package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
- package/examples/stageflow.yaml +6 -0
- package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
- package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
- package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
- package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
- package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
- package/examples/triggers-readiness/README.md +48 -0
- package/examples/triggers-readiness/readiness-emit.yaml +18 -0
- package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
- package/examples/triggers-readiness/readiness.task.yaml +6 -0
- package/examples/triggers-readiness/stageflow.yaml +13 -0
- package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
- package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
- package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
- package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
- package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
- package/package.json +8 -3
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
- package/skills/stageflow-run/SKILL.md +1 -1
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-setup/SKILL.md +1 -1
- package/dist/ui/assets/index-59cjF0OA.js +0 -133
|
@@ -1,30 +1,70 @@
|
|
|
1
1
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { loadHostConfig } from "../config/hostConfig.js";
|
|
3
|
+
import { browserHostForOwner, loadHostConfig } from "../config/hostConfig.js";
|
|
4
4
|
import { stageDir } from "../runstore/paths.js";
|
|
5
5
|
import { safeAudit } from "./auditSink.js";
|
|
6
6
|
import { assertBrowserSitesAllowed } from "./sitePolicy.js";
|
|
7
7
|
import { BROWSER_ENV_FILENAME, BROWSER_POLICY_FILENAME, } from "./browserHost.js";
|
|
8
8
|
import { ensureRunProfileBrowser } from "./anchor.js";
|
|
9
9
|
import { BROWSER_CLOSED_FILENAME, closeBrowserSession, defaultBrowserRunner, writeSessionOwner, } from "./browserTeardown.js";
|
|
10
|
-
import {
|
|
10
|
+
import { loginPageUrl } from "./humanLogin.js";
|
|
11
|
+
import { persistStageHandoff, resolveStageHandoffCapabilities } from "./stageHandoff.js";
|
|
11
12
|
import { ensureStageLoginCheck, OPEN_COMMAND_TIMEOUT_MS } from "./loginCheck.js";
|
|
12
13
|
import { readPersistedBrowserEnv } from "./persistedEnv.js";
|
|
14
|
+
import { createContainerBrowserHost } from "./containerBrowserHost.js";
|
|
15
|
+
import { createDockerSandboxOrchestrator } from "./dockerSandboxOrchestrator.js";
|
|
16
|
+
import { createVolumeProfileStore } from "./volumeProfileStore.js";
|
|
13
17
|
import { createLocalBrowserHost } from "./localBrowserHost.js";
|
|
14
18
|
import { createLocalProfileStore } from "./localProfileStore.js";
|
|
15
|
-
import {
|
|
19
|
+
import { localOwnerScope } from "./ownerScope.js";
|
|
16
20
|
export { BROWSER_ENV_FILENAME };
|
|
17
21
|
export async function readStagePersistedBrowserEnv(runDir, stageId) {
|
|
18
22
|
return readPersistedBrowserEnv(path.join(stageDir(runDir, stageId), BROWSER_ENV_FILENAME));
|
|
19
23
|
}
|
|
20
24
|
let defaultSupport;
|
|
21
|
-
export function
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
export function hostLaunchOptions() {
|
|
26
|
+
const config = loadHostConfig();
|
|
27
|
+
return {
|
|
28
|
+
launchArgs: config.browserLaunchArgs,
|
|
29
|
+
...(config.browserExecutablePath !== undefined
|
|
30
|
+
? { executablePath: config.browserExecutablePath }
|
|
31
|
+
: {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function configuredBrowserSupport(liveView) {
|
|
35
|
+
const config = loadHostConfig();
|
|
36
|
+
if (browserHostForOwner(config, localOwnerScope()) === "container") {
|
|
37
|
+
const container = config.browserContainer;
|
|
38
|
+
return {
|
|
39
|
+
host: createContainerBrowserHost({
|
|
40
|
+
orchestrator: createDockerSandboxOrchestrator({
|
|
41
|
+
image: container.image,
|
|
42
|
+
shmSize: container.shmSize,
|
|
43
|
+
memory: container.memory,
|
|
44
|
+
pidsLimit: container.pidsLimit,
|
|
45
|
+
user: "1000:1000",
|
|
46
|
+
}),
|
|
47
|
+
liveView,
|
|
48
|
+
local: hostLaunchOptions(),
|
|
49
|
+
}),
|
|
50
|
+
profiles: createVolumeProfileStore(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
host: createLocalBrowserHost({ liveView, ...hostLaunchOptions() }),
|
|
24
55
|
profiles: createLocalProfileStore(),
|
|
25
56
|
};
|
|
57
|
+
}
|
|
58
|
+
export function defaultStageBrowserSupport() {
|
|
59
|
+
defaultSupport ??= configuredBrowserSupport("none");
|
|
26
60
|
return defaultSupport;
|
|
27
61
|
}
|
|
62
|
+
let consoleSupport;
|
|
63
|
+
/** Browser support for a process that serves the live view routes (`sf ui`, `sf mcp`). */
|
|
64
|
+
export function consoleStageBrowserSupport() {
|
|
65
|
+
consoleSupport ??= configuredBrowserSupport("relay");
|
|
66
|
+
return consoleSupport;
|
|
67
|
+
}
|
|
28
68
|
/**
|
|
29
69
|
* Computed once per stage and persisted in the run dir: agent-browser silently
|
|
30
70
|
* relaunches the browser when a command's launch env differs from the daemon's,
|
|
@@ -34,11 +74,9 @@ export async function resolveStageBrowserEnv(support, input) {
|
|
|
34
74
|
const { browser } = input;
|
|
35
75
|
if (browser === undefined)
|
|
36
76
|
return undefined;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
throw noScreenError(screen.docker);
|
|
41
|
-
}
|
|
77
|
+
const capabilities = resolveStageHandoffCapabilities(support, {
|
|
78
|
+
...(input.humanLogin === true ? { humanLogin: true } : {}),
|
|
79
|
+
});
|
|
42
80
|
if ((browser.allow_domains?.length ?? 0) > 0 ||
|
|
43
81
|
browser.check !== undefined ||
|
|
44
82
|
browser.login_url !== undefined) {
|
|
@@ -46,10 +84,11 @@ export async function resolveStageBrowserEnv(support, input) {
|
|
|
46
84
|
}
|
|
47
85
|
const dir = stageDir(input.runDir, input.stageId);
|
|
48
86
|
const file = path.join(dir, BROWSER_ENV_FILENAME);
|
|
49
|
-
|
|
87
|
+
await mkdir(dir, { recursive: true });
|
|
88
|
+
await persistStageHandoff({ runDir: input.runDir, stageId: input.stageId }, capabilities);
|
|
50
89
|
const profile = browser.profile !== undefined
|
|
51
90
|
? await support.profiles.open({
|
|
52
|
-
scope:
|
|
91
|
+
scope: input.scope,
|
|
53
92
|
name: browser.profile,
|
|
54
93
|
})
|
|
55
94
|
: undefined;
|
|
@@ -97,10 +136,9 @@ export async function resolveStageBrowserEnv(support, input) {
|
|
|
97
136
|
if (persisted === undefined) {
|
|
98
137
|
await writeEnv(fresh);
|
|
99
138
|
if (browser.profile !== undefined) {
|
|
100
|
-
// TODO(multi-tenant): audit the run owner's scope.
|
|
101
139
|
await safeAudit(support.audit, {
|
|
102
140
|
event: "profile_used",
|
|
103
|
-
scope:
|
|
141
|
+
scope: input.scope,
|
|
104
142
|
profile: browser.profile,
|
|
105
143
|
runId: input.runId,
|
|
106
144
|
stageId: input.stageId,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { StageBrowserConfig } from "../types/stage.js";
|
|
2
|
+
import type { StageBrowserSupport } from "./browserHost.js";
|
|
3
|
+
import { type GateHandoff, type HostGateContext } from "./gateHandoff.js";
|
|
4
|
+
import { type BrowserHostCapabilities } from "./hostCapabilities.js";
|
|
5
|
+
/** The slice of the host capability record a stage's handoff depends on; persisted per stage. */
|
|
6
|
+
export type StageHandoffCapabilities = Pick<BrowserHostCapabilities, "display" | "liveView">;
|
|
7
|
+
type HandoffInput = {
|
|
8
|
+
runDir: string;
|
|
9
|
+
runId: string;
|
|
10
|
+
stageId: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Probes the display, applies the host's capabilities, and rejects a human-login
|
|
14
|
+
* stage that has neither a screen nor a live view. Pure of disk writes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveStageHandoffCapabilities(support: StageBrowserSupport, options?: {
|
|
17
|
+
humanLogin?: boolean;
|
|
18
|
+
}): StageHandoffCapabilities;
|
|
19
|
+
export declare function persistStageHandoff(input: {
|
|
20
|
+
runDir: string;
|
|
21
|
+
stageId: string;
|
|
22
|
+
}, capabilities: StageHandoffCapabilities): Promise<void>;
|
|
23
|
+
/** Persisted capabilities of a stage; undefined when none were recorded (older runs, direct stage runs). */
|
|
24
|
+
export declare function readStageHandoff(input: {
|
|
25
|
+
runDir: string;
|
|
26
|
+
stageId: string;
|
|
27
|
+
}): Promise<StageHandoffCapabilities | undefined>;
|
|
28
|
+
export declare function gateHandoffFor(context: {
|
|
29
|
+
runId: string;
|
|
30
|
+
stageId: string;
|
|
31
|
+
capabilities: StageHandoffCapabilities;
|
|
32
|
+
}): GateHandoff | undefined;
|
|
33
|
+
/** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
|
|
34
|
+
export declare function hostGateContextFor(browser: StageBrowserConfig | undefined, context: {
|
|
35
|
+
runId: string;
|
|
36
|
+
stageId: string;
|
|
37
|
+
capabilities: StageHandoffCapabilities;
|
|
38
|
+
}): HostGateContext | undefined;
|
|
39
|
+
/** The handoff for a stage's gates and login prompt. `dirStageId` is the manifest id when it differs from the gate's stage id. */
|
|
40
|
+
export declare function stageGateHandoff(input: HandoffInput & {
|
|
41
|
+
dirStageId?: string;
|
|
42
|
+
}): Promise<GateHandoff | undefined>;
|
|
43
|
+
/** Host-owned gate fields for a stage; undefined for a stage without a browser. */
|
|
44
|
+
export declare function stageGateContext(input: HandoffInput & {
|
|
45
|
+
browser: StageBrowserConfig | undefined;
|
|
46
|
+
}): Promise<HostGateContext | undefined>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stageDir } from "../runstore/paths.js";
|
|
4
|
+
import { browserSite, } from "./gateHandoff.js";
|
|
5
|
+
import { resolveBrowserHostCapabilities, } from "./hostCapabilities.js";
|
|
6
|
+
import { defaultDisplayProbe, noScreenError } from "./humanLogin.js";
|
|
7
|
+
const BROWSER_CAPABILITIES_FILENAME = "browser-capabilities.json";
|
|
8
|
+
/**
|
|
9
|
+
* Probes the display, applies the host's capabilities, and rejects a human-login
|
|
10
|
+
* stage that has neither a screen nor a live view. Pure of disk writes.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveStageHandoffCapabilities(support, options = {}) {
|
|
13
|
+
const capabilities = resolveBrowserHostCapabilities(support.host.capabilities);
|
|
14
|
+
const probe = support.display ?? defaultDisplayProbe;
|
|
15
|
+
if (support.host.capabilities === undefined) {
|
|
16
|
+
capabilities.display = probe().hasDisplay ? "local_window" : "headless_only";
|
|
17
|
+
}
|
|
18
|
+
else if (capabilities.display === "local_window" && support.display !== undefined && !support.display().hasDisplay) {
|
|
19
|
+
capabilities.display = "headless_only";
|
|
20
|
+
}
|
|
21
|
+
if (options.humanLogin === true &&
|
|
22
|
+
capabilities.display === "headless_only" &&
|
|
23
|
+
capabilities.liveView === "none") {
|
|
24
|
+
throw noScreenError(probe().docker);
|
|
25
|
+
}
|
|
26
|
+
if (options.humanLogin === true &&
|
|
27
|
+
capabilities.display !== "local_window" &&
|
|
28
|
+
capabilities.viewerInput === "view_only") {
|
|
29
|
+
throw new Error("A human login stage needs an interactive live view, but this browser host offers a view-only one (missing capability viewer_input: interactive).");
|
|
30
|
+
}
|
|
31
|
+
return { display: capabilities.display, liveView: capabilities.liveView };
|
|
32
|
+
}
|
|
33
|
+
export async function persistStageHandoff(input, capabilities) {
|
|
34
|
+
const dir = stageDir(input.runDir, input.stageId);
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(path.join(dir, BROWSER_CAPABILITIES_FILENAME), `${JSON.stringify({ display: capabilities.display, liveView: capabilities.liveView })}\n`, { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
/** Persisted capabilities of a stage; undefined when none were recorded (older runs, direct stage runs). */
|
|
39
|
+
export async function readStageHandoff(input) {
|
|
40
|
+
const file = path.join(stageDir(input.runDir, input.stageId), BROWSER_CAPABILITIES_FILENAME);
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
43
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
44
|
+
const { display, liveView } = resolveBrowserHostCapabilities(parsed);
|
|
45
|
+
return { display, liveView };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err.code !== "ENOENT")
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
export function gateHandoffFor(context) {
|
|
55
|
+
const { capabilities } = context;
|
|
56
|
+
if (capabilities.display === "local_window")
|
|
57
|
+
return { kind: "local_window" };
|
|
58
|
+
if (capabilities.liveView !== "none") {
|
|
59
|
+
return {
|
|
60
|
+
kind: "live_view",
|
|
61
|
+
url: `/api/runs/${encodeURIComponent(context.runId)}/stages/${encodeURIComponent(context.stageId)}/live-view`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
|
|
67
|
+
export function hostGateContextFor(browser, context) {
|
|
68
|
+
if (browser === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
const site = browserSite(browser);
|
|
71
|
+
const handoff = gateHandoffFor(context);
|
|
72
|
+
return {
|
|
73
|
+
...(handoff !== undefined ? { handoff } : {}),
|
|
74
|
+
...(site !== undefined ? { site } : {}),
|
|
75
|
+
...(browser.profile !== undefined ? { profile: browser.profile } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// No record (older runs, direct stage runs) keeps its historical meaning: a local window.
|
|
79
|
+
const LEGACY_CAPABILITIES = { display: "local_window", liveView: "none" };
|
|
80
|
+
async function capabilitiesOf(input) {
|
|
81
|
+
return ((await readStageHandoff({ runDir: input.runDir, stageId: input.dirStageId ?? input.stageId })) ??
|
|
82
|
+
LEGACY_CAPABILITIES);
|
|
83
|
+
}
|
|
84
|
+
/** The handoff for a stage's gates and login prompt. `dirStageId` is the manifest id when it differs from the gate's stage id. */
|
|
85
|
+
export async function stageGateHandoff(input) {
|
|
86
|
+
return gateHandoffFor({
|
|
87
|
+
runId: input.runId,
|
|
88
|
+
stageId: input.stageId,
|
|
89
|
+
capabilities: await capabilitiesOf(input),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Host-owned gate fields for a stage; undefined for a stage without a browser. */
|
|
93
|
+
export async function stageGateContext(input) {
|
|
94
|
+
if (input.browser === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return hostGateContextFor(input.browser, {
|
|
97
|
+
runId: input.runId,
|
|
98
|
+
stageId: input.stageId,
|
|
99
|
+
capabilities: await capabilitiesOf(input),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { StageBrowserSupport } from "./browserHost.js";
|
|
|
2
2
|
import type { ProfileLock, ProfileLockOwner, RunLiveness } from "./profileLock.js";
|
|
3
3
|
export declare function stageProfileLock(support: StageBrowserSupport): ProfileLock;
|
|
4
4
|
export type ProfileWaitInput = {
|
|
5
|
+
scope: string;
|
|
5
6
|
profile: string;
|
|
6
7
|
owner: ProfileLockOwner;
|
|
7
8
|
isRunLive?: RunLiveness;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createLocalProfileLock } from "./localProfileLock.js";
|
|
2
|
-
import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
|
|
3
2
|
const DEFAULT_LOCK_POLL_MS = 250;
|
|
4
3
|
let defaultLock;
|
|
5
4
|
export function stageProfileLock(support) {
|
|
@@ -14,8 +13,7 @@ export function stageProfileLock(support) {
|
|
|
14
13
|
*/
|
|
15
14
|
export async function acquireStageProfile(support, input) {
|
|
16
15
|
const locks = stageProfileLock(support);
|
|
17
|
-
|
|
18
|
-
const key = { scope: LOCAL_BROWSER_SCOPE, name: input.profile };
|
|
16
|
+
const key = { scope: input.scope, name: input.profile };
|
|
19
17
|
const pollMs = support.lockPollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
20
18
|
let announced;
|
|
21
19
|
for (;;) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ProfileStore } from "./profileStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Profiles that live in a volume the sandbox orchestrator provisions. The handle
|
|
4
|
+
* carries a reference, never a folder; creating, sizing and removing the volume
|
|
5
|
+
* belong to the orchestrator, so `delete` only forgets the reference.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createVolumeProfileStore(): ProfileStore;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { validateProfileKey, validateProfileScope, } from "./profileStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Profiles that live in a volume the sandbox orchestrator provisions. The handle
|
|
4
|
+
* carries a reference, never a folder; creating, sizing and removing the volume
|
|
5
|
+
* belong to the orchestrator, so `delete` only forgets the reference.
|
|
6
|
+
*/
|
|
7
|
+
export function createVolumeProfileStore() {
|
|
8
|
+
const scopes = new Map();
|
|
9
|
+
return {
|
|
10
|
+
async open(rawKey) {
|
|
11
|
+
const key = validateProfileKey(rawKey);
|
|
12
|
+
let profiles = scopes.get(key.scope);
|
|
13
|
+
if (!profiles) {
|
|
14
|
+
profiles = new Map();
|
|
15
|
+
scopes.set(key.scope, profiles);
|
|
16
|
+
}
|
|
17
|
+
let handle = profiles.get(key.name);
|
|
18
|
+
if (!handle) {
|
|
19
|
+
handle = { key, volumeRef: `${key.scope}/${key.name}` };
|
|
20
|
+
profiles.set(key.name, handle);
|
|
21
|
+
}
|
|
22
|
+
return handle;
|
|
23
|
+
},
|
|
24
|
+
async list(scope) {
|
|
25
|
+
return [...(scopes.get(validateProfileScope(scope))?.keys() ?? [])].sort();
|
|
26
|
+
},
|
|
27
|
+
async delete(rawKey) {
|
|
28
|
+
const key = validateProfileKey(rawKey);
|
|
29
|
+
scopes.get(key.scope)?.delete(key.name);
|
|
30
|
+
},
|
|
31
|
+
async deleteScope(scope) {
|
|
32
|
+
scopes.delete(validateProfileScope(scope));
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConnectionEntry, CredentialEntry } from "./types.js";
|
|
2
|
+
export declare const CONNECTIONS_FILE_LIMIT_BYTES = 262144;
|
|
3
|
+
export declare const MAX_CONNECTIONS = 100;
|
|
4
|
+
export declare const MAX_CREDENTIALS = 20;
|
|
5
|
+
export type ConnectionsFile = {
|
|
6
|
+
credentials: CredentialEntry[];
|
|
7
|
+
connections: ConnectionEntry[];
|
|
8
|
+
};
|
|
9
|
+
export declare function parseConnectionsYaml(text: string): ConnectionsFile;
|
|
10
|
+
/** A missing file is an empty configuration. Any other fault stops startup instead of half-applying. */
|
|
11
|
+
export declare function loadConnectionsFile(file: string): ConnectionsFile;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ChannelError } from "./errors.js";
|
|
5
|
+
import { SECRET_REF_PATTERN } from "./secrets.js";
|
|
6
|
+
export const CONNECTIONS_FILE_LIMIT_BYTES = 262144;
|
|
7
|
+
export const MAX_CONNECTIONS = 100;
|
|
8
|
+
export const MAX_CREDENTIALS = 20;
|
|
9
|
+
const idSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/);
|
|
10
|
+
const providerSchema = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
11
|
+
const secretRefSchema = z.string().regex(SECRET_REF_PATTERN);
|
|
12
|
+
const secretFieldSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/);
|
|
13
|
+
const secretsSchema = z.record(secretFieldSchema, secretRefSchema).default({});
|
|
14
|
+
const credentialSchema = z.object({
|
|
15
|
+
id: idSchema,
|
|
16
|
+
provider: providerSchema,
|
|
17
|
+
secrets: secretsSchema,
|
|
18
|
+
}).strict();
|
|
19
|
+
const connectionSchema = z.object({
|
|
20
|
+
id: idSchema,
|
|
21
|
+
provider: providerSchema,
|
|
22
|
+
displayName: z.string().min(1).max(200).optional(),
|
|
23
|
+
enabled: z.boolean().default(true),
|
|
24
|
+
credential: idSchema.optional(),
|
|
25
|
+
config: z.record(z.string(), z.unknown()).default({}),
|
|
26
|
+
secrets: secretsSchema,
|
|
27
|
+
}).strict();
|
|
28
|
+
const fileSchema = z.object({
|
|
29
|
+
version: z.literal(1),
|
|
30
|
+
credentials: z.array(credentialSchema).max(MAX_CREDENTIALS).default([]),
|
|
31
|
+
connections: z.array(connectionSchema).max(MAX_CONNECTIONS).default([]),
|
|
32
|
+
}).strict();
|
|
33
|
+
export function parseConnectionsYaml(text) {
|
|
34
|
+
const document = parseDocument(text, { uniqueKeys: true, strict: true });
|
|
35
|
+
if (document.errors.length || document.warnings.length)
|
|
36
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = document.toJS({ maxAliasCount: 0 });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
43
|
+
}
|
|
44
|
+
const parsed = fileSchema.safeParse(raw);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
47
|
+
assertUnique(parsed.data.credentials.map(entry => entry.id));
|
|
48
|
+
assertUnique(parsed.data.connections.map(entry => entry.id));
|
|
49
|
+
return {
|
|
50
|
+
credentials: parsed.data.credentials.map(entry => ({ ...entry, secrets: entry.secrets })),
|
|
51
|
+
connections: parsed.data.connections.map(entry => ({ ...entry, secrets: entry.secrets })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function assertUnique(ids) {
|
|
55
|
+
if (new Set(ids).size !== ids.length)
|
|
56
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
57
|
+
}
|
|
58
|
+
/** A missing file is an empty configuration. Any other fault stops startup instead of half-applying. */
|
|
59
|
+
export function loadConnectionsFile(file) {
|
|
60
|
+
let descriptor;
|
|
61
|
+
try {
|
|
62
|
+
descriptor = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code === "ENOENT")
|
|
66
|
+
return { credentials: [], connections: [] };
|
|
67
|
+
throw new ChannelError("CHANNEL_STORAGE_FAILED");
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const stat = fstatSync(descriptor);
|
|
71
|
+
if (!stat.isFile())
|
|
72
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
73
|
+
if (stat.size > CONNECTIONS_FILE_LIMIT_BYTES)
|
|
74
|
+
throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
|
|
75
|
+
const bytes = Buffer.alloc(CONNECTIONS_FILE_LIMIT_BYTES + 1);
|
|
76
|
+
let length = 0;
|
|
77
|
+
while (length < bytes.length) {
|
|
78
|
+
const count = readSync(descriptor, bytes, length, bytes.length - length, null);
|
|
79
|
+
if (!count)
|
|
80
|
+
break;
|
|
81
|
+
length += count;
|
|
82
|
+
}
|
|
83
|
+
if (length > CONNECTIONS_FILE_LIMIT_BYTES)
|
|
84
|
+
throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
|
|
85
|
+
return parseConnectionsYaml(bytes.toString("utf8", 0, length));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof ChannelError)
|
|
89
|
+
throw error;
|
|
90
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
closeSync(descriptor);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ConnectionEntry, CredentialEntry } from "./types.js";
|
|
2
|
+
/** Plain-object form of an entry. Optional fields are omitted when unset. */
|
|
3
|
+
export type ConnectionYamlEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
credential?: string;
|
|
9
|
+
config?: Record<string, unknown>;
|
|
10
|
+
secrets?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export declare function toYamlEntry(entry: Pick<ConnectionEntry, "id" | "provider" | "enabled" | "config" | "secrets"> & {
|
|
13
|
+
displayName?: string;
|
|
14
|
+
credential?: string;
|
|
15
|
+
}): ConnectionYamlEntry;
|
|
16
|
+
/** Edits keep comments and key order that the user wrote. */
|
|
17
|
+
export declare function addConnectionYaml(text: string, entry: ConnectionYamlEntry, credential?: {
|
|
18
|
+
id: string;
|
|
19
|
+
provider: string;
|
|
20
|
+
secrets: Record<string, string>;
|
|
21
|
+
}): string;
|
|
22
|
+
export type ConnectionPatch = {
|
|
23
|
+
displayName?: string | null;
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
config?: Record<string, unknown>;
|
|
26
|
+
secrets?: Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
export declare function updateConnectionYaml(text: string, id: string, patch: ConnectionPatch): string;
|
|
29
|
+
/** Removes the entry and any credential that no remaining connection uses. Returns what was dropped. */
|
|
30
|
+
export declare function removeConnectionYaml(text: string, id: string): {
|
|
31
|
+
text: string;
|
|
32
|
+
removedCredential?: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function readConnectionsText(file: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Replaces the file atomically. A bind-mounted file cannot be renamed over, so that case falls
|
|
37
|
+
* back to writing in place.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeConnectionsText(file: string, text: string): void;
|
|
40
|
+
export type { CredentialEntry };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSeq, parseDocument } from "yaml";
|
|
4
|
+
import { CONNECTIONS_FILE_LIMIT_BYTES } from "./config.js";
|
|
5
|
+
import { ChannelError } from "./errors.js";
|
|
6
|
+
export function toYamlEntry(entry) {
|
|
7
|
+
return {
|
|
8
|
+
id: entry.id,
|
|
9
|
+
provider: entry.provider,
|
|
10
|
+
...(entry.displayName ? { displayName: entry.displayName } : {}),
|
|
11
|
+
...(entry.enabled === false ? { enabled: false } : {}),
|
|
12
|
+
...(entry.credential ? { credential: entry.credential } : {}),
|
|
13
|
+
...(Object.keys(entry.config).length ? { config: entry.config } : {}),
|
|
14
|
+
...(Object.keys(entry.secrets).length ? { secrets: entry.secrets } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function open(text) {
|
|
18
|
+
const source = text.trim().length ? text : "version: 1\n";
|
|
19
|
+
const document = parseDocument(source, { uniqueKeys: true, strict: true });
|
|
20
|
+
if (document.errors.length)
|
|
21
|
+
throw new ChannelError("CHANNEL_INVALID_INPUT");
|
|
22
|
+
return document;
|
|
23
|
+
}
|
|
24
|
+
function sequence(document, key) {
|
|
25
|
+
let seq = document.get(key, true);
|
|
26
|
+
if (!isSeq(seq)) {
|
|
27
|
+
document.set(key, document.createNode([]));
|
|
28
|
+
seq = document.get(key, true);
|
|
29
|
+
}
|
|
30
|
+
const result = seq;
|
|
31
|
+
result.flow = false;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function indexOf(seq, id) {
|
|
35
|
+
return seq.items.findIndex(item => item?.get?.("id") === id);
|
|
36
|
+
}
|
|
37
|
+
/** Edits keep comments and key order that the user wrote. */
|
|
38
|
+
export function addConnectionYaml(text, entry, credential) {
|
|
39
|
+
const document = open(text);
|
|
40
|
+
if (credential) {
|
|
41
|
+
const credentials = sequence(document, "credentials");
|
|
42
|
+
if (indexOf(credentials, credential.id) < 0)
|
|
43
|
+
credentials.add(document.createNode({ id: credential.id, provider: credential.provider, secrets: credential.secrets }));
|
|
44
|
+
}
|
|
45
|
+
const connections = sequence(document, "connections");
|
|
46
|
+
if (indexOf(connections, entry.id) >= 0)
|
|
47
|
+
throw new ChannelError("CHANNEL_OPERATION_CONFLICT", false, { connectionId: entry.id });
|
|
48
|
+
connections.add(document.createNode(entry));
|
|
49
|
+
return document.toString();
|
|
50
|
+
}
|
|
51
|
+
export function updateConnectionYaml(text, id, patch) {
|
|
52
|
+
const document = open(text);
|
|
53
|
+
const connections = sequence(document, "connections");
|
|
54
|
+
const index = indexOf(connections, id);
|
|
55
|
+
if (index < 0)
|
|
56
|
+
throw new ChannelError("CHANNEL_NOT_FOUND", false, { connectionId: id });
|
|
57
|
+
const node = connections.items[index];
|
|
58
|
+
if (patch.displayName !== undefined) {
|
|
59
|
+
if (patch.displayName === null || patch.displayName === "")
|
|
60
|
+
node.delete("displayName");
|
|
61
|
+
else
|
|
62
|
+
node.set("displayName", patch.displayName);
|
|
63
|
+
}
|
|
64
|
+
if (patch.enabled !== undefined) {
|
|
65
|
+
if (patch.enabled)
|
|
66
|
+
node.delete("enabled");
|
|
67
|
+
else
|
|
68
|
+
node.set("enabled", false);
|
|
69
|
+
}
|
|
70
|
+
if (patch.config !== undefined) {
|
|
71
|
+
if (Object.keys(patch.config).length)
|
|
72
|
+
node.set("config", document.createNode(patch.config));
|
|
73
|
+
else
|
|
74
|
+
node.delete("config");
|
|
75
|
+
}
|
|
76
|
+
for (const [field, ref] of Object.entries(patch.secrets ?? {}))
|
|
77
|
+
node.setIn(["secrets", field], ref);
|
|
78
|
+
return document.toString();
|
|
79
|
+
}
|
|
80
|
+
/** Removes the entry and any credential that no remaining connection uses. Returns what was dropped. */
|
|
81
|
+
export function removeConnectionYaml(text, id) {
|
|
82
|
+
const document = open(text);
|
|
83
|
+
const connections = sequence(document, "connections");
|
|
84
|
+
const index = indexOf(connections, id);
|
|
85
|
+
if (index < 0)
|
|
86
|
+
throw new ChannelError("CHANNEL_NOT_FOUND", false, { connectionId: id });
|
|
87
|
+
const credentialId = connections.items[index].get("credential");
|
|
88
|
+
connections.items.splice(index, 1);
|
|
89
|
+
let removedCredential;
|
|
90
|
+
if (typeof credentialId === "string") {
|
|
91
|
+
const stillUsed = connections.items.some(item => item.get("credential") === credentialId);
|
|
92
|
+
const credentials = document.get("credentials", true);
|
|
93
|
+
if (!stillUsed && isSeq(credentials)) {
|
|
94
|
+
const at = indexOf(credentials, credentialId);
|
|
95
|
+
if (at >= 0) {
|
|
96
|
+
credentials.items.splice(at, 1);
|
|
97
|
+
removedCredential = credentialId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { text: document.toString(), ...(removedCredential ? { removedCredential } : {}) };
|
|
102
|
+
}
|
|
103
|
+
export function readConnectionsText(file) {
|
|
104
|
+
if (!existsSync(file))
|
|
105
|
+
return "";
|
|
106
|
+
const text = readFileSync(file, "utf8");
|
|
107
|
+
if (Buffer.byteLength(text) > CONNECTIONS_FILE_LIMIT_BYTES)
|
|
108
|
+
throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Replaces the file atomically. A bind-mounted file cannot be renamed over, so that case falls
|
|
113
|
+
* back to writing in place.
|
|
114
|
+
*/
|
|
115
|
+
export function writeConnectionsText(file, text) {
|
|
116
|
+
if (Buffer.byteLength(text) > CONNECTIONS_FILE_LIMIT_BYTES)
|
|
117
|
+
throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
120
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(temporary, text, { mode: 0o644 });
|
|
123
|
+
renameSync(temporary, file);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const code = error.code;
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(temporary);
|
|
129
|
+
}
|
|
130
|
+
catch { /* nothing to clean */ }
|
|
131
|
+
if (code !== "EBUSY" && code !== "EXDEV" && code !== "EPERM")
|
|
132
|
+
throw error;
|
|
133
|
+
const descriptor = openSync(file, constants.O_WRONLY | constants.O_TRUNC);
|
|
134
|
+
try {
|
|
135
|
+
writeSync(descriptor, text);
|
|
136
|
+
fsyncSync(descriptor);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
closeSync(descriptor);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof ChannelError)
|
|
145
|
+
throw error;
|
|
146
|
+
throw new ChannelError("CHANNEL_STORAGE_FAILED");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** What the edit module needs from the secret store. `ChannelSecretStore` satisfies it. */
|
|
2
|
+
export interface SecretWriter {
|
|
3
|
+
get(name: string): string | undefined;
|
|
4
|
+
set(name: string, value: string): void;
|
|
5
|
+
delete(name: string): void;
|
|
6
|
+
deleteWithPrefix(prefix: string): void;
|
|
7
|
+
}
|
|
8
|
+
export type ConnectionEdit = {
|
|
9
|
+
/** Secret store name to value. Written before the file so a reference never points at nothing. */
|
|
10
|
+
secrets?: Readonly<Record<string, string>>;
|
|
11
|
+
/** Pure transform of the file text. May name secret prefixes to drop once the edit has committed. */
|
|
12
|
+
rewrite(text: string): {
|
|
13
|
+
text: string;
|
|
14
|
+
dropSecretPrefixes?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type ConnectionEditsOptions = {
|
|
18
|
+
file: () => string;
|
|
19
|
+
store: SecretWriter;
|
|
20
|
+
/** Throws if the new text would not load. Runs before anything is written to the file. */
|
|
21
|
+
validate: (text: string) => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* One atomic change to connections: secrets and file together. Either both change or neither
|
|
25
|
+
* does. A secret that already existed is restored to its previous value, not deleted, and a
|
|
26
|
+
* failure while writing, validating or saving leaves nothing half-applied. Dropping the secrets
|
|
27
|
+
* of a removed connection happens only after the file has been saved, and a failure there only
|
|
28
|
+
* leaves an orphaned secret behind, never a broken connection.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ConnectionEdits {
|
|
31
|
+
private readonly options;
|
|
32
|
+
constructor(options: ConnectionEditsOptions);
|
|
33
|
+
apply(edit: ConnectionEdit): void;
|
|
34
|
+
}
|