stageflow 0.32.0 → 0.35.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 +4 -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 +50 -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/fakeAgent.js +0 -6
- package/dist/agent/piAdapter.d.ts +2 -2
- package/dist/agent/piAdapter.js +116 -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 +2 -2
- package/dist/agent/providerAuth.d.ts +6 -0
- package/dist/agent/providerAuth.js +86 -17
- package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -6
- package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +13 -9
- 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/cli/browserCommand.d.ts +3 -1
- package/dist/cli/browserCommand.js +25 -20
- 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 +41 -0
- package/dist/config/draftPackage.js +216 -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 +113 -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/listConfig.d.ts +0 -16
- package/dist/config/listConfig.js +0 -3
- package/dist/config/loadOutcome.d.ts +11 -6
- package/dist/config/loadPipeline.js +0 -1
- package/dist/config/loadStage.js +2 -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.js +3 -0
- package/dist/config/normalizePipelineStageEntry.js +10 -1
- 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 +6 -2
- package/dist/config/validateCatalog.js +48 -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/hosted/containment.d.ts +3 -0
- package/dist/hosted/containment.js +36 -0
- package/dist/hosted/errors.d.ts +21 -0
- package/dist/hosted/errors.js +30 -0
- package/dist/hosted/packageSkills.d.ts +3 -0
- package/dist/hosted/packageSkills.js +80 -0
- package/dist/hosted/runtime.d.ts +12 -0
- package/dist/hosted/runtime.js +596 -0
- package/dist/hosted/sequencer.d.ts +28 -0
- package/dist/hosted/sequencer.js +129 -0
- package/dist/hosted/translate.d.ts +3 -0
- package/dist/hosted/translate.js +43 -0
- package/dist/hosted/types.d.ts +108 -0
- package/dist/hosted/types.js +10 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -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/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/projection/projectRun.d.ts +0 -1
- package/dist/projection/projectRun.js +0 -1
- 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/containerLimits.d.ts +0 -3
- package/dist/runtime/containerLimits.js +0 -5
- package/dist/runtime/credentialBinding.d.ts +3 -0
- package/dist/runtime/credentialBinding.js +23 -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/pipelineScheduler.d.ts +2 -2
- package/dist/runtime/pipelineScheduler.js +3 -0
- 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 +95 -30
- package/dist/runtime/runRetentionSweep.d.ts +0 -2
- package/dist/runtime/runRetentionSweep.js +0 -8
- 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 +10 -1
- package/dist/runtime/stageProcessLauncher.js +2 -2
- package/dist/runtime/stageRoots.js +12 -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 +7 -2
- package/dist/server/bootstrap.js +15 -1
- 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 +605 -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/readEmail.d.ts +3 -3
- package/dist/tools/writeStageArtifact.d.ts +9 -0
- package/dist/tools/writeStageArtifact.js +2 -47
- package/dist/types/pipeline.d.ts +1 -1
- package/dist/types/stage.d.ts +1 -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-BoFPkKFg.js +256 -0
- package/dist/ui/assets/{index-Bv4p1aPK.css → index-CIPbbBUi.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/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 +9 -1
- 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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type LiveViewRouteKind = "ticket" | "events" | "input" | "dialog" | "reopen-tab" | "viewer";
|
|
2
|
+
export type LiveViewRouteMatch = {
|
|
3
|
+
kind: LiveViewRouteKind;
|
|
4
|
+
/** Raw (still percent-encoded) path segments. */
|
|
5
|
+
rawRunId: string;
|
|
6
|
+
rawStageId: string;
|
|
7
|
+
};
|
|
8
|
+
/** Sole definition of the live-view path shape; path-only, method is checked by each consumer. */
|
|
9
|
+
export declare function matchLiveViewRoute(pathname: string): LiveViewRouteMatch | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const LIVE_VIEW_ROUTE = /^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/live-view\/(ticket|events|input|dialog|reopen-tab|viewer)$/;
|
|
2
|
+
/** Sole definition of the live-view path shape; path-only, method is checked by each consumer. */
|
|
3
|
+
export function matchLiveViewRoute(pathname) {
|
|
4
|
+
const m = LIVE_VIEW_ROUTE.exec(pathname);
|
|
5
|
+
if (m === null)
|
|
6
|
+
return null;
|
|
7
|
+
return { rawRunId: m[1], rawStageId: m[2], kind: m[3] };
|
|
8
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type AuditSink } from "../browser/auditSink.js";
|
|
3
|
+
import type { LiveViewSource, SandboxIdResolver } from "../browser/liveViewSource.js";
|
|
4
|
+
import type { LiveViewRelay } from "../browser/liveViewRelay.js";
|
|
5
|
+
import { type LiveViewSessionManager, type LiveViewSessionManagerOptions } from "../browser/liveViewSessions.js";
|
|
6
|
+
import { type LiveViewTicketService } from "../browser/liveViewTickets.js";
|
|
7
|
+
import type { RunManager } from "../runtime/runManager.js";
|
|
8
|
+
import type { RunStore } from "../runstore/port.js";
|
|
9
|
+
import { type ControlTokens } from "./controlToken.js";
|
|
10
|
+
export declare const LIVE_VIEW_COOKIE = "sf_live_view";
|
|
11
|
+
export declare const LIVE_VIEW_CSRF_HEADER = "x-stageflow-live-view";
|
|
12
|
+
export declare const LIVE_VIEW_MAX_INPUT_BYTES: number;
|
|
13
|
+
export declare const LIVE_VIEW_MAX_DIALOG_BYTES: number;
|
|
14
|
+
export declare const LIVE_VIEW_MAX_REOPEN_BYTES = 256;
|
|
15
|
+
export declare const LIVE_VIEW_HEARTBEAT_MS = 15000;
|
|
16
|
+
export declare function isLiveViewPath(pathname: string): boolean;
|
|
17
|
+
export declare function isLiveViewInputPath(method: string, pathname: string): boolean;
|
|
18
|
+
export declare function isLiveViewReopenTabPath(method: string, pathname: string): boolean;
|
|
19
|
+
export declare function isLiveViewDialogPath(method: string, pathname: string): boolean;
|
|
20
|
+
export declare function applyLiveViewHeaders(res: ServerResponse): void;
|
|
21
|
+
export type LiveViewRoutesOptions = {
|
|
22
|
+
store: RunStore;
|
|
23
|
+
controlTokens: ControlTokens;
|
|
24
|
+
relay?: LiveViewRelay;
|
|
25
|
+
/** Serves the `viewer` route for stages whose host reports `live_view: provider_view`. */
|
|
26
|
+
viewer?: {
|
|
27
|
+
source: LiveViewSource;
|
|
28
|
+
sandboxIdOf: SandboxIdResolver;
|
|
29
|
+
};
|
|
30
|
+
audit?: AuditSink;
|
|
31
|
+
tickets?: LiveViewTicketService;
|
|
32
|
+
sessions?: LiveViewSessionManager;
|
|
33
|
+
/** Owner scope of the request. Defaults to the fixed local scope. */
|
|
34
|
+
resolveScope?: (req: IncomingMessage) => string;
|
|
35
|
+
graceMs?: LiveViewSessionManagerOptions["graceMs"];
|
|
36
|
+
setTimer?: LiveViewSessionManagerOptions["setTimer"];
|
|
37
|
+
clearTimer?: LiveViewSessionManagerOptions["clearTimer"];
|
|
38
|
+
now?: () => number;
|
|
39
|
+
/** Production relay only: how long an unanswered page dialog waits before the Host dismisses it. */
|
|
40
|
+
dialogTimeoutMs?: number;
|
|
41
|
+
heartbeatMs?: number;
|
|
42
|
+
setHeartbeat?: (fn: () => void, ms: number) => unknown;
|
|
43
|
+
clearHeartbeat?: (handle: unknown) => void;
|
|
44
|
+
};
|
|
45
|
+
export type LiveViewRoutes = {
|
|
46
|
+
handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
47
|
+
/** Revokes the stage's credentials, then resolves once its relay session is closed. */
|
|
48
|
+
revokeStage(runId: string, stageId: string): Promise<void>;
|
|
49
|
+
revokeRun(runId: string): Promise<void>;
|
|
50
|
+
/** A gate stopped taking input: revokes control only, so read-only watchers keep watching the running stage. */
|
|
51
|
+
revokeStageControl(runId: string, stageId: string): Promise<void>;
|
|
52
|
+
/** Ends every stream, revokes every credential and closes every session; the Host keeps running. */
|
|
53
|
+
dispose(): Promise<void>;
|
|
54
|
+
/** Wires revoke-on-gate-close, revoke-before-browser-teardown and dispose-on-shutdown; returns an unsubscribe. */
|
|
55
|
+
attach(manager: LiveViewManagerHooks): () => void;
|
|
56
|
+
};
|
|
57
|
+
export type LiveViewManagerHooks = Pick<RunManager, "onGateClosed" | "beforeBrowserTeardown" | "onShutdown">;
|
|
58
|
+
export declare function createLiveViewRoutes(options: LiveViewRoutesOptions): LiveViewRoutes;
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { safeAudit } from "../browser/auditSink.js";
|
|
2
|
+
import { parseHostGateContext } from "../browser/gateHandoff.js";
|
|
3
|
+
import { readLiveViewSessionRequest } from "../browser/liveViewPersisted.js";
|
|
4
|
+
import { readStageHandoff } from "../browser/stageHandoff.js";
|
|
5
|
+
import { loadHostConfig } from "../config/hostConfig.js";
|
|
6
|
+
import { createAgentBrowserLiveViewRelay } from "../browser/agentBrowserLiveViewRelay.js";
|
|
7
|
+
import { defaultBrowserRunner } from "../browser/browserTeardown.js";
|
|
8
|
+
import { createLiveViewSessionManager, } from "../browser/liveViewSessions.js";
|
|
9
|
+
import { createLiveViewTicketService, } from "../browser/liveViewTickets.js";
|
|
10
|
+
import { localOwnerScope } from "../browser/ownerScope.js";
|
|
11
|
+
import { readStagePersistedBrowserEnv } from "../browser/stageBrowserEnv.js";
|
|
12
|
+
import { matchLiveViewRoute } from "./liveViewPath.js";
|
|
13
|
+
import { enforceBearerAuth } from "./controlToken.js";
|
|
14
|
+
export const LIVE_VIEW_COOKIE = "sf_live_view";
|
|
15
|
+
export const LIVE_VIEW_CSRF_HEADER = "x-stageflow-live-view";
|
|
16
|
+
export const LIVE_VIEW_MAX_INPUT_BYTES = 64 * 1024;
|
|
17
|
+
export const LIVE_VIEW_MAX_DIALOG_BYTES = 8 * 1024;
|
|
18
|
+
export const LIVE_VIEW_MAX_REOPEN_BYTES = 256;
|
|
19
|
+
export const LIVE_VIEW_HEARTBEAT_MS = 15_000;
|
|
20
|
+
const MAX_TICKET_BODY_BYTES = 1024;
|
|
21
|
+
const STREAMED = new Set(["frame", "status", "tabs", "url", "retarget", "dialog", "dialog_closed"]);
|
|
22
|
+
const INPUT_STATUS = {
|
|
23
|
+
batch_too_large: 413,
|
|
24
|
+
rate_limited: 429,
|
|
25
|
+
invalid_event: 400,
|
|
26
|
+
not_array: 400,
|
|
27
|
+
closed: 409,
|
|
28
|
+
upstream_unavailable: 409,
|
|
29
|
+
};
|
|
30
|
+
export function isLiveViewPath(pathname) {
|
|
31
|
+
return matchLiveViewRoute(pathname) !== null;
|
|
32
|
+
}
|
|
33
|
+
export function isLiveViewInputPath(method, pathname) {
|
|
34
|
+
return method === "POST" && matchLiveViewRoute(pathname)?.kind === "input";
|
|
35
|
+
}
|
|
36
|
+
const DIALOG_STATUS = {
|
|
37
|
+
no_dialog: 409,
|
|
38
|
+
not_answerable: 400,
|
|
39
|
+
invalid: 400,
|
|
40
|
+
closed: 409,
|
|
41
|
+
};
|
|
42
|
+
const REOPEN_STATUS = {
|
|
43
|
+
closed: 409,
|
|
44
|
+
no_tab: 409,
|
|
45
|
+
upstream_unavailable: 409,
|
|
46
|
+
rate_limited: 429,
|
|
47
|
+
failed: 502,
|
|
48
|
+
};
|
|
49
|
+
export function isLiveViewReopenTabPath(method, pathname) {
|
|
50
|
+
return method === "POST" && matchLiveViewRoute(pathname)?.kind === "reopen-tab";
|
|
51
|
+
}
|
|
52
|
+
export function isLiveViewDialogPath(method, pathname) {
|
|
53
|
+
return method === "POST" && matchLiveViewRoute(pathname)?.kind === "dialog";
|
|
54
|
+
}
|
|
55
|
+
export function applyLiveViewHeaders(res) {
|
|
56
|
+
res.setHeader("Cache-Control", "no-store");
|
|
57
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
58
|
+
res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
|
|
59
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
60
|
+
}
|
|
61
|
+
function send(res, status, body) {
|
|
62
|
+
const payload = JSON.stringify(body);
|
|
63
|
+
res.writeHead(status, {
|
|
64
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
65
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
66
|
+
});
|
|
67
|
+
res.end(payload);
|
|
68
|
+
}
|
|
69
|
+
function readCookie(req, name) {
|
|
70
|
+
const header = req.headers.cookie;
|
|
71
|
+
if (typeof header !== "string")
|
|
72
|
+
return undefined;
|
|
73
|
+
for (const part of header.split(";")) {
|
|
74
|
+
const eq = part.indexOf("=");
|
|
75
|
+
if (eq === -1)
|
|
76
|
+
continue;
|
|
77
|
+
if (part.slice(0, eq).trim() === name)
|
|
78
|
+
return part.slice(eq + 1).trim();
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
function isSecureRequest(req) {
|
|
83
|
+
if (req.socket.encrypted === true)
|
|
84
|
+
return true;
|
|
85
|
+
const forwarded = req.headers["x-forwarded-proto"];
|
|
86
|
+
const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim();
|
|
87
|
+
return first?.toLowerCase() === "https";
|
|
88
|
+
}
|
|
89
|
+
/** Reads at most `max` bytes; resolves undefined when the body is larger (the rest is discarded). */
|
|
90
|
+
function readLimitedBody(req, max) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
let tooLarge = false;
|
|
95
|
+
req.on("data", (chunk) => {
|
|
96
|
+
if (tooLarge)
|
|
97
|
+
return;
|
|
98
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
99
|
+
size += buf.length;
|
|
100
|
+
if (size > max) {
|
|
101
|
+
tooLarge = true;
|
|
102
|
+
chunks.length = 0;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
chunks.push(buf);
|
|
106
|
+
});
|
|
107
|
+
req.on("end", () => resolve(tooLarge ? undefined : Buffer.concat(chunks)));
|
|
108
|
+
req.on("error", reject);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function configuredDialogTimeoutMs() {
|
|
112
|
+
try {
|
|
113
|
+
return loadHostConfig().browserDialogTimeoutSeconds * 1000;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function createLiveViewRoutes(options) {
|
|
120
|
+
const { store, controlTokens } = options;
|
|
121
|
+
const tickets = options.tickets ?? createLiveViewTicketService({ now: options.now });
|
|
122
|
+
const resolveScope = options.resolveScope ?? (() => localOwnerScope());
|
|
123
|
+
const heartbeatMs = options.heartbeatMs ?? LIVE_VIEW_HEARTBEAT_MS;
|
|
124
|
+
const setHeartbeat = options.setHeartbeat ??
|
|
125
|
+
((fn, ms) => {
|
|
126
|
+
const handle = setInterval(fn, ms);
|
|
127
|
+
handle.unref();
|
|
128
|
+
return handle;
|
|
129
|
+
});
|
|
130
|
+
const clearHeartbeat = options.clearHeartbeat ?? ((handle) => clearInterval(handle));
|
|
131
|
+
let relay = options.relay;
|
|
132
|
+
const sessions = options.sessions ??
|
|
133
|
+
createLiveViewSessionManager({
|
|
134
|
+
relay: {
|
|
135
|
+
open: (request) => {
|
|
136
|
+
relay ??= createAgentBrowserLiveViewRelay({
|
|
137
|
+
runner: defaultBrowserRunner,
|
|
138
|
+
dialogTimeoutMs: options.dialogTimeoutMs ?? configuredDialogTimeoutMs(),
|
|
139
|
+
});
|
|
140
|
+
return relay.open(request);
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
graceMs: options.graceMs,
|
|
144
|
+
setTimer: options.setTimer,
|
|
145
|
+
clearTimer: options.clearTimer,
|
|
146
|
+
request: async (runId, stageId) => {
|
|
147
|
+
const detail = await store.readRun(runId);
|
|
148
|
+
const stage = detail.stages.find((s) => s.stage_id === stageId);
|
|
149
|
+
return readLiveViewSessionRequest({
|
|
150
|
+
runDir: store.getWorkspaceDir(runId),
|
|
151
|
+
runId,
|
|
152
|
+
stageId,
|
|
153
|
+
profile: parseHostGateContext(stage?.pending_prompt).profile,
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
async function issueTicket(req, res, target) {
|
|
158
|
+
const base = enforceBearerAuth(controlTokens, req, res, "read");
|
|
159
|
+
if (!base.ok)
|
|
160
|
+
return;
|
|
161
|
+
const raw = await readLimitedBody(req, MAX_TICKET_BODY_BYTES);
|
|
162
|
+
let mode;
|
|
163
|
+
try {
|
|
164
|
+
mode = raw === undefined ? undefined : JSON.parse(raw.toString("utf8") || "{}").mode;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
mode = undefined;
|
|
168
|
+
}
|
|
169
|
+
if (mode !== "view" && mode !== "control") {
|
|
170
|
+
send(res, 400, { error: 'mode must be "view" or "control"' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const auth = mode === "control" ? enforceBearerAuth(controlTokens, req, res, "drive") : base;
|
|
174
|
+
if (!auth.ok)
|
|
175
|
+
return;
|
|
176
|
+
let detail;
|
|
177
|
+
try {
|
|
178
|
+
detail = await store.readRun(target.runId);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
send(res, 404, { error: `Run not found: ${target.runId}` });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const stage = detail.stages.find((s) => s.stage_id === target.stageId);
|
|
185
|
+
if (stage === undefined) {
|
|
186
|
+
send(res, 404, { error: `Stage not found: ${target.stageId}` });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (mode === "control") {
|
|
190
|
+
const handoff = parseHostGateContext(stage.pending_prompt).handoff;
|
|
191
|
+
if (stage.status !== "waiting_for_input" || handoff?.kind !== "live_view") {
|
|
192
|
+
send(res, 409, {
|
|
193
|
+
error: "Control is only available while the stage waits at a live view gate",
|
|
194
|
+
code: "no_live_view_gate",
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const live = stage.status === "running" || stage.status === "waiting_for_input";
|
|
201
|
+
const env = live
|
|
202
|
+
? await readStagePersistedBrowserEnv(store.getWorkspaceDir(target.runId), target.stageId).catch(() => undefined)
|
|
203
|
+
: undefined;
|
|
204
|
+
if (env === undefined) {
|
|
205
|
+
send(res, 409, {
|
|
206
|
+
error: "Stage has no browser to show",
|
|
207
|
+
code: "no_browser",
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const issued = tickets.issue({
|
|
213
|
+
...target,
|
|
214
|
+
mode,
|
|
215
|
+
callerId: auth.auth?.caller_id ?? null,
|
|
216
|
+
});
|
|
217
|
+
send(res, 200, {
|
|
218
|
+
ticket: issued.ticket,
|
|
219
|
+
expires_at: new Date(issued.expiresAt).toISOString(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function audit(event, grant) {
|
|
223
|
+
void safeAudit(options.audit, {
|
|
224
|
+
event,
|
|
225
|
+
scope: grant.scope,
|
|
226
|
+
runId: grant.runId,
|
|
227
|
+
stageId: grant.stageId,
|
|
228
|
+
mode: grant.mode,
|
|
229
|
+
callerId: grant.callerId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function openStream(req, res, target, pathPrefix, ticket) {
|
|
233
|
+
let grant = ticket !== null ? tickets.redeem(ticket, target) : undefined;
|
|
234
|
+
const redeemed = grant !== undefined;
|
|
235
|
+
if (grant === undefined) {
|
|
236
|
+
const cookie = readCookie(req, LIVE_VIEW_COOKIE);
|
|
237
|
+
grant = cookie !== undefined ? tickets.resolve(cookie, target) : undefined;
|
|
238
|
+
}
|
|
239
|
+
if (grant === undefined) {
|
|
240
|
+
send(res, 401, { error: "Invalid or expired live view ticket", code: "ticket_invalid" });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const credential = grant.credential;
|
|
244
|
+
let lease;
|
|
245
|
+
try {
|
|
246
|
+
lease = await sessions.acquire(target.runId, target.stageId);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
send(res, 409, { error: "Live view is unavailable", code: "live_view_unavailable" });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (lease === undefined) {
|
|
253
|
+
send(res, 409, { error: "Stage has no browser to show", code: "no_browser" });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (tickets.resolve(credential, target) === undefined) {
|
|
257
|
+
lease.release();
|
|
258
|
+
send(res, 401, { error: "Live view session ended", code: "session_ended" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const headers = {
|
|
262
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
263
|
+
Connection: "keep-alive",
|
|
264
|
+
"X-Accel-Buffering": "no",
|
|
265
|
+
};
|
|
266
|
+
if (redeemed) {
|
|
267
|
+
headers["Set-Cookie"] =
|
|
268
|
+
`${LIVE_VIEW_COOKIE}=${credential}; Path=${pathPrefix}; HttpOnly; SameSite=Strict${isSecureRequest(req) ? "; Secure" : ""}`;
|
|
269
|
+
}
|
|
270
|
+
res.writeHead(200, headers);
|
|
271
|
+
res.flushHeaders();
|
|
272
|
+
audit("live_view_opened", grant);
|
|
273
|
+
const activeGrant = grant;
|
|
274
|
+
let ended = false;
|
|
275
|
+
const heartbeat = setHeartbeat(() => {
|
|
276
|
+
if (!ended)
|
|
277
|
+
res.write(": keepalive\n\n");
|
|
278
|
+
}, heartbeatMs);
|
|
279
|
+
let unsubscribe = () => undefined;
|
|
280
|
+
let stopRevoked = () => undefined;
|
|
281
|
+
const finish = (final) => {
|
|
282
|
+
if (ended)
|
|
283
|
+
return;
|
|
284
|
+
ended = true;
|
|
285
|
+
clearHeartbeat(heartbeat);
|
|
286
|
+
unsubscribe();
|
|
287
|
+
stopRevoked();
|
|
288
|
+
lease.release();
|
|
289
|
+
audit("live_view_closed", activeGrant);
|
|
290
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
291
|
+
if (final !== undefined)
|
|
292
|
+
res.write(`event: ${final.type}\ndata: ${JSON.stringify(final.data)}\n\n`);
|
|
293
|
+
res.end();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
stopRevoked = tickets.onRevoked((revoked) => {
|
|
297
|
+
if (revoked.includes(credential))
|
|
298
|
+
finish({ type: "closed", data: { reason: "revoked" } });
|
|
299
|
+
});
|
|
300
|
+
res.on("close", () => finish());
|
|
301
|
+
unsubscribe = lease.session.subscribe((message) => {
|
|
302
|
+
if (ended)
|
|
303
|
+
return;
|
|
304
|
+
if (message.type === "closed") {
|
|
305
|
+
finish(message);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!STREAMED.has(message.type))
|
|
309
|
+
return;
|
|
310
|
+
if (message.type === "frame" && res.writableNeedDrain)
|
|
311
|
+
return;
|
|
312
|
+
res.write(`event: ${message.type}\ndata: ${JSON.stringify(message.data)}\n\n`);
|
|
313
|
+
});
|
|
314
|
+
if (ended)
|
|
315
|
+
unsubscribe();
|
|
316
|
+
}
|
|
317
|
+
async function openViewer(req, res, target, pathPrefix, ticket) {
|
|
318
|
+
let grant = ticket !== null ? tickets.redeem(ticket, target) : undefined;
|
|
319
|
+
const redeemed = grant !== undefined;
|
|
320
|
+
if (grant === undefined) {
|
|
321
|
+
const cookie = readCookie(req, LIVE_VIEW_COOKIE);
|
|
322
|
+
grant = cookie !== undefined ? tickets.resolve(cookie, target) : undefined;
|
|
323
|
+
}
|
|
324
|
+
if (grant === undefined) {
|
|
325
|
+
send(res, 401, { error: "Invalid or expired live view ticket", code: "ticket_invalid" });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const runDir = store.getWorkspaceDir(target.runId);
|
|
329
|
+
const handoff = await readStageHandoff({ runDir, stageId: target.stageId }).catch(() => undefined);
|
|
330
|
+
if (handoff?.liveView !== "provider_view" || options.viewer === undefined) {
|
|
331
|
+
send(res, 409, { error: "This stage's browser has no provider viewer", code: "no_provider_view" });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
let address;
|
|
335
|
+
try {
|
|
336
|
+
const detail = await store.readRun(target.runId);
|
|
337
|
+
const stage = detail.stages.find((s) => s.stage_id === target.stageId);
|
|
338
|
+
const sandboxId = await options.viewer.sandboxIdOf({
|
|
339
|
+
scope: target.scope,
|
|
340
|
+
runId: target.runId,
|
|
341
|
+
stageId: target.stageId,
|
|
342
|
+
profile: parseHostGateContext(stage?.pending_prompt).profile,
|
|
343
|
+
});
|
|
344
|
+
if (sandboxId === undefined) {
|
|
345
|
+
send(res, 409, { error: "Stage has no browser to show", code: "no_browser" });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
address = await options.viewer.source.viewerAddress({
|
|
349
|
+
runId: target.runId,
|
|
350
|
+
stageId: target.stageId,
|
|
351
|
+
sandboxId,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
send(res, 409, { error: "Live view is unavailable", code: "live_view_unavailable" });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (tickets.resolve(grant.credential, target) === undefined) {
|
|
359
|
+
send(res, 401, { error: "Live view session ended", code: "session_ended" });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (redeemed) {
|
|
363
|
+
res.setHeader("Set-Cookie", `${LIVE_VIEW_COOKIE}=${grant.credential}; Path=${pathPrefix}; HttpOnly; SameSite=Strict${isSecureRequest(req) ? "; Secure" : ""}`);
|
|
364
|
+
}
|
|
365
|
+
audit("live_view_opened", grant);
|
|
366
|
+
send(res, 200, {
|
|
367
|
+
url: address.url,
|
|
368
|
+
embed_origin: address.embedOrigin,
|
|
369
|
+
expires_at: new Date(address.expiresAt).toISOString(),
|
|
370
|
+
mode: grant.mode,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/** Cookie credential, anti-forgery header, control mode, then a size-limited JSON body. */
|
|
374
|
+
async function readControlBody(req, res, target, max, viewOnlyError, tooLargeError, allowEmpty = false) {
|
|
375
|
+
const cookie = readCookie(req, LIVE_VIEW_COOKIE);
|
|
376
|
+
const grant = cookie !== undefined ? tickets.resolve(cookie, target) : undefined;
|
|
377
|
+
if (grant === undefined) {
|
|
378
|
+
req.resume();
|
|
379
|
+
send(res, 401, { error: "Live view session required", code: "session_required" });
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
if (req.headers[LIVE_VIEW_CSRF_HEADER] !== "1") {
|
|
383
|
+
req.resume();
|
|
384
|
+
send(res, 403, { error: "Missing live view request header", code: "csrf_header_required" });
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
if (grant.mode !== "control") {
|
|
388
|
+
req.resume();
|
|
389
|
+
send(res, 403, { error: viewOnlyError, code: "view_only" });
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const declared = Number(req.headers["content-length"]);
|
|
393
|
+
if (Number.isFinite(declared) && declared > max) {
|
|
394
|
+
req.resume();
|
|
395
|
+
send(res, 413, { error: tooLargeError, code: "batch_too_large" });
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const raw = await readLimitedBody(req, max);
|
|
399
|
+
if (raw === undefined) {
|
|
400
|
+
send(res, 413, { error: tooLargeError, code: "batch_too_large" });
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
if (allowEmpty && raw.length === 0)
|
|
404
|
+
return { body: undefined };
|
|
405
|
+
try {
|
|
406
|
+
return { body: JSON.parse(raw.toString("utf8")) };
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
send(res, 400, { error: "Invalid JSON body", code: "invalid_json" });
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function postDialog(req, res, target) {
|
|
414
|
+
const read = await readControlBody(req, res, target, LIVE_VIEW_MAX_DIALOG_BYTES, "This live view session cannot answer dialogs", "Dialog answer too large");
|
|
415
|
+
if (read === undefined)
|
|
416
|
+
return;
|
|
417
|
+
const session = sessions.peek(target.runId, target.stageId);
|
|
418
|
+
if (session === undefined) {
|
|
419
|
+
send(res, 409, { error: "Live view is closed", code: "closed" });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const body = read.body;
|
|
423
|
+
const fields = typeof body === "object" && body !== null && !Array.isArray(body)
|
|
424
|
+
? body
|
|
425
|
+
: undefined;
|
|
426
|
+
if (fields === undefined ||
|
|
427
|
+
typeof fields.id !== "string" ||
|
|
428
|
+
typeof fields.accept !== "boolean" ||
|
|
429
|
+
(fields.promptText !== undefined && typeof fields.promptText !== "string")) {
|
|
430
|
+
send(res, 400, { error: "invalid", code: "invalid" });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const result = await session.answerDialog({
|
|
434
|
+
id: fields.id,
|
|
435
|
+
accept: fields.accept,
|
|
436
|
+
...(typeof fields.promptText === "string" ? { promptText: fields.promptText } : {}),
|
|
437
|
+
});
|
|
438
|
+
if (result.ok) {
|
|
439
|
+
send(res, 200, { ok: true });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
send(res, DIALOG_STATUS[result.reason], { error: result.reason, code: result.reason });
|
|
443
|
+
}
|
|
444
|
+
async function postReopenTab(req, res, target) {
|
|
445
|
+
const read = await readControlBody(req, res, target, LIVE_VIEW_MAX_REOPEN_BYTES, "This live view session cannot reopen the tab", "Request too large", true);
|
|
446
|
+
if (read === undefined)
|
|
447
|
+
return;
|
|
448
|
+
const session = sessions.peek(target.runId, target.stageId);
|
|
449
|
+
if (session === undefined) {
|
|
450
|
+
send(res, 409, { error: "Live view is closed", code: "closed" });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const result = await session.reopenTab();
|
|
454
|
+
if (result.ok) {
|
|
455
|
+
send(res, 200, { ok: true });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
send(res, REOPEN_STATUS[result.reason], { error: result.reason, code: result.reason });
|
|
459
|
+
}
|
|
460
|
+
async function postInput(req, res, target) {
|
|
461
|
+
const read = await readControlBody(req, res, target, LIVE_VIEW_MAX_INPUT_BYTES, "This live view session cannot send input", "Input batch too large");
|
|
462
|
+
if (read === undefined)
|
|
463
|
+
return;
|
|
464
|
+
const events = read.body;
|
|
465
|
+
const session = sessions.peek(target.runId, target.stageId);
|
|
466
|
+
if (session === undefined) {
|
|
467
|
+
send(res, 409, { error: "Live view is closed", code: "closed" });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const result = await session.sendInput(events);
|
|
471
|
+
if (result.ok) {
|
|
472
|
+
send(res, 200, { accepted: result.accepted });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
send(res, INPUT_STATUS[result.reason], { error: result.reason, code: result.reason });
|
|
476
|
+
}
|
|
477
|
+
const routes = {
|
|
478
|
+
attach(manager) {
|
|
479
|
+
const offs = [
|
|
480
|
+
manager.onGateClosed((runId, stageId) => {
|
|
481
|
+
if (stageId === undefined)
|
|
482
|
+
void routes.revokeRun(runId);
|
|
483
|
+
else
|
|
484
|
+
void routes.revokeStageControl(runId, stageId);
|
|
485
|
+
}),
|
|
486
|
+
manager.beforeBrowserTeardown(({ runId, stageId }) => stageId === undefined ? routes.revokeRun(runId) : routes.revokeStage(runId, stageId)),
|
|
487
|
+
manager.onShutdown(() => void routes.dispose()),
|
|
488
|
+
];
|
|
489
|
+
return () => {
|
|
490
|
+
for (const off of offs)
|
|
491
|
+
off();
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
async handle(req, res) {
|
|
495
|
+
const method = req.method ?? "GET";
|
|
496
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
497
|
+
const match = matchLiveViewRoute(url.pathname);
|
|
498
|
+
if (match === null)
|
|
499
|
+
return false;
|
|
500
|
+
const { rawRunId: rawRun, rawStageId: rawStage, kind: action } = match;
|
|
501
|
+
let target;
|
|
502
|
+
try {
|
|
503
|
+
target = {
|
|
504
|
+
scope: resolveScope(req),
|
|
505
|
+
runId: decodeURIComponent(rawRun),
|
|
506
|
+
stageId: decodeURIComponent(rawStage),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
send(res, 400, { error: "Invalid path" });
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
const pathPrefix = `/api/runs/${rawRun}/stages/${rawStage}/live-view`;
|
|
514
|
+
try {
|
|
515
|
+
if (action === "ticket" && method === "POST") {
|
|
516
|
+
await issueTicket(req, res, target);
|
|
517
|
+
}
|
|
518
|
+
else if (action === "events" && method === "GET") {
|
|
519
|
+
await openStream(req, res, target, pathPrefix, url.searchParams.get("ticket"));
|
|
520
|
+
}
|
|
521
|
+
else if (action === "viewer" && method === "GET") {
|
|
522
|
+
await openViewer(req, res, target, pathPrefix, url.searchParams.get("ticket"));
|
|
523
|
+
}
|
|
524
|
+
else if (action === "input" && method === "POST") {
|
|
525
|
+
await postInput(req, res, target);
|
|
526
|
+
}
|
|
527
|
+
else if (action === "dialog" && method === "POST") {
|
|
528
|
+
await postDialog(req, res, target);
|
|
529
|
+
}
|
|
530
|
+
else if (action === "reopen-tab" && method === "POST") {
|
|
531
|
+
await postReopenTab(req, res, target);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
send(res, 405, { error: "Method not allowed" });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
if (!res.headersSent)
|
|
539
|
+
send(res, 500, { error: "Live view request failed" });
|
|
540
|
+
else if (!res.writableEnded)
|
|
541
|
+
res.end();
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
},
|
|
545
|
+
revokeStage(runId, stageId) {
|
|
546
|
+
tickets.revoke(runId, stageId);
|
|
547
|
+
return sessions.closeStage(runId, stageId);
|
|
548
|
+
},
|
|
549
|
+
revokeStageControl(runId, stageId) {
|
|
550
|
+
tickets.revoke(runId, stageId, "control");
|
|
551
|
+
if (tickets.hasCredentials(runId, stageId))
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
return sessions.closeStage(runId, stageId);
|
|
554
|
+
},
|
|
555
|
+
revokeRun(runId) {
|
|
556
|
+
tickets.revokeRun(runId);
|
|
557
|
+
return sessions.closeRun(runId);
|
|
558
|
+
},
|
|
559
|
+
dispose() {
|
|
560
|
+
tickets.revokeAll();
|
|
561
|
+
return sessions.dispose();
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
return routes;
|
|
565
|
+
}
|
package/dist/server/mcpHost.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StagePort } from "../agent/port.js";
|
|
2
|
+
import type { StageBrowserSupport } from "../browser/browserHost.js";
|
|
2
3
|
import type { ProviderAuthContext } from "../agent/providerAuth.js";
|
|
3
4
|
import type { RunStoreKind } from "../runstore/createStore.js";
|
|
4
5
|
import type { RunStore } from "../runstore/port.js";
|
|
@@ -8,7 +9,7 @@ import type { ControlTokens } from "./controlToken.js";
|
|
|
8
9
|
import { DEFAULT_PORT, type HttpHostEnvelope } from "./createHttpHost.js";
|
|
9
10
|
import { type ShutdownController } from "./shutdown.js";
|
|
10
11
|
export type McpServerOptions = {
|
|
11
|
-
agent:
|
|
12
|
+
agent: StagePort;
|
|
12
13
|
cwd?: string;
|
|
13
14
|
agentDir?: string;
|
|
14
15
|
rootDir?: string;
|
|
@@ -24,6 +25,7 @@ export type McpServerOptions = {
|
|
|
24
25
|
controlTokens?: ControlTokens;
|
|
25
26
|
requestTimeoutMs?: number;
|
|
26
27
|
maxConnections?: number;
|
|
28
|
+
browser?: StageBrowserSupport;
|
|
27
29
|
};
|
|
28
30
|
export declare function startMcpServer(options: McpServerOptions): Promise<HttpHostEnvelope & {
|
|
29
31
|
shutdown: ShutdownController;
|
package/dist/server/mcpHost.js
CHANGED