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.
Files changed (335) hide show
  1. package/README.md +4 -3
  2. package/dist/agent/adapterSpill.d.ts +14 -0
  3. package/dist/agent/adapterSpill.js +83 -0
  4. package/dist/agent/agentBackend.d.ts +2 -2
  5. package/dist/agent/agentBackend.js +2 -2
  6. package/dist/agent/claudeAdapter.d.ts +13 -2
  7. package/dist/agent/claudeAdapter.js +50 -6
  8. package/dist/agent/cursorProvider.d.ts +3 -0
  9. package/dist/agent/cursorProvider.js +23 -6
  10. package/dist/agent/fakeAgent.d.ts +3 -3
  11. package/dist/agent/fakeAgent.js +0 -6
  12. package/dist/agent/piAdapter.d.ts +2 -2
  13. package/dist/agent/piAdapter.js +116 -15
  14. package/dist/agent/piIsolatedMcp.d.ts +1 -0
  15. package/dist/agent/piIsolatedMcp.js +19 -1
  16. package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
  17. package/dist/agent/piIsolatedMcpProbe.js +20 -0
  18. package/dist/agent/port.d.ts +2 -2
  19. package/dist/agent/providerAuth.d.ts +6 -0
  20. package/dist/agent/providerAuth.js +86 -17
  21. package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -6
  22. package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +13 -9
  23. package/dist/agent/stageTurnDriver.d.ts +33 -0
  24. package/dist/agent/stageTurnDriver.js +64 -0
  25. package/dist/agent/toolOutputBudget.d.ts +46 -0
  26. package/dist/agent/toolOutputBudget.js +97 -0
  27. package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
  28. package/dist/agent/toolOutputBudgetExtension.js +141 -0
  29. package/dist/agent/toolOutputSpill.d.ts +34 -0
  30. package/dist/agent/toolOutputSpill.js +85 -0
  31. package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
  32. package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
  33. package/dist/browser/anchor.d.ts +2 -0
  34. package/dist/browser/anchor.js +2 -1
  35. package/dist/browser/auditSink.d.ts +8 -0
  36. package/dist/browser/browserHost.d.ts +29 -0
  37. package/dist/browser/browserSweep.d.ts +4 -1
  38. package/dist/browser/browserSweep.js +7 -0
  39. package/dist/browser/browserTeardown.d.ts +1 -0
  40. package/dist/browser/browserTeardown.js +32 -2
  41. package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
  42. package/dist/browser/containerBrowserEndpoint.js +57 -0
  43. package/dist/browser/containerBrowserHost.d.ts +24 -0
  44. package/dist/browser/containerBrowserHost.js +147 -0
  45. package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
  46. package/dist/browser/dockerSandboxOrchestrator.js +142 -0
  47. package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
  48. package/dist/browser/fakeLiveViewRelay.js +94 -0
  49. package/dist/browser/fakeLiveViewSource.d.ts +6 -0
  50. package/dist/browser/fakeLiveViewSource.js +16 -0
  51. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
  52. package/dist/browser/fakeRemoteBrowserHost.js +2 -1
  53. package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
  54. package/dist/browser/fakeSandboxOrchestrator.js +44 -0
  55. package/dist/browser/gateHandoff.d.ts +0 -2
  56. package/dist/browser/gateHandoff.js +5 -11
  57. package/dist/browser/hostCapabilities.d.ts +23 -0
  58. package/dist/browser/hostCapabilities.js +23 -0
  59. package/dist/browser/humanLogin.d.ts +2 -2
  60. package/dist/browser/humanLogin.js +11 -2
  61. package/dist/browser/liveViewPersisted.d.ts +11 -0
  62. package/dist/browser/liveViewPersisted.js +22 -0
  63. package/dist/browser/liveViewRelay.d.ts +76 -0
  64. package/dist/browser/liveViewRelay.js +1 -0
  65. package/dist/browser/liveViewSessions.d.ts +25 -0
  66. package/dist/browser/liveViewSessions.js +139 -0
  67. package/dist/browser/liveViewSource.d.ts +25 -0
  68. package/dist/browser/liveViewSource.js +7 -0
  69. package/dist/browser/liveViewTickets.d.ts +46 -0
  70. package/dist/browser/liveViewTickets.js +122 -0
  71. package/dist/browser/localBrowserHost.d.ts +10 -0
  72. package/dist/browser/localBrowserHost.js +53 -7
  73. package/dist/browser/ownerScope.d.ts +10 -0
  74. package/dist/browser/ownerScope.js +6 -0
  75. package/dist/browser/profileStore.d.ts +5 -2
  76. package/dist/browser/profileStore.js +3 -5
  77. package/dist/browser/sandboxOrchestrator.d.ts +60 -0
  78. package/dist/browser/sandboxOrchestrator.js +8 -0
  79. package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
  80. package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
  81. package/dist/browser/stageBrowserEnv.d.ts +7 -0
  82. package/dist/browser/stageBrowserEnv.js +53 -15
  83. package/dist/browser/stageHandoff.d.ts +47 -0
  84. package/dist/browser/stageHandoff.js +101 -0
  85. package/dist/browser/stageProfileLock.d.ts +1 -0
  86. package/dist/browser/stageProfileLock.js +1 -3
  87. package/dist/browser/volumeProfileStore.d.ts +7 -0
  88. package/dist/browser/volumeProfileStore.js +35 -0
  89. package/dist/cli/browserCommand.d.ts +3 -1
  90. package/dist/cli/browserCommand.js +25 -20
  91. package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
  92. package/dist/cli/resolveCliProjectRoot.js +27 -0
  93. package/dist/cli/runCommand.d.ts +1 -1
  94. package/dist/cli/runCommand.js +59 -17
  95. package/dist/cli/skillsCommand.d.ts +20 -0
  96. package/dist/cli/skillsCommand.js +74 -31
  97. package/dist/cli/validateCommand.d.ts +1 -1
  98. package/dist/cli/validateCommand.js +22 -4
  99. package/dist/cli.js +4 -4
  100. package/dist/config/browseCatalog.d.ts +2 -0
  101. package/dist/config/browseCatalog.js +6 -0
  102. package/dist/config/catalogRelativePath.d.ts +4 -0
  103. package/dist/config/catalogRelativePath.js +37 -9
  104. package/dist/config/createStage.d.ts +1 -0
  105. package/dist/config/createStage.js +11 -2
  106. package/dist/config/createTask.d.ts +30 -0
  107. package/dist/config/createTask.js +161 -0
  108. package/dist/config/createTrigger.d.ts +15 -0
  109. package/dist/config/createTrigger.js +32 -25
  110. package/dist/config/draftPackage.d.ts +41 -0
  111. package/dist/config/draftPackage.js +216 -23
  112. package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
  113. package/dist/config/ensureCatalogScanRoot.js +6 -2
  114. package/dist/config/findTaskInCatalog.d.ts +33 -0
  115. package/dist/config/findTaskInCatalog.js +69 -0
  116. package/dist/config/hostConfig.d.ts +21 -0
  117. package/dist/config/hostConfig.js +113 -1
  118. package/dist/config/importTask.d.ts +19 -0
  119. package/dist/config/importTask.js +133 -0
  120. package/dist/config/installSkillRequest.d.ts +40 -0
  121. package/dist/config/installSkillRequest.js +286 -0
  122. package/dist/config/listConfig.d.ts +0 -16
  123. package/dist/config/listConfig.js +0 -3
  124. package/dist/config/loadOutcome.d.ts +11 -6
  125. package/dist/config/loadPipeline.js +0 -1
  126. package/dist/config/loadStage.js +2 -0
  127. package/dist/config/loadStageflowManifest.js +4 -1
  128. package/dist/config/loadTask.js +2 -0
  129. package/dist/config/loadTrigger.js +2 -0
  130. package/dist/config/mergePipelineIncludes.js +3 -0
  131. package/dist/config/normalizePipelineStageEntry.js +10 -1
  132. package/dist/config/printTargetYaml.js +0 -1
  133. package/dist/config/publishDraftPackage.d.ts +5 -1
  134. package/dist/config/publishDraftPackage.js +4 -1
  135. package/dist/config/resolveCatalogRoots.d.ts +4 -1
  136. package/dist/config/resolveCatalogRoots.js +18 -1
  137. package/dist/config/skillUsageIndex.d.ts +6 -0
  138. package/dist/config/skillUsageIndex.js +39 -0
  139. package/dist/config/stageBrowser.js +13 -0
  140. package/dist/config/triggerAdapterStatus.d.ts +11 -0
  141. package/dist/config/triggerAdapterStatus.js +90 -0
  142. package/dist/config/triggerCatalog.d.ts +41 -2
  143. package/dist/config/triggerCatalog.js +107 -15
  144. package/dist/config/updateTask.d.ts +30 -0
  145. package/dist/config/updateTask.js +119 -0
  146. package/dist/config/updateTrigger.d.ts +38 -0
  147. package/dist/config/updateTrigger.js +220 -0
  148. package/dist/config/validateCatalog.d.ts +6 -2
  149. package/dist/config/validateCatalog.js +48 -7
  150. package/dist/config/workshopCatalogFilenames.d.ts +37 -0
  151. package/dist/config/workshopCatalogFilenames.js +125 -0
  152. package/dist/config/workshopCatalogRepair.d.ts +7 -0
  153. package/dist/config/workshopCatalogRepair.js +144 -0
  154. package/dist/config/yamlParsePosition.d.ts +4 -0
  155. package/dist/config/yamlParsePosition.js +9 -0
  156. package/dist/hosted/containment.d.ts +3 -0
  157. package/dist/hosted/containment.js +36 -0
  158. package/dist/hosted/errors.d.ts +21 -0
  159. package/dist/hosted/errors.js +30 -0
  160. package/dist/hosted/packageSkills.d.ts +3 -0
  161. package/dist/hosted/packageSkills.js +80 -0
  162. package/dist/hosted/runtime.d.ts +12 -0
  163. package/dist/hosted/runtime.js +596 -0
  164. package/dist/hosted/sequencer.d.ts +28 -0
  165. package/dist/hosted/sequencer.js +129 -0
  166. package/dist/hosted/translate.d.ts +3 -0
  167. package/dist/hosted/translate.js +43 -0
  168. package/dist/hosted/types.d.ts +108 -0
  169. package/dist/hosted/types.js +10 -0
  170. package/dist/index.d.ts +9 -1
  171. package/dist/index.js +6 -0
  172. package/dist/mcp/getStartedTool.js +15 -6
  173. package/dist/mcp/triggerTools.js +2 -8
  174. package/dist/operatorAgent/agentPort.d.ts +11 -0
  175. package/dist/operatorAgent/agentPort.js +12 -0
  176. package/dist/operatorAgent/index.d.ts +1 -0
  177. package/dist/operatorAgent/index.js +1 -0
  178. package/dist/operatorAgent/piModel.js +26 -6
  179. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  180. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
  181. package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
  182. package/dist/package-meta.d.ts +1 -1
  183. package/dist/package-meta.js +1 -1
  184. package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
  185. package/dist/project/ensureProjectRootsFromEnv.js +22 -0
  186. package/dist/project/globalHome.d.ts +1 -0
  187. package/dist/project/globalHome.js +18 -0
  188. package/dist/project/workspaceCatalog.d.ts +10 -0
  189. package/dist/project/workspaceCatalog.js +91 -0
  190. package/dist/projection/projectRun.d.ts +0 -1
  191. package/dist/projection/projectRun.js +0 -1
  192. package/dist/runstore/backup.js +14 -4
  193. package/dist/runstore/port.d.ts +11 -1
  194. package/dist/runstore/restore.js +5 -2
  195. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
  196. package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
  197. package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
  198. package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
  199. package/dist/runstore/sqlite/migrations/index.js +2 -0
  200. package/dist/runstore/stageSnapshot.d.ts +3 -1
  201. package/dist/runstore/stageSnapshot.js +19 -0
  202. package/dist/runstore/workspaceLayout.js +27 -0
  203. package/dist/runstore/writeContainedArtifact.d.ts +1 -0
  204. package/dist/runstore/writeContainedArtifact.js +52 -0
  205. package/dist/runtime/containerLimits.d.ts +0 -3
  206. package/dist/runtime/containerLimits.js +0 -5
  207. package/dist/runtime/credentialBinding.d.ts +3 -0
  208. package/dist/runtime/credentialBinding.js +23 -6
  209. package/dist/runtime/emailSource.js +20 -28
  210. package/dist/runtime/githubPollSource.js +20 -28
  211. package/dist/runtime/pipelineRunner.d.ts +4 -4
  212. package/dist/runtime/pipelineScheduler.d.ts +2 -2
  213. package/dist/runtime/pipelineScheduler.js +3 -0
  214. package/dist/runtime/resumeReconstruct.d.ts +2 -2
  215. package/dist/runtime/resumeTimedOut.d.ts +2 -2
  216. package/dist/runtime/runManager.d.ts +18 -2
  217. package/dist/runtime/runManager.js +95 -30
  218. package/dist/runtime/runRetentionSweep.d.ts +0 -2
  219. package/dist/runtime/runRetentionSweep.js +0 -8
  220. package/dist/runtime/runRetryCoordinator.d.ts +2 -2
  221. package/dist/runtime/scheduleSource.js +15 -24
  222. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  223. package/dist/runtime/stageAttemptBootstrap.js +10 -1
  224. package/dist/runtime/stageProcessLauncher.js +2 -2
  225. package/dist/runtime/stageRoots.js +12 -1
  226. package/dist/runtime/stageRunner.d.ts +2 -2
  227. package/dist/runtime/stageRunner.js +8 -2
  228. package/dist/runtime/stageWorker.js +2 -2
  229. package/dist/runtime/triggerPort.d.ts +5 -3
  230. package/dist/runtime/triggerRunner.d.ts +1 -0
  231. package/dist/runtime/triggerRunner.js +27 -20
  232. package/dist/runtime/webhookAccept.d.ts +25 -0
  233. package/dist/runtime/webhookAccept.js +56 -0
  234. package/dist/server/bootstrap.d.ts +7 -2
  235. package/dist/server/bootstrap.js +15 -1
  236. package/dist/server/controlToken.d.ts +1 -1
  237. package/dist/server/controlToken.js +18 -2
  238. package/dist/server/healthSurfaces.d.ts +12 -0
  239. package/dist/server/healthSurfaces.js +27 -6
  240. package/dist/server/http.d.ts +19 -10
  241. package/dist/server/http.js +605 -85
  242. package/dist/server/liveViewPath.d.ts +9 -0
  243. package/dist/server/liveViewPath.js +8 -0
  244. package/dist/server/liveViewRoutes.d.ts +58 -0
  245. package/dist/server/liveViewRoutes.js +565 -0
  246. package/dist/server/mcpHost.d.ts +4 -2
  247. package/dist/server/mcpHost.js +2 -0
  248. package/dist/server/readCatalogFile.d.ts +29 -0
  249. package/dist/server/readCatalogFile.js +215 -0
  250. package/dist/server/triggerRoutes.d.ts +1 -0
  251. package/dist/server/triggerRoutes.js +200 -73
  252. package/dist/tools/readEmail.d.ts +3 -3
  253. package/dist/tools/writeStageArtifact.d.ts +9 -0
  254. package/dist/tools/writeStageArtifact.js +2 -47
  255. package/dist/types/pipeline.d.ts +1 -1
  256. package/dist/types/stage.d.ts +1 -1
  257. package/dist/types/stageflowManifest.d.ts +1 -1
  258. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  259. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  260. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  261. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  262. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  263. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  264. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  265. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  266. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  267. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  268. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  269. package/dist/ui/assets/index-BoFPkKFg.js +256 -0
  270. package/dist/ui/assets/{index-Bv4p1aPK.css → index-CIPbbBUi.css} +1 -1
  271. package/dist/ui/index.html +4 -4
  272. package/dist/workshop/autosave.d.ts +2 -2
  273. package/dist/workshop/autosave.js +4 -6
  274. package/dist/workshop/buildStore.d.ts +2 -0
  275. package/dist/workshop/chatAttachments.d.ts +27 -0
  276. package/dist/workshop/chatAttachments.js +98 -0
  277. package/dist/workshop/chatTurn.d.ts +5 -2
  278. package/dist/workshop/chatTurn.js +22 -18
  279. package/dist/workshop/sessionMutations.d.ts +17 -0
  280. package/dist/workshop/sessionMutations.js +107 -0
  281. package/dist/workshop/sessionStore.d.ts +10 -0
  282. package/dist/workshop/sessionStore.js +52 -0
  283. package/dist/workshop/workshopPin.d.ts +4 -3
  284. package/dist/workshop/workshopPin.js +26 -6
  285. package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
  286. package/dist/workshop/workshopTranscriptTurn.js +46 -0
  287. package/examples/README.md +1 -1
  288. package/examples/archify-on-pr/README.md +1 -1
  289. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
  290. package/examples/archify-on-pr/author-diagrams.yaml +1 -0
  291. package/examples/conditional-fork/branch-a.yaml +1 -1
  292. package/examples/conditional-fork/decide.yaml +2 -1
  293. package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
  294. package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
  295. package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
  296. package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
  297. package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
  298. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
  299. package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
  300. package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
  301. package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
  302. package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
  303. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
  304. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
  305. package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
  306. package/examples/hello-world/hello.pipeline.yaml +2 -2
  307. package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
  308. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
  309. package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
  310. package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
  311. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
  312. package/examples/stageflow.yaml +6 -0
  313. package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
  314. package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
  315. package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
  316. package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
  317. package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
  318. package/examples/triggers-readiness/README.md +48 -0
  319. package/examples/triggers-readiness/readiness-emit.yaml +18 -0
  320. package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
  321. package/examples/triggers-readiness/readiness.task.yaml +6 -0
  322. package/examples/triggers-readiness/stageflow.yaml +13 -0
  323. package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
  324. package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
  325. package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
  326. package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
  327. package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
  328. package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
  329. package/package.json +9 -1
  330. package/skills/stageflow/references/control-surface.md +1 -1
  331. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  332. package/skills/stageflow-run/SKILL.md +1 -1
  333. package/skills/stageflow-run/references/mcp-call.md +1 -1
  334. package/skills/stageflow-setup/SKILL.md +1 -1
  335. package/dist/ui/assets/index-59cjF0OA.js +0 -133
@@ -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
  };
@@ -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
- if (gone) {
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;