stageflow 0.33.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 +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 +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/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.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/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/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/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/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/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/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/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 +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 +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 +1 -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
package/dist/browser/anchor.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare const BROWSER_ANCHOR_FILENAME = "anchor.json";
|
|
|
5
5
|
export type PersistedAnchor = ProfileBrowser & {
|
|
6
6
|
runId: string;
|
|
7
7
|
profile: string;
|
|
8
|
+
/** Owner scope the profile was opened in; absent in anchors written before it was persisted. */
|
|
9
|
+
scope?: string;
|
|
8
10
|
/** Bumped each time the anchor was replaced after the first start. */
|
|
9
11
|
restarts: number;
|
|
10
12
|
};
|
package/dist/browser/anchor.js
CHANGED
|
@@ -101,9 +101,10 @@ export function ensureRunProfileBrowser(support, input) {
|
|
|
101
101
|
anchorEnv: result.anchorEnv,
|
|
102
102
|
runId: input.runId,
|
|
103
103
|
profile: input.profile.key.name,
|
|
104
|
+
scope: input.profile.key.scope,
|
|
104
105
|
restarts: (stored?.restarts ?? 0) + (stored !== undefined && changed ? 1 : 0),
|
|
105
106
|
};
|
|
106
|
-
if (changed) {
|
|
107
|
+
if (changed || stored?.scope === undefined) {
|
|
107
108
|
const dir = anchorDir(input.runDir, input.profile.key.name);
|
|
108
109
|
const file = path.join(dir, BROWSER_ANCHOR_FILENAME);
|
|
109
110
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LiveViewMode } from "./liveViewTickets.js";
|
|
1
2
|
/** Records carry names, ids and hosts only: never paths, URLs with queries, or cookie values. */
|
|
2
3
|
export type AuditRecord = {
|
|
3
4
|
event: "profile_created";
|
|
@@ -25,6 +26,13 @@ export type AuditRecord = {
|
|
|
25
26
|
stageId: string;
|
|
26
27
|
profile?: string;
|
|
27
28
|
reason: string;
|
|
29
|
+
} | {
|
|
30
|
+
event: "live_view_opened" | "live_view_closed";
|
|
31
|
+
scope: string;
|
|
32
|
+
runId: string;
|
|
33
|
+
stageId: string;
|
|
34
|
+
mode: LiveViewMode;
|
|
35
|
+
callerId: string | null;
|
|
28
36
|
};
|
|
29
37
|
export interface AuditSink {
|
|
30
38
|
record(entry: AuditRecord): Promise<void>;
|
|
@@ -2,6 +2,8 @@ import type { StageBrowserConfig } from "../types/stage.js";
|
|
|
2
2
|
import type { ProfileLock } from "./profileLock.js";
|
|
3
3
|
import type { AuditSink } from "./auditSink.js";
|
|
4
4
|
import type { ProfileHandle, ProfileStore } from "./profileStore.js";
|
|
5
|
+
import type { BrowserHostCapabilityRecord } from "./hostCapabilities.js";
|
|
6
|
+
import type { OwnerScopeResolver } from "./ownerScope.js";
|
|
5
7
|
export type BrowserEnv = Record<string, string>;
|
|
6
8
|
export type BrowserStageRequest = {
|
|
7
9
|
runId: string;
|
|
@@ -31,12 +33,30 @@ export type ProfileBrowserResult = ProfileBrowser & {
|
|
|
31
33
|
/** True when a previous anchor existed but its address changed or it was replaced. */
|
|
32
34
|
restarted: boolean;
|
|
33
35
|
};
|
|
36
|
+
export type ProfileBrowserRelease = {
|
|
37
|
+
scope: string;
|
|
38
|
+
runId: string;
|
|
39
|
+
profile: string;
|
|
40
|
+
};
|
|
34
41
|
export interface BrowserHost {
|
|
42
|
+
/** What this host can do; read through `resolveBrowserHostCapabilities` so unset fields get safe defaults. */
|
|
43
|
+
readonly capabilities?: BrowserHostCapabilityRecord;
|
|
35
44
|
/**
|
|
36
45
|
* Starts or reuses the one browser for (run, profile) and returns its CDP
|
|
37
46
|
* address. Callers serialize per (run, profile) and persist the result.
|
|
38
47
|
*/
|
|
39
48
|
ensureProfileBrowser(request: ProfileBrowserRequest): Promise<ProfileBrowserResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Closes the run's shared browser for a profile gracefully (bounded) and releases
|
|
51
|
+
* what hosts it. Only hosts whose browser lives outside the Host's own sockets implement it.
|
|
52
|
+
*/
|
|
53
|
+
releaseProfileBrowser?(request: ProfileBrowserRelease): Promise<void>;
|
|
54
|
+
/** At Host start: releases browsers of runs that are no longer live; returns what it released. */
|
|
55
|
+
sweepOrphans?(input: {
|
|
56
|
+
isRunLive: (runId: string) => Promise<boolean>;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
released: string[];
|
|
59
|
+
}>;
|
|
40
60
|
/** Env of one stage's agent-browser session; with a profile it attaches to `cdpAddress` in its own tab. */
|
|
41
61
|
stageEnv(request: BrowserStageRequest): Promise<BrowserEnv>;
|
|
42
62
|
/** Env of the session that owns the profile's browser (anchor, or `sf browser login`). */
|
|
@@ -72,6 +92,8 @@ export type StageBrowserSupport = {
|
|
|
72
92
|
socketRoot?: string;
|
|
73
93
|
/** Defaults to the local audit log in the Stageflow home. */
|
|
74
94
|
audit?: AuditSink;
|
|
95
|
+
/** Owner scope of a run; defaults to the fixed local owner. Chosen by the Host, never by YAML. */
|
|
96
|
+
ownerScope?: OwnerScopeResolver;
|
|
75
97
|
/** Host-blocked sites; defaults to `browser.blocked_sites` from Host config. */
|
|
76
98
|
blockedSites?: readonly string[];
|
|
77
99
|
/** Screen detection for human login stages; defaults to the real Host. */
|
|
@@ -79,6 +101,13 @@ export type StageBrowserSupport = {
|
|
|
79
101
|
hasDisplay: boolean;
|
|
80
102
|
docker: boolean;
|
|
81
103
|
};
|
|
104
|
+
/** Awaited (bounded) before a stage's or run's browser is torn down; stageId absent means the whole run. */
|
|
105
|
+
beforeTeardown?: (input: {
|
|
106
|
+
runId: string;
|
|
107
|
+
stageId?: string;
|
|
108
|
+
}) => Promise<void> | void;
|
|
109
|
+
/** Upper bound for `beforeTeardown`; teardown proceeds when it is exceeded. */
|
|
110
|
+
beforeTeardownWaitMs?: number;
|
|
82
111
|
};
|
|
83
112
|
export declare const BROWSER_ENV_PREFIX = "AGENT_BROWSER_";
|
|
84
113
|
export declare function shortHash(value: string, length: number): string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { BrowserRunner } from "./browserHost.js";
|
|
1
|
+
import type { BrowserHost, BrowserRunner } from "./browserHost.js";
|
|
2
2
|
export type BrowserSweepOptions = {
|
|
3
3
|
isRunLive: (runId: string) => Promise<boolean>;
|
|
4
4
|
socketRoot?: string;
|
|
5
5
|
runner?: BrowserRunner;
|
|
6
6
|
closeWaitMs?: number;
|
|
7
|
+
/** A host whose browsers live outside the local sockets sweeps them itself (containers, by label). */
|
|
8
|
+
host?: BrowserHost;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
9
11
|
* At Host start: close sessions whose run is gone or terminal, clean leftover
|
|
@@ -11,4 +13,5 @@ export type BrowserSweepOptions = {
|
|
|
11
13
|
*/
|
|
12
14
|
export declare function sweepOrphanBrowserSessions(options: BrowserSweepOptions): Promise<{
|
|
13
15
|
closed: string[];
|
|
16
|
+
released: string[];
|
|
14
17
|
}>;
|
|
@@ -20,6 +20,13 @@ async function readOwner(dir) {
|
|
|
20
20
|
* files, and leave sessions of live or waiting runs alone.
|
|
21
21
|
*/
|
|
22
22
|
export async function sweepOrphanBrowserSessions(options) {
|
|
23
|
+
const { closed } = await sweepLocalSessions(options);
|
|
24
|
+
// Stage sessions are closed first; the host then releases the browsers they were attached to.
|
|
25
|
+
const swept = await (options.host?.sweepOrphans?.({ isRunLive: options.isRunLive }) ?? Promise.resolve({ released: [] }))
|
|
26
|
+
.catch(() => ({ released: [] }));
|
|
27
|
+
return { closed, released: swept.released };
|
|
28
|
+
}
|
|
29
|
+
async function sweepLocalSessions(options) {
|
|
23
30
|
const root = options.socketRoot ?? defaultSocketRoot(process.platform);
|
|
24
31
|
const closed = [];
|
|
25
32
|
let dirs;
|
|
@@ -3,6 +3,7 @@ import { type BrowserEnv, type BrowserRunner, type StageBrowserSupport } from ".
|
|
|
3
3
|
export declare const BROWSER_OWNER_FILENAME = "owner.json";
|
|
4
4
|
/** Written once a stage's tab and session are closed; cleared when its env is resolved again. */
|
|
5
5
|
export declare const BROWSER_CLOSED_FILENAME = "browser-closed.json";
|
|
6
|
+
export declare const DEFAULT_BEFORE_TEARDOWN_WAIT_MS = 5000;
|
|
6
7
|
export type BrowserSessionOwner = {
|
|
7
8
|
runId: string;
|
|
8
9
|
stageId: string;
|
|
@@ -6,6 +6,7 @@ import { readPersistedBrowserEnv } from "./persistedEnv.js";
|
|
|
6
6
|
import { stageDir } from "../runstore/paths.js";
|
|
7
7
|
import { auditStageNavigations } from "./navigationAudit.js";
|
|
8
8
|
import { BROWSER_ENV_FILENAME, BROWSER_POLICY_FILENAME, } from "./browserHost.js";
|
|
9
|
+
import { runOwnerScope } from "./ownerScope.js";
|
|
9
10
|
import { stageProfileLock } from "./stageProfileLock.js";
|
|
10
11
|
import { BROWSER_ANCHOR_FILENAME, anchorDir, readPersistedAnchor, } from "./anchor.js";
|
|
11
12
|
export const BROWSER_OWNER_FILENAME = "owner.json";
|
|
@@ -14,6 +15,7 @@ export const BROWSER_CLOSED_FILENAME = "browser-closed.json";
|
|
|
14
15
|
const DEFAULT_CLOSE_WAIT_MS = 10_000;
|
|
15
16
|
const POLL_MS = 50;
|
|
16
17
|
const CLOSE_COMMAND_TIMEOUT_MS = 20_000;
|
|
18
|
+
export const DEFAULT_BEFORE_TEARDOWN_WAIT_MS = 5_000;
|
|
17
19
|
export const defaultBrowserRunner = (args, env, options) => new Promise((resolve) => {
|
|
18
20
|
execFile("agent-browser", args, {
|
|
19
21
|
env: { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", ...env },
|
|
@@ -102,6 +104,21 @@ async function readEnvFile(file) {
|
|
|
102
104
|
return undefined;
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
async function runBeforeTeardown(support, input) {
|
|
108
|
+
if (support.beforeTeardown === undefined)
|
|
109
|
+
return;
|
|
110
|
+
const hook = support.beforeTeardown;
|
|
111
|
+
let timer;
|
|
112
|
+
const bound = new Promise((resolve) => {
|
|
113
|
+
timer = setTimeout(resolve, support.beforeTeardownWaitMs ?? DEFAULT_BEFORE_TEARDOWN_WAIT_MS);
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
await Promise.race([Promise.resolve().then(() => hook(input)).catch(() => undefined), bound]);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
105
122
|
function closeOptions(support) {
|
|
106
123
|
return {
|
|
107
124
|
...(support.runner !== undefined ? { runner: support.runner } : {}),
|
|
@@ -155,6 +172,7 @@ async function auditAndClearPolicy(support, input) {
|
|
|
155
172
|
await rm(file, { force: true }).catch(() => undefined);
|
|
156
173
|
}
|
|
157
174
|
async function teardownStageBrowserOnce(support, input) {
|
|
175
|
+
await runBeforeTeardown(support, { runId: input.runId, stageId: input.stageId });
|
|
158
176
|
const env = await readEnvFile(path.join(stageDir(input.runDir, input.stageId), BROWSER_ENV_FILENAME));
|
|
159
177
|
if (env === undefined)
|
|
160
178
|
return;
|
|
@@ -195,8 +213,18 @@ async function closeRunAnchorsOnce(support, run) {
|
|
|
195
213
|
const anchor = await readPersistedAnchor(run.runDir, name);
|
|
196
214
|
if (anchor === undefined)
|
|
197
215
|
continue;
|
|
198
|
-
const { gone } = await closeBrowserSession(anchor.anchorEnv, closeOptions(support)).catch(() => ({ gone: false }));
|
|
199
|
-
|
|
216
|
+
const { gone: sessionGone } = await closeBrowserSession(anchor.anchorEnv, closeOptions(support)).catch(() => ({ gone: false }));
|
|
217
|
+
// Anchors written before the scope was persisted fall back to the run owner's scope.
|
|
218
|
+
const released = support.host.releaseProfileBrowser === undefined ||
|
|
219
|
+
(await support.host
|
|
220
|
+
.releaseProfileBrowser({
|
|
221
|
+
scope: anchor.scope ?? runOwnerScope(support, { runId: anchor.runId }),
|
|
222
|
+
runId: anchor.runId,
|
|
223
|
+
profile: name,
|
|
224
|
+
})
|
|
225
|
+
.then(() => true)
|
|
226
|
+
.catch(() => false));
|
|
227
|
+
if (sessionGone && released) {
|
|
200
228
|
await rm(path.join(anchorDir(run.runDir, name), BROWSER_ANCHOR_FILENAME), {
|
|
201
229
|
force: true,
|
|
202
230
|
}).catch(() => undefined);
|
|
@@ -209,6 +237,8 @@ async function closeRunAnchorsOnce(support, run) {
|
|
|
209
237
|
* just those stages and leaves anchor and leases alone.
|
|
210
238
|
*/
|
|
211
239
|
export async function teardownRunBrowsers(support, run, only) {
|
|
240
|
+
if (only === undefined)
|
|
241
|
+
await runBeforeTeardown(support, { runId: run.runId });
|
|
212
242
|
let ids = [];
|
|
213
243
|
try {
|
|
214
244
|
ids = await readdir(path.join(run.runDir, "stages"));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BrowserEndpointClient {
|
|
2
|
+
/** The browser-level debugging address behind an `http(s)://ip:port` endpoint, or undefined while nothing answers. */
|
|
3
|
+
resolve(endpoint: string): Promise<string | undefined>;
|
|
4
|
+
/** Asks the browser to close itself over the debugging protocol (this flushes the profile); resolves once it hung up or the bound passed. */
|
|
5
|
+
closeBrowser(cdpAddress: string, timeoutMs: number): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function endpointOf(cdpAddress: string): string;
|
|
8
|
+
export declare function createCdpEndpointClient(options?: {
|
|
9
|
+
WebSocketImpl?: typeof WebSocket;
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}): BrowserEndpointClient;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const PROBE_TIMEOUT_MS = 2_000;
|
|
2
|
+
export function endpointOf(cdpAddress) {
|
|
3
|
+
const url = new URL(cdpAddress);
|
|
4
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
5
|
+
return `${url.protocol}//${url.host}`;
|
|
6
|
+
}
|
|
7
|
+
export function createCdpEndpointClient(options = {}) {
|
|
8
|
+
const doFetch = options.fetchImpl ?? globalThis.fetch;
|
|
9
|
+
return {
|
|
10
|
+
async resolve(endpoint) {
|
|
11
|
+
const base = endpoint.startsWith("ws") ? endpointOf(endpoint) : endpoint.replace(/\/+$/, "");
|
|
12
|
+
try {
|
|
13
|
+
const res = await doFetch(`${base}/json/version`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
return undefined;
|
|
16
|
+
const body = (await res.json());
|
|
17
|
+
if (typeof body.webSocketDebuggerUrl !== "string")
|
|
18
|
+
return undefined;
|
|
19
|
+
const advertised = new URL(body.webSocketDebuggerUrl);
|
|
20
|
+
const origin = new URL(base);
|
|
21
|
+
advertised.protocol = origin.protocol === "https:" ? "wss:" : "ws:";
|
|
22
|
+
advertised.host = origin.host;
|
|
23
|
+
return advertised.toString();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
closeBrowser(cdpAddress, timeoutMs) {
|
|
30
|
+
const Impl = options.WebSocketImpl ?? globalThis.WebSocket;
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let ws;
|
|
33
|
+
try {
|
|
34
|
+
ws = new Impl(cdpAddress);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
resolve();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const timer = setTimeout(done, timeoutMs);
|
|
41
|
+
function done() {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
try {
|
|
44
|
+
ws.close();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// already closed
|
|
48
|
+
}
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
ws.onopen = () => ws.send(JSON.stringify({ id: 1, method: "Browser.close" }));
|
|
52
|
+
ws.onclose = done;
|
|
53
|
+
ws.onerror = done;
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type BrowserHost } from "./browserHost.js";
|
|
2
|
+
import { type BrowserEndpointClient } from "./containerBrowserEndpoint.js";
|
|
3
|
+
import type { BrowserHostCapabilityRecord } from "./hostCapabilities.js";
|
|
4
|
+
import { type LocalBrowserHostOptions } from "./localBrowserHost.js";
|
|
5
|
+
import type { BrowserSandboxOrchestrator } from "./sandboxOrchestrator.js";
|
|
6
|
+
export type ContainerBrowserHostOptions = {
|
|
7
|
+
orchestrator: BrowserSandboxOrchestrator;
|
|
8
|
+
/** `relay` only when the process also serves the live view routes; default `none`. */
|
|
9
|
+
liveView?: "relay" | "provider_view" | "none";
|
|
10
|
+
/** Capabilities the orchestrator behind this host adds or overrides (for example a view-only viewer). */
|
|
11
|
+
capabilities?: BrowserHostCapabilityRecord;
|
|
12
|
+
/** Builds stage session envs (socket dir, session name, config file); defaults to a local host. */
|
|
13
|
+
base?: BrowserHost;
|
|
14
|
+
local?: LocalBrowserHostOptions;
|
|
15
|
+
endpoint?: BrowserEndpointClient;
|
|
16
|
+
/** Upper bound for a new container's debugging endpoint to answer. */
|
|
17
|
+
startTimeoutMs?: number;
|
|
18
|
+
/** Upper bound for a graceful browser close to take effect before the orchestrator stops the container. */
|
|
19
|
+
closeWaitMs?: number;
|
|
20
|
+
pollMs?: number;
|
|
21
|
+
/** Resolves a non-IP attach host; Chrome refuses debugging requests addressed by name. */
|
|
22
|
+
resolveHost?: (hostname: string) => Promise<string>;
|
|
23
|
+
};
|
|
24
|
+
export declare function createContainerBrowserHost(options: ContainerBrowserHostOptions): BrowserHost;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { createCdpEndpointClient, endpointOf, } from "./containerBrowserEndpoint.js";
|
|
4
|
+
import { createLocalBrowserHost } from "./localBrowserHost.js";
|
|
5
|
+
const DEFAULT_START_TIMEOUT_MS = 30_000;
|
|
6
|
+
const DEFAULT_CLOSE_WAIT_MS = 10_000;
|
|
7
|
+
const DEFAULT_POLL_MS = 250;
|
|
8
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
async function defaultResolveHost(hostname) {
|
|
10
|
+
return (await lookup(hostname)).address;
|
|
11
|
+
}
|
|
12
|
+
export function createContainerBrowserHost(options) {
|
|
13
|
+
const { orchestrator } = options;
|
|
14
|
+
const client = options.endpoint ?? createCdpEndpointClient();
|
|
15
|
+
const base = options.base ?? createLocalBrowserHost(options.local);
|
|
16
|
+
const startTimeoutMs = options.startTimeoutMs ?? DEFAULT_START_TIMEOUT_MS;
|
|
17
|
+
const closeWaitMs = options.closeWaitMs ?? DEFAULT_CLOSE_WAIT_MS;
|
|
18
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
19
|
+
const resolveHost = options.resolveHost ?? defaultResolveHost;
|
|
20
|
+
async function ipEndpoint(attachAddress) {
|
|
21
|
+
const url = new URL(attachAddress);
|
|
22
|
+
let host = url.hostname.replace(/^\[|\]$/g, "");
|
|
23
|
+
if (host === "localhost")
|
|
24
|
+
host = "127.0.0.1";
|
|
25
|
+
else if (isIP(host) === 0)
|
|
26
|
+
host = await resolveHost(host);
|
|
27
|
+
const secure = url.protocol === "wss:" || url.protocol === "https:";
|
|
28
|
+
const printable = isIP(host) === 6 ? `[${host}]` : host;
|
|
29
|
+
return `${secure ? "https" : "http"}://${printable}${url.port !== "" ? `:${url.port}` : ""}`;
|
|
30
|
+
}
|
|
31
|
+
async function reach(info) {
|
|
32
|
+
if (info.status !== "running" || info.attachAddress === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
return client.resolve(await ipEndpoint(info.attachAddress));
|
|
35
|
+
}
|
|
36
|
+
async function waitFor(done, timeoutMs) {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (await done())
|
|
40
|
+
return true;
|
|
41
|
+
if (Date.now() >= deadline)
|
|
42
|
+
return false;
|
|
43
|
+
await sleep(pollMs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function closeAndRelease(info) {
|
|
47
|
+
const address = await reach(info).catch(() => undefined);
|
|
48
|
+
if (address !== undefined) {
|
|
49
|
+
await client.closeBrowser(address, closeWaitMs).catch(() => undefined);
|
|
50
|
+
const gone = await waitFor(async () => (await reach(info).catch(() => undefined)) === undefined, closeWaitMs);
|
|
51
|
+
if (!gone)
|
|
52
|
+
await orchestrator.stopGracefully(info.ref).catch(() => undefined);
|
|
53
|
+
}
|
|
54
|
+
await orchestrator.release(info.ref);
|
|
55
|
+
}
|
|
56
|
+
async function anchorEnvFor(request, cdpAddress) {
|
|
57
|
+
const env = await base.stageEnv({
|
|
58
|
+
runId: request.runId,
|
|
59
|
+
stageId: `anchor:${request.profile.key.name}`,
|
|
60
|
+
browser: request.browser,
|
|
61
|
+
profile: request.profile,
|
|
62
|
+
cdpAddress,
|
|
63
|
+
});
|
|
64
|
+
delete env.AGENT_BROWSER_PIN_TAB;
|
|
65
|
+
return env;
|
|
66
|
+
}
|
|
67
|
+
const capabilities = {
|
|
68
|
+
display: "virtual_display",
|
|
69
|
+
liveView: options.liveView ?? "none",
|
|
70
|
+
attach: "cdp",
|
|
71
|
+
profilePersistence: "host_volume",
|
|
72
|
+
gracefulCloseRequired: true,
|
|
73
|
+
...options.capabilities,
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
capabilities,
|
|
77
|
+
async ensureProfileBrowser(request) {
|
|
78
|
+
const { profile, previous } = request;
|
|
79
|
+
const labels = { scope: profile.key.scope, runId: request.runId, profile: profile.key.name };
|
|
80
|
+
if (previous !== undefined) {
|
|
81
|
+
const alive = await client.resolve(endpointOf(previous.cdpAddress)).catch(() => undefined);
|
|
82
|
+
if (alive === previous.cdpAddress) {
|
|
83
|
+
return { cdpAddress: previous.cdpAddress, anchorEnv: previous.anchorEnv, restarted: false };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let adopted;
|
|
87
|
+
for (const info of await orchestrator.listByLabel(labels)) {
|
|
88
|
+
const address = adopted === undefined ? await reach(info).catch(() => undefined) : undefined;
|
|
89
|
+
if (address !== undefined)
|
|
90
|
+
adopted = { address, info };
|
|
91
|
+
else
|
|
92
|
+
await closeAndRelease(info).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
let cdpAddress;
|
|
95
|
+
if (adopted !== undefined) {
|
|
96
|
+
cdpAddress = adopted.address;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const info = await orchestrator.start({
|
|
100
|
+
labels,
|
|
101
|
+
profile: { scope: profile.key.scope, name: profile.key.name },
|
|
102
|
+
});
|
|
103
|
+
let ready;
|
|
104
|
+
await waitFor(async () => {
|
|
105
|
+
const current = info.attachAddress !== undefined ? info : await orchestrator.inspect(info.ref);
|
|
106
|
+
ready = current !== undefined ? await reach(current).catch(() => undefined) : undefined;
|
|
107
|
+
return ready !== undefined;
|
|
108
|
+
}, startTimeoutMs);
|
|
109
|
+
if (ready === undefined) {
|
|
110
|
+
await orchestrator.release(info.ref).catch(() => undefined);
|
|
111
|
+
throw new Error("the container browser did not answer on its debugging port in time");
|
|
112
|
+
}
|
|
113
|
+
cdpAddress = ready;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
cdpAddress,
|
|
117
|
+
anchorEnv: await anchorEnvFor(request, cdpAddress),
|
|
118
|
+
restarted: previous !== undefined && previous.cdpAddress !== cdpAddress,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
async releaseProfileBrowser(request) {
|
|
122
|
+
const found = await orchestrator.listByLabel({
|
|
123
|
+
scope: request.scope,
|
|
124
|
+
runId: request.runId,
|
|
125
|
+
profile: request.profile,
|
|
126
|
+
});
|
|
127
|
+
for (const info of found)
|
|
128
|
+
await closeAndRelease(info);
|
|
129
|
+
},
|
|
130
|
+
async sweepOrphans(input) {
|
|
131
|
+
const released = [];
|
|
132
|
+
for (const info of await orchestrator.listByLabel({})) {
|
|
133
|
+
if (await input.isRunLive(info.labels.runId).catch(() => true))
|
|
134
|
+
continue;
|
|
135
|
+
await closeAndRelease(info);
|
|
136
|
+
released.push(info.ref.id);
|
|
137
|
+
}
|
|
138
|
+
return { released };
|
|
139
|
+
},
|
|
140
|
+
async profileBrowserEnv() {
|
|
141
|
+
throw new Error("the container browser host has no profile folder; `sf browser login` needs the local host");
|
|
142
|
+
},
|
|
143
|
+
stageEnv(request) {
|
|
144
|
+
return base.stageEnv(request);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type BrowserSandboxOrchestrator } from "./sandboxOrchestrator.js";
|
|
2
|
+
export type DockerCliResult = {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
export type DockerCliRunner = (args: string[]) => Promise<DockerCliResult>;
|
|
8
|
+
export type DockerSandboxOptions = {
|
|
9
|
+
image: string;
|
|
10
|
+
command?: string[];
|
|
11
|
+
debugPort?: number;
|
|
12
|
+
shmSize?: string;
|
|
13
|
+
memory?: string;
|
|
14
|
+
pidsLimit?: number;
|
|
15
|
+
user?: string;
|
|
16
|
+
tmpfs?: string[];
|
|
17
|
+
stopTimeoutSeconds?: number;
|
|
18
|
+
profileMountPath?: string;
|
|
19
|
+
run?: DockerCliRunner;
|
|
20
|
+
};
|
|
21
|
+
export declare const defaultDockerCliRunner: DockerCliRunner;
|
|
22
|
+
/**
|
|
23
|
+
* Development only: shells out to the local container CLI. It needs a runtime
|
|
24
|
+
* socket on the machine running it, so it must never run inside a Stageflow container.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createDockerSandboxOrchestrator(options: DockerSandboxOptions): BrowserSandboxOrchestrator;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { SandboxError, } from "./sandboxOrchestrator.js";
|
|
3
|
+
const LABEL_PREFIX = "stageflow.";
|
|
4
|
+
const MARKER = `${LABEL_PREFIX}sandbox=1`;
|
|
5
|
+
const LABEL_KEYS = {
|
|
6
|
+
scope: `${LABEL_PREFIX}scope`,
|
|
7
|
+
runId: `${LABEL_PREFIX}run`,
|
|
8
|
+
stageId: `${LABEL_PREFIX}stage`,
|
|
9
|
+
profile: `${LABEL_PREFIX}profile`,
|
|
10
|
+
};
|
|
11
|
+
export const defaultDockerCliRunner = (args) => new Promise((resolve) => {
|
|
12
|
+
execFile("docker", args, { timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
13
|
+
const code = err === null ? 0 : typeof err.code === "number" ? err.code : 1;
|
|
14
|
+
resolve({ code, stdout: String(stdout), stderr: err !== null && stderr === "" ? err.message : String(stderr) });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
function volumeName(scope, name) {
|
|
18
|
+
const safe = (v) => v.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
19
|
+
return `sf-profile-${safe(scope)}-${safe(name)}`;
|
|
20
|
+
}
|
|
21
|
+
function statusOf(raw) {
|
|
22
|
+
if (raw === "running" || raw === "restarting" || raw === "paused")
|
|
23
|
+
return "running";
|
|
24
|
+
if (raw === "dead")
|
|
25
|
+
return "dead";
|
|
26
|
+
return "stopped";
|
|
27
|
+
}
|
|
28
|
+
function isMissing(result) {
|
|
29
|
+
return /no such (container|object)/i.test(result.stderr);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Development only: shells out to the local container CLI. It needs a runtime
|
|
33
|
+
* socket on the machine running it, so it must never run inside a Stageflow container.
|
|
34
|
+
*/
|
|
35
|
+
export function createDockerSandboxOrchestrator(options) {
|
|
36
|
+
const run = options.run ?? defaultDockerCliRunner;
|
|
37
|
+
const debugPort = options.debugPort ?? 9222;
|
|
38
|
+
const stopTimeout = options.stopTimeoutSeconds ?? 10;
|
|
39
|
+
const profileMount = options.profileMountPath ?? "/profile";
|
|
40
|
+
function toInfo(entry) {
|
|
41
|
+
const raw = entry.Config?.Labels ?? {};
|
|
42
|
+
const labels = { scope: raw[LABEL_KEYS.scope] ?? "", runId: raw[LABEL_KEYS.runId] ?? "" };
|
|
43
|
+
if (raw[LABEL_KEYS.stageId] !== undefined)
|
|
44
|
+
labels.stageId = raw[LABEL_KEYS.stageId];
|
|
45
|
+
if (raw[LABEL_KEYS.profile] !== undefined)
|
|
46
|
+
labels.profile = raw[LABEL_KEYS.profile];
|
|
47
|
+
const status = statusOf(entry.State?.Status);
|
|
48
|
+
const info = { ref: { id: entry.Id, adapter: { id: "docker", version: 1 } }, labels, status };
|
|
49
|
+
const binding = entry.NetworkSettings?.Ports?.[`${debugPort}/tcp`]?.[0];
|
|
50
|
+
if (status === "running" && binding?.HostPort !== undefined) {
|
|
51
|
+
info.attachAddress = `http://127.0.0.1:${binding.HostPort}`;
|
|
52
|
+
}
|
|
53
|
+
return info;
|
|
54
|
+
}
|
|
55
|
+
async function inspectMany(ids) {
|
|
56
|
+
if (ids.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
const result = await run(["inspect", ...ids]);
|
|
59
|
+
if (result.code !== 0) {
|
|
60
|
+
if (isMissing(result))
|
|
61
|
+
return [];
|
|
62
|
+
throw new SandboxError("unavailable", `docker inspect failed: ${result.stderr.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(result.stdout).map(toInfo);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
async start(request) {
|
|
68
|
+
if (request.egress !== undefined) {
|
|
69
|
+
throw new SandboxError("not_supported", "the docker development orchestrator cannot enforce an egress policy");
|
|
70
|
+
}
|
|
71
|
+
const args = [
|
|
72
|
+
"run",
|
|
73
|
+
"-d",
|
|
74
|
+
"--init",
|
|
75
|
+
"--read-only",
|
|
76
|
+
"--cap-drop",
|
|
77
|
+
"ALL",
|
|
78
|
+
"--security-opt",
|
|
79
|
+
"no-new-privileges",
|
|
80
|
+
"--pids-limit",
|
|
81
|
+
String(options.pidsLimit ?? 512),
|
|
82
|
+
"--memory",
|
|
83
|
+
options.memory ?? "1g",
|
|
84
|
+
"--label",
|
|
85
|
+
MARKER,
|
|
86
|
+
"-p",
|
|
87
|
+
`127.0.0.1::${debugPort}`,
|
|
88
|
+
];
|
|
89
|
+
for (const mount of options.tmpfs ?? ["/tmp"])
|
|
90
|
+
args.push("--tmpfs", mount);
|
|
91
|
+
if (options.user !== undefined)
|
|
92
|
+
args.push("--user", options.user);
|
|
93
|
+
if (options.shmSize !== undefined)
|
|
94
|
+
args.push("--shm-size", options.shmSize);
|
|
95
|
+
for (const [key, label] of Object.entries(LABEL_KEYS)) {
|
|
96
|
+
const value = request.labels[key];
|
|
97
|
+
if (value !== undefined)
|
|
98
|
+
args.push("--label", `${label}=${value}`);
|
|
99
|
+
}
|
|
100
|
+
if (request.profile !== undefined) {
|
|
101
|
+
args.push("-v", `${volumeName(request.profile.scope, request.profile.name)}:${profileMount}`);
|
|
102
|
+
}
|
|
103
|
+
args.push(options.image, ...(options.command ?? []));
|
|
104
|
+
const started = await run(args);
|
|
105
|
+
if (started.code !== 0) {
|
|
106
|
+
throw new SandboxError("unavailable", `docker run failed: ${started.stderr.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
const id = started.stdout.trim().split("\n").pop() ?? "";
|
|
109
|
+
const [info] = await inspectMany([id]);
|
|
110
|
+
if (info === undefined)
|
|
111
|
+
throw new SandboxError("failed", "container vanished after start");
|
|
112
|
+
return info;
|
|
113
|
+
},
|
|
114
|
+
async stopGracefully(ref) {
|
|
115
|
+
const result = await run(["stop", "-t", String(stopTimeout), ref.id]);
|
|
116
|
+
if (result.code !== 0 && !isMissing(result)) {
|
|
117
|
+
throw new SandboxError("failed", `docker stop failed: ${result.stderr.trim()}`);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async listByLabel(labels) {
|
|
121
|
+
const args = ["ps", "-a", "--no-trunc", "--format", "{{.ID}}", "--filter", `label=${MARKER}`];
|
|
122
|
+
for (const [key, label] of Object.entries(LABEL_KEYS)) {
|
|
123
|
+
const value = labels[key];
|
|
124
|
+
if (value !== undefined)
|
|
125
|
+
args.push("--filter", `label=${label}=${value}`);
|
|
126
|
+
}
|
|
127
|
+
const listed = await run(args);
|
|
128
|
+
if (listed.code !== 0)
|
|
129
|
+
throw new SandboxError("unavailable", `docker ps failed: ${listed.stderr.trim()}`);
|
|
130
|
+
return inspectMany(listed.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== ""));
|
|
131
|
+
},
|
|
132
|
+
async inspect(ref) {
|
|
133
|
+
return (await inspectMany([ref.id]))[0];
|
|
134
|
+
},
|
|
135
|
+
async release(ref) {
|
|
136
|
+
const result = await run(["rm", "-f", "-v", ref.id]);
|
|
137
|
+
if (result.code !== 0 && !isMissing(result)) {
|
|
138
|
+
throw new SandboxError("failed", `docker rm failed: ${result.stderr.trim()}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LiveViewDialogAnswer, LiveViewInputEvent, LiveViewMessage, LiveViewRelay, LiveViewReopenResult, LiveViewSession } from "./liveViewRelay.js";
|
|
2
|
+
export type FakeLiveViewSession = LiveViewSession & {
|
|
3
|
+
/** Feeds a message as if it came from the browser's stream. */
|
|
4
|
+
emit(message: LiveViewMessage): void;
|
|
5
|
+
readonly input: readonly LiveViewInputEvent[];
|
|
6
|
+
readonly answers: readonly LiveViewDialogAnswer[];
|
|
7
|
+
readonly closed: boolean;
|
|
8
|
+
readonly reopens: number;
|
|
9
|
+
/** Result the next reopenTab() returns; defaults to ok. */
|
|
10
|
+
reopenResult: LiveViewReopenResult;
|
|
11
|
+
};
|
|
12
|
+
export type FakeLiveViewRelay = LiveViewRelay & {
|
|
13
|
+
readonly sessions: readonly FakeLiveViewSession[];
|
|
14
|
+
};
|
|
15
|
+
export declare function createFakeLiveViewRelay(): FakeLiveViewRelay;
|