stageflow 0.33.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/README.md +3 -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 +56 -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/piAdapter.d.ts +2 -2
  12. package/dist/agent/piAdapter.js +125 -15
  13. package/dist/agent/piIsolatedMcp.d.ts +1 -0
  14. package/dist/agent/piIsolatedMcp.js +19 -1
  15. package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
  16. package/dist/agent/piIsolatedMcpProbe.js +20 -0
  17. package/dist/agent/port.d.ts +4 -2
  18. package/dist/agent/providerAuth.js +36 -3
  19. package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -8
  20. package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +11 -11
  21. package/dist/agent/stageTurnDriver.d.ts +33 -0
  22. package/dist/agent/stageTurnDriver.js +64 -0
  23. package/dist/agent/toolOutputBudget.d.ts +46 -0
  24. package/dist/agent/toolOutputBudget.js +97 -0
  25. package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
  26. package/dist/agent/toolOutputBudgetExtension.js +141 -0
  27. package/dist/agent/toolOutputSpill.d.ts +34 -0
  28. package/dist/agent/toolOutputSpill.js +85 -0
  29. package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
  30. package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
  31. package/dist/browser/anchor.d.ts +2 -0
  32. package/dist/browser/anchor.js +2 -1
  33. package/dist/browser/auditSink.d.ts +8 -0
  34. package/dist/browser/browserHost.d.ts +29 -0
  35. package/dist/browser/browserSweep.d.ts +4 -1
  36. package/dist/browser/browserSweep.js +7 -0
  37. package/dist/browser/browserTeardown.d.ts +1 -0
  38. package/dist/browser/browserTeardown.js +32 -2
  39. package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
  40. package/dist/browser/containerBrowserEndpoint.js +57 -0
  41. package/dist/browser/containerBrowserHost.d.ts +24 -0
  42. package/dist/browser/containerBrowserHost.js +147 -0
  43. package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
  44. package/dist/browser/dockerSandboxOrchestrator.js +142 -0
  45. package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
  46. package/dist/browser/fakeLiveViewRelay.js +94 -0
  47. package/dist/browser/fakeLiveViewSource.d.ts +6 -0
  48. package/dist/browser/fakeLiveViewSource.js +16 -0
  49. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
  50. package/dist/browser/fakeRemoteBrowserHost.js +2 -1
  51. package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
  52. package/dist/browser/fakeSandboxOrchestrator.js +44 -0
  53. package/dist/browser/gateHandoff.d.ts +0 -2
  54. package/dist/browser/gateHandoff.js +5 -11
  55. package/dist/browser/hostCapabilities.d.ts +23 -0
  56. package/dist/browser/hostCapabilities.js +23 -0
  57. package/dist/browser/humanLogin.d.ts +2 -2
  58. package/dist/browser/humanLogin.js +11 -2
  59. package/dist/browser/liveViewPersisted.d.ts +11 -0
  60. package/dist/browser/liveViewPersisted.js +22 -0
  61. package/dist/browser/liveViewRelay.d.ts +76 -0
  62. package/dist/browser/liveViewRelay.js +1 -0
  63. package/dist/browser/liveViewSessions.d.ts +25 -0
  64. package/dist/browser/liveViewSessions.js +139 -0
  65. package/dist/browser/liveViewSource.d.ts +25 -0
  66. package/dist/browser/liveViewSource.js +7 -0
  67. package/dist/browser/liveViewTickets.d.ts +46 -0
  68. package/dist/browser/liveViewTickets.js +122 -0
  69. package/dist/browser/localBrowserHost.d.ts +10 -0
  70. package/dist/browser/localBrowserHost.js +53 -7
  71. package/dist/browser/ownerScope.d.ts +10 -0
  72. package/dist/browser/ownerScope.js +6 -0
  73. package/dist/browser/profileStore.d.ts +5 -2
  74. package/dist/browser/profileStore.js +3 -5
  75. package/dist/browser/sandboxOrchestrator.d.ts +60 -0
  76. package/dist/browser/sandboxOrchestrator.js +8 -0
  77. package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
  78. package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
  79. package/dist/browser/stageBrowserEnv.d.ts +7 -0
  80. package/dist/browser/stageBrowserEnv.js +53 -15
  81. package/dist/browser/stageHandoff.d.ts +47 -0
  82. package/dist/browser/stageHandoff.js +101 -0
  83. package/dist/browser/stageProfileLock.d.ts +1 -0
  84. package/dist/browser/stageProfileLock.js +1 -3
  85. package/dist/browser/volumeProfileStore.d.ts +7 -0
  86. package/dist/browser/volumeProfileStore.js +35 -0
  87. package/dist/channels/config.d.ts +11 -0
  88. package/dist/channels/config.js +95 -0
  89. package/dist/channels/configWriter.d.ts +40 -0
  90. package/dist/channels/configWriter.js +148 -0
  91. package/dist/channels/connectionEdits.d.ts +34 -0
  92. package/dist/channels/connectionEdits.js +53 -0
  93. package/dist/channels/errorInfo.d.ts +18 -0
  94. package/dist/channels/errorInfo.js +71 -0
  95. package/dist/channels/errors.d.ts +12 -0
  96. package/dist/channels/errors.js +13 -0
  97. package/dist/channels/health.d.ts +41 -0
  98. package/dist/channels/health.js +99 -0
  99. package/dist/channels/host.d.ts +55 -0
  100. package/dist/channels/host.js +185 -0
  101. package/dist/channels/http.d.ts +31 -0
  102. package/dist/channels/http.js +79 -0
  103. package/dist/channels/index.d.ts +10 -0
  104. package/dist/channels/index.js +9 -0
  105. package/dist/channels/ledger.d.ts +37 -0
  106. package/dist/channels/ledger.js +95 -0
  107. package/dist/channels/providers/agentmail.d.ts +26 -0
  108. package/dist/channels/providers/agentmail.js +81 -0
  109. package/dist/channels/providers/kit.d.ts +40 -0
  110. package/dist/channels/providers/kit.js +78 -0
  111. package/dist/channels/providers/photonImessage.d.ts +45 -0
  112. package/dist/channels/providers/photonImessage.js +152 -0
  113. package/dist/channels/providers/photonWhatsapp.d.ts +49 -0
  114. package/dist/channels/providers/photonWhatsapp.js +146 -0
  115. package/dist/channels/providers/slack.d.ts +25 -0
  116. package/dist/channels/providers/slack.js +101 -0
  117. package/dist/channels/providers/telegram.d.ts +21 -0
  118. package/dist/channels/providers/telegram.js +100 -0
  119. package/dist/channels/providers.d.ts +5 -0
  120. package/dist/channels/providers.js +22 -0
  121. package/dist/channels/registry.d.ts +54 -0
  122. package/dist/channels/registry.js +186 -0
  123. package/dist/channels/secretStore.d.ts +21 -0
  124. package/dist/channels/secretStore.js +75 -0
  125. package/dist/channels/secrets.d.ts +15 -0
  126. package/dist/channels/secrets.js +50 -0
  127. package/dist/channels/service.d.ts +54 -0
  128. package/dist/channels/service.js +307 -0
  129. package/dist/channels/stageChannels.d.ts +45 -0
  130. package/dist/channels/stageChannels.js +110 -0
  131. package/dist/channels/types.d.ts +142 -0
  132. package/dist/channels/types.js +1 -0
  133. package/dist/cli/browserCommand.d.ts +3 -1
  134. package/dist/cli/browserCommand.js +25 -20
  135. package/dist/cli/doctorCommand.js +3 -1
  136. package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
  137. package/dist/cli/resolveCliProjectRoot.js +27 -0
  138. package/dist/cli/runCommand.d.ts +1 -1
  139. package/dist/cli/runCommand.js +59 -17
  140. package/dist/cli/skillsCommand.d.ts +20 -0
  141. package/dist/cli/skillsCommand.js +74 -31
  142. package/dist/cli/validateCommand.d.ts +1 -1
  143. package/dist/cli/validateCommand.js +22 -4
  144. package/dist/cli.js +4 -4
  145. package/dist/config/browseCatalog.d.ts +2 -0
  146. package/dist/config/browseCatalog.js +6 -0
  147. package/dist/config/catalogRelativePath.d.ts +4 -0
  148. package/dist/config/catalogRelativePath.js +37 -9
  149. package/dist/config/createStage.d.ts +1 -0
  150. package/dist/config/createStage.js +11 -2
  151. package/dist/config/createTask.d.ts +30 -0
  152. package/dist/config/createTask.js +161 -0
  153. package/dist/config/createTrigger.d.ts +15 -0
  154. package/dist/config/createTrigger.js +32 -25
  155. package/dist/config/draftPackage.d.ts +42 -0
  156. package/dist/config/draftPackage.js +220 -23
  157. package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
  158. package/dist/config/ensureCatalogScanRoot.js +6 -2
  159. package/dist/config/findTaskInCatalog.d.ts +33 -0
  160. package/dist/config/findTaskInCatalog.js +69 -0
  161. package/dist/config/hostConfig.d.ts +21 -0
  162. package/dist/config/hostConfig.js +114 -1
  163. package/dist/config/importTask.d.ts +19 -0
  164. package/dist/config/importTask.js +133 -0
  165. package/dist/config/installSkillRequest.d.ts +40 -0
  166. package/dist/config/installSkillRequest.js +286 -0
  167. package/dist/config/loadOutcome.d.ts +11 -6
  168. package/dist/config/loadPipeline.js +27 -4
  169. package/dist/config/loadStage.js +17 -0
  170. package/dist/config/loadStageflowManifest.js +4 -1
  171. package/dist/config/loadTask.js +2 -0
  172. package/dist/config/loadTrigger.js +2 -0
  173. package/dist/config/mergePipelineIncludes.d.ts +3 -0
  174. package/dist/config/mergePipelineIncludes.js +30 -0
  175. package/dist/config/normalizePipelineStageEntry.js +10 -1
  176. package/dist/config/pipelineStageKeys.js +1 -0
  177. package/dist/config/printTargetYaml.js +0 -1
  178. package/dist/config/publishDraftPackage.d.ts +5 -1
  179. package/dist/config/publishDraftPackage.js +4 -1
  180. package/dist/config/resolveCatalogRoots.d.ts +4 -1
  181. package/dist/config/resolveCatalogRoots.js +18 -1
  182. package/dist/config/skillUsageIndex.d.ts +6 -0
  183. package/dist/config/skillUsageIndex.js +39 -0
  184. package/dist/config/stageBrowser.js +13 -0
  185. package/dist/config/triggerAdapterStatus.d.ts +11 -0
  186. package/dist/config/triggerAdapterStatus.js +90 -0
  187. package/dist/config/triggerCatalog.d.ts +41 -2
  188. package/dist/config/triggerCatalog.js +107 -15
  189. package/dist/config/updateTask.d.ts +30 -0
  190. package/dist/config/updateTask.js +119 -0
  191. package/dist/config/updateTrigger.d.ts +38 -0
  192. package/dist/config/updateTrigger.js +220 -0
  193. package/dist/config/validateCatalog.d.ts +9 -2
  194. package/dist/config/validateCatalog.js +71 -7
  195. package/dist/config/workshopCatalogFilenames.d.ts +37 -0
  196. package/dist/config/workshopCatalogFilenames.js +125 -0
  197. package/dist/config/workshopCatalogRepair.d.ts +7 -0
  198. package/dist/config/workshopCatalogRepair.js +144 -0
  199. package/dist/config/yamlParsePosition.d.ts +4 -0
  200. package/dist/config/yamlParsePosition.js +9 -0
  201. package/dist/errors/codes.d.ts +1 -1
  202. package/dist/errors/codes.js +1 -0
  203. package/dist/hosted/runtime.d.ts +2 -2
  204. package/dist/hosted/types.d.ts +1 -1
  205. package/dist/hosted/types.js +1 -1
  206. package/dist/index.d.ts +3 -2
  207. package/dist/index.js +2 -1
  208. package/dist/mcp/catalogTools.js +1 -0
  209. package/dist/mcp/getStartedTool.js +15 -6
  210. package/dist/mcp/triggerTools.js +2 -8
  211. package/dist/operatorAgent/agentPort.d.ts +11 -0
  212. package/dist/operatorAgent/agentPort.js +12 -0
  213. package/dist/operatorAgent/index.d.ts +1 -0
  214. package/dist/operatorAgent/index.js +1 -0
  215. package/dist/operatorAgent/piModel.js +26 -6
  216. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  217. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
  218. package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
  219. package/dist/package-meta.d.ts +1 -1
  220. package/dist/package-meta.js +1 -1
  221. package/dist/preflight/channelChecks.d.ts +17 -0
  222. package/dist/preflight/channelChecks.js +106 -0
  223. package/dist/preflight/pipelinePreflight.d.ts +9 -2
  224. package/dist/preflight/pipelinePreflight.js +9 -1
  225. package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
  226. package/dist/project/ensureProjectRootsFromEnv.js +22 -0
  227. package/dist/project/globalHome.d.ts +1 -0
  228. package/dist/project/globalHome.js +18 -0
  229. package/dist/project/workspaceCatalog.d.ts +10 -0
  230. package/dist/project/workspaceCatalog.js +91 -0
  231. package/dist/runstore/backup.js +14 -4
  232. package/dist/runstore/port.d.ts +11 -1
  233. package/dist/runstore/restore.js +5 -2
  234. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
  235. package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
  236. package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
  237. package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
  238. package/dist/runstore/sqlite/migrations/index.js +2 -0
  239. package/dist/runstore/stageSnapshot.d.ts +3 -1
  240. package/dist/runstore/stageSnapshot.js +19 -0
  241. package/dist/runstore/workspaceLayout.js +27 -0
  242. package/dist/runstore/writeContainedArtifact.d.ts +1 -0
  243. package/dist/runstore/writeContainedArtifact.js +52 -0
  244. package/dist/runtime/credentialBinding.js +14 -6
  245. package/dist/runtime/emailSource.js +20 -28
  246. package/dist/runtime/githubPollSource.js +20 -28
  247. package/dist/runtime/pipelineRunner.d.ts +4 -4
  248. package/dist/runtime/pipelineRunner.js +3 -1
  249. package/dist/runtime/pipelineScheduler.d.ts +2 -2
  250. package/dist/runtime/pipelineScheduler.js +3 -0
  251. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  252. package/dist/runtime/resumeReconstruct.d.ts +2 -2
  253. package/dist/runtime/resumeTimedOut.d.ts +2 -2
  254. package/dist/runtime/runManager.d.ts +18 -2
  255. package/dist/runtime/runManager.js +99 -31
  256. package/dist/runtime/runRetryCoordinator.d.ts +2 -2
  257. package/dist/runtime/scheduleSource.js +15 -24
  258. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  259. package/dist/runtime/stageAttemptBootstrap.js +26 -1
  260. package/dist/runtime/stageProcessLauncher.js +33 -3
  261. package/dist/runtime/stageRoots.js +8 -1
  262. package/dist/runtime/stageRunner.d.ts +2 -2
  263. package/dist/runtime/stageRunner.js +8 -2
  264. package/dist/runtime/stageWorker.js +2 -2
  265. package/dist/runtime/triggerPort.d.ts +5 -3
  266. package/dist/runtime/triggerRunner.d.ts +1 -0
  267. package/dist/runtime/triggerRunner.js +27 -20
  268. package/dist/runtime/webhookAccept.d.ts +25 -0
  269. package/dist/runtime/webhookAccept.js +56 -0
  270. package/dist/server/bootstrap.d.ts +10 -2
  271. package/dist/server/bootstrap.js +19 -1
  272. package/dist/server/channelRoutes.d.ts +9 -0
  273. package/dist/server/channelRoutes.js +88 -0
  274. package/dist/server/controlToken.d.ts +1 -1
  275. package/dist/server/controlToken.js +18 -2
  276. package/dist/server/healthSurfaces.d.ts +12 -0
  277. package/dist/server/healthSurfaces.js +27 -6
  278. package/dist/server/http.d.ts +19 -10
  279. package/dist/server/http.js +616 -85
  280. package/dist/server/liveViewPath.d.ts +9 -0
  281. package/dist/server/liveViewPath.js +8 -0
  282. package/dist/server/liveViewRoutes.d.ts +58 -0
  283. package/dist/server/liveViewRoutes.js +565 -0
  284. package/dist/server/mcpHost.d.ts +4 -2
  285. package/dist/server/mcpHost.js +2 -0
  286. package/dist/server/readCatalogFile.d.ts +29 -0
  287. package/dist/server/readCatalogFile.js +215 -0
  288. package/dist/server/triggerRoutes.d.ts +1 -0
  289. package/dist/server/triggerRoutes.js +200 -73
  290. package/dist/tools/sendMessage.d.ts +21 -0
  291. package/dist/tools/sendMessage.js +25 -0
  292. package/dist/tools/writeStageArtifact.d.ts +9 -0
  293. package/dist/tools/writeStageArtifact.js +2 -47
  294. package/dist/types/pipeline.d.ts +5 -2
  295. package/dist/types/stage.d.ts +14 -1
  296. package/dist/types/stageflowManifest.d.ts +1 -1
  297. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  298. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  299. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  300. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  301. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  302. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  303. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  304. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  305. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  306. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  307. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  308. package/dist/ui/assets/index-BOhqnPNg.js +260 -0
  309. package/dist/ui/assets/{index-Bv4p1aPK.css → index-Clv4sJpU.css} +1 -1
  310. package/dist/ui/index.html +4 -4
  311. package/dist/workshop/autosave.d.ts +2 -2
  312. package/dist/workshop/autosave.js +4 -6
  313. package/dist/workshop/buildStore.d.ts +2 -0
  314. package/dist/workshop/chatAttachments.d.ts +27 -0
  315. package/dist/workshop/chatAttachments.js +98 -0
  316. package/dist/workshop/chatTurn.d.ts +5 -2
  317. package/dist/workshop/chatTurn.js +22 -18
  318. package/dist/workshop/sessionMutations.d.ts +17 -0
  319. package/dist/workshop/sessionMutations.js +107 -0
  320. package/dist/workshop/sessionStore.d.ts +10 -0
  321. package/dist/workshop/sessionStore.js +52 -0
  322. package/dist/workshop/workshopPin.d.ts +4 -3
  323. package/dist/workshop/workshopPin.js +26 -6
  324. package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
  325. package/dist/workshop/workshopTranscriptTurn.js +46 -0
  326. package/examples/README.md +1 -1
  327. package/examples/archify-on-pr/README.md +1 -1
  328. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
  329. package/examples/archify-on-pr/author-diagrams.yaml +1 -0
  330. package/examples/channels-smoke/README.md +12 -0
  331. package/examples/channels-smoke/channels-smoke.pipeline.yaml +11 -0
  332. package/examples/channels-smoke/channels-smoke.task.yaml +3 -0
  333. package/examples/channels-smoke/send-email.yaml +32 -0
  334. package/examples/channels-smoke/send-text.yaml +31 -0
  335. package/examples/conditional-fork/branch-a.yaml +1 -1
  336. package/examples/conditional-fork/decide.yaml +2 -1
  337. package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
  338. package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
  339. package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
  340. package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
  341. package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
  342. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
  343. package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
  344. package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
  345. package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
  346. package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
  347. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
  348. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
  349. package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
  350. package/examples/hello-world/hello.pipeline.yaml +2 -2
  351. package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
  352. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
  353. package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
  354. package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
  355. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
  356. package/examples/stageflow.yaml +6 -0
  357. package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
  358. package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
  359. package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
  360. package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
  361. package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
  362. package/examples/triggers-readiness/README.md +48 -0
  363. package/examples/triggers-readiness/readiness-emit.yaml +18 -0
  364. package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
  365. package/examples/triggers-readiness/readiness.task.yaml +6 -0
  366. package/examples/triggers-readiness/stageflow.yaml +13 -0
  367. package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
  368. package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
  369. package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
  370. package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
  371. package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
  372. package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
  373. package/package.json +8 -3
  374. package/skills/stageflow/references/control-surface.md +1 -1
  375. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  376. package/skills/stageflow-run/SKILL.md +1 -1
  377. package/skills/stageflow-run/references/mcp-call.md +1 -1
  378. package/skills/stageflow-setup/SKILL.md +1 -1
  379. package/dist/ui/assets/index-59cjF0OA.js +0 -133
@@ -1,30 +1,70 @@
1
1
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadHostConfig } from "../config/hostConfig.js";
3
+ import { browserHostForOwner, loadHostConfig } from "../config/hostConfig.js";
4
4
  import { stageDir } from "../runstore/paths.js";
5
5
  import { safeAudit } from "./auditSink.js";
6
6
  import { assertBrowserSitesAllowed } from "./sitePolicy.js";
7
7
  import { BROWSER_ENV_FILENAME, BROWSER_POLICY_FILENAME, } from "./browserHost.js";
8
8
  import { ensureRunProfileBrowser } from "./anchor.js";
9
9
  import { BROWSER_CLOSED_FILENAME, closeBrowserSession, defaultBrowserRunner, writeSessionOwner, } from "./browserTeardown.js";
10
- import { defaultDisplayProbe, loginPageUrl, noScreenError } from "./humanLogin.js";
10
+ import { loginPageUrl } from "./humanLogin.js";
11
+ import { persistStageHandoff, resolveStageHandoffCapabilities } from "./stageHandoff.js";
11
12
  import { ensureStageLoginCheck, OPEN_COMMAND_TIMEOUT_MS } from "./loginCheck.js";
12
13
  import { readPersistedBrowserEnv } from "./persistedEnv.js";
14
+ import { createContainerBrowserHost } from "./containerBrowserHost.js";
15
+ import { createDockerSandboxOrchestrator } from "./dockerSandboxOrchestrator.js";
16
+ import { createVolumeProfileStore } from "./volumeProfileStore.js";
13
17
  import { createLocalBrowserHost } from "./localBrowserHost.js";
14
18
  import { createLocalProfileStore } from "./localProfileStore.js";
15
- import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
19
+ import { localOwnerScope } from "./ownerScope.js";
16
20
  export { BROWSER_ENV_FILENAME };
17
21
  export async function readStagePersistedBrowserEnv(runDir, stageId) {
18
22
  return readPersistedBrowserEnv(path.join(stageDir(runDir, stageId), BROWSER_ENV_FILENAME));
19
23
  }
20
24
  let defaultSupport;
21
- export function defaultStageBrowserSupport() {
22
- defaultSupport ??= {
23
- host: createLocalBrowserHost(),
25
+ export function hostLaunchOptions() {
26
+ const config = loadHostConfig();
27
+ return {
28
+ launchArgs: config.browserLaunchArgs,
29
+ ...(config.browserExecutablePath !== undefined
30
+ ? { executablePath: config.browserExecutablePath }
31
+ : {}),
32
+ };
33
+ }
34
+ function configuredBrowserSupport(liveView) {
35
+ const config = loadHostConfig();
36
+ if (browserHostForOwner(config, localOwnerScope()) === "container") {
37
+ const container = config.browserContainer;
38
+ return {
39
+ host: createContainerBrowserHost({
40
+ orchestrator: createDockerSandboxOrchestrator({
41
+ image: container.image,
42
+ shmSize: container.shmSize,
43
+ memory: container.memory,
44
+ pidsLimit: container.pidsLimit,
45
+ user: "1000:1000",
46
+ }),
47
+ liveView,
48
+ local: hostLaunchOptions(),
49
+ }),
50
+ profiles: createVolumeProfileStore(),
51
+ };
52
+ }
53
+ return {
54
+ host: createLocalBrowserHost({ liveView, ...hostLaunchOptions() }),
24
55
  profiles: createLocalProfileStore(),
25
56
  };
57
+ }
58
+ export function defaultStageBrowserSupport() {
59
+ defaultSupport ??= configuredBrowserSupport("none");
26
60
  return defaultSupport;
27
61
  }
62
+ let consoleSupport;
63
+ /** Browser support for a process that serves the live view routes (`sf ui`, `sf mcp`). */
64
+ export function consoleStageBrowserSupport() {
65
+ consoleSupport ??= configuredBrowserSupport("relay");
66
+ return consoleSupport;
67
+ }
28
68
  /**
29
69
  * Computed once per stage and persisted in the run dir: agent-browser silently
30
70
  * relaunches the browser when a command's launch env differs from the daemon's,
@@ -34,11 +74,9 @@ export async function resolveStageBrowserEnv(support, input) {
34
74
  const { browser } = input;
35
75
  if (browser === undefined)
36
76
  return undefined;
37
- if (input.humanLogin === true) {
38
- const screen = (support.display ?? defaultDisplayProbe)();
39
- if (!screen.hasDisplay)
40
- throw noScreenError(screen.docker);
41
- }
77
+ const capabilities = resolveStageHandoffCapabilities(support, {
78
+ ...(input.humanLogin === true ? { humanLogin: true } : {}),
79
+ });
42
80
  if ((browser.allow_domains?.length ?? 0) > 0 ||
43
81
  browser.check !== undefined ||
44
82
  browser.login_url !== undefined) {
@@ -46,10 +84,11 @@ export async function resolveStageBrowserEnv(support, input) {
46
84
  }
47
85
  const dir = stageDir(input.runDir, input.stageId);
48
86
  const file = path.join(dir, BROWSER_ENV_FILENAME);
49
- // TODO(multi-tenant): open the profile in the run owner's scope, not the fixed local scope.
87
+ await mkdir(dir, { recursive: true });
88
+ await persistStageHandoff({ runDir: input.runDir, stageId: input.stageId }, capabilities);
50
89
  const profile = browser.profile !== undefined
51
90
  ? await support.profiles.open({
52
- scope: LOCAL_BROWSER_SCOPE,
91
+ scope: input.scope,
53
92
  name: browser.profile,
54
93
  })
55
94
  : undefined;
@@ -97,10 +136,9 @@ export async function resolveStageBrowserEnv(support, input) {
97
136
  if (persisted === undefined) {
98
137
  await writeEnv(fresh);
99
138
  if (browser.profile !== undefined) {
100
- // TODO(multi-tenant): audit the run owner's scope.
101
139
  await safeAudit(support.audit, {
102
140
  event: "profile_used",
103
- scope: LOCAL_BROWSER_SCOPE,
141
+ scope: input.scope,
104
142
  profile: browser.profile,
105
143
  runId: input.runId,
106
144
  stageId: input.stageId,
@@ -0,0 +1,47 @@
1
+ import type { StageBrowserConfig } from "../types/stage.js";
2
+ import type { StageBrowserSupport } from "./browserHost.js";
3
+ import { type GateHandoff, type HostGateContext } from "./gateHandoff.js";
4
+ import { type BrowserHostCapabilities } from "./hostCapabilities.js";
5
+ /** The slice of the host capability record a stage's handoff depends on; persisted per stage. */
6
+ export type StageHandoffCapabilities = Pick<BrowserHostCapabilities, "display" | "liveView">;
7
+ type HandoffInput = {
8
+ runDir: string;
9
+ runId: string;
10
+ stageId: string;
11
+ };
12
+ /**
13
+ * Probes the display, applies the host's capabilities, and rejects a human-login
14
+ * stage that has neither a screen nor a live view. Pure of disk writes.
15
+ */
16
+ export declare function resolveStageHandoffCapabilities(support: StageBrowserSupport, options?: {
17
+ humanLogin?: boolean;
18
+ }): StageHandoffCapabilities;
19
+ export declare function persistStageHandoff(input: {
20
+ runDir: string;
21
+ stageId: string;
22
+ }, capabilities: StageHandoffCapabilities): Promise<void>;
23
+ /** Persisted capabilities of a stage; undefined when none were recorded (older runs, direct stage runs). */
24
+ export declare function readStageHandoff(input: {
25
+ runDir: string;
26
+ stageId: string;
27
+ }): Promise<StageHandoffCapabilities | undefined>;
28
+ export declare function gateHandoffFor(context: {
29
+ runId: string;
30
+ stageId: string;
31
+ capabilities: StageHandoffCapabilities;
32
+ }): GateHandoff | undefined;
33
+ /** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
34
+ export declare function hostGateContextFor(browser: StageBrowserConfig | undefined, context: {
35
+ runId: string;
36
+ stageId: string;
37
+ capabilities: StageHandoffCapabilities;
38
+ }): HostGateContext | undefined;
39
+ /** The handoff for a stage's gates and login prompt. `dirStageId` is the manifest id when it differs from the gate's stage id. */
40
+ export declare function stageGateHandoff(input: HandoffInput & {
41
+ dirStageId?: string;
42
+ }): Promise<GateHandoff | undefined>;
43
+ /** Host-owned gate fields for a stage; undefined for a stage without a browser. */
44
+ export declare function stageGateContext(input: HandoffInput & {
45
+ browser: StageBrowserConfig | undefined;
46
+ }): Promise<HostGateContext | undefined>;
47
+ export {};
@@ -0,0 +1,101 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stageDir } from "../runstore/paths.js";
4
+ import { browserSite, } from "./gateHandoff.js";
5
+ import { resolveBrowserHostCapabilities, } from "./hostCapabilities.js";
6
+ import { defaultDisplayProbe, noScreenError } from "./humanLogin.js";
7
+ const BROWSER_CAPABILITIES_FILENAME = "browser-capabilities.json";
8
+ /**
9
+ * Probes the display, applies the host's capabilities, and rejects a human-login
10
+ * stage that has neither a screen nor a live view. Pure of disk writes.
11
+ */
12
+ export function resolveStageHandoffCapabilities(support, options = {}) {
13
+ const capabilities = resolveBrowserHostCapabilities(support.host.capabilities);
14
+ const probe = support.display ?? defaultDisplayProbe;
15
+ if (support.host.capabilities === undefined) {
16
+ capabilities.display = probe().hasDisplay ? "local_window" : "headless_only";
17
+ }
18
+ else if (capabilities.display === "local_window" && support.display !== undefined && !support.display().hasDisplay) {
19
+ capabilities.display = "headless_only";
20
+ }
21
+ if (options.humanLogin === true &&
22
+ capabilities.display === "headless_only" &&
23
+ capabilities.liveView === "none") {
24
+ throw noScreenError(probe().docker);
25
+ }
26
+ if (options.humanLogin === true &&
27
+ capabilities.display !== "local_window" &&
28
+ capabilities.viewerInput === "view_only") {
29
+ throw new Error("A human login stage needs an interactive live view, but this browser host offers a view-only one (missing capability viewer_input: interactive).");
30
+ }
31
+ return { display: capabilities.display, liveView: capabilities.liveView };
32
+ }
33
+ export async function persistStageHandoff(input, capabilities) {
34
+ const dir = stageDir(input.runDir, input.stageId);
35
+ await mkdir(dir, { recursive: true });
36
+ await writeFile(path.join(dir, BROWSER_CAPABILITIES_FILENAME), `${JSON.stringify({ display: capabilities.display, liveView: capabilities.liveView })}\n`, { mode: 0o600 });
37
+ }
38
+ /** Persisted capabilities of a stage; undefined when none were recorded (older runs, direct stage runs). */
39
+ export async function readStageHandoff(input) {
40
+ const file = path.join(stageDir(input.runDir, input.stageId), BROWSER_CAPABILITIES_FILENAME);
41
+ try {
42
+ const parsed = JSON.parse(await readFile(file, "utf8"));
43
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
44
+ const { display, liveView } = resolveBrowserHostCapabilities(parsed);
45
+ return { display, liveView };
46
+ }
47
+ }
48
+ catch (err) {
49
+ if (err.code !== "ENOENT")
50
+ throw err;
51
+ }
52
+ return undefined;
53
+ }
54
+ export function gateHandoffFor(context) {
55
+ const { capabilities } = context;
56
+ if (capabilities.display === "local_window")
57
+ return { kind: "local_window" };
58
+ if (capabilities.liveView !== "none") {
59
+ return {
60
+ kind: "live_view",
61
+ url: `/api/runs/${encodeURIComponent(context.runId)}/stages/${encodeURIComponent(context.stageId)}/live-view`,
62
+ };
63
+ }
64
+ return undefined;
65
+ }
66
+ /** What the Host stamps on every gate of a browser stage; the agent cannot supply it. */
67
+ export function hostGateContextFor(browser, context) {
68
+ if (browser === undefined)
69
+ return undefined;
70
+ const site = browserSite(browser);
71
+ const handoff = gateHandoffFor(context);
72
+ return {
73
+ ...(handoff !== undefined ? { handoff } : {}),
74
+ ...(site !== undefined ? { site } : {}),
75
+ ...(browser.profile !== undefined ? { profile: browser.profile } : {}),
76
+ };
77
+ }
78
+ // No record (older runs, direct stage runs) keeps its historical meaning: a local window.
79
+ const LEGACY_CAPABILITIES = { display: "local_window", liveView: "none" };
80
+ async function capabilitiesOf(input) {
81
+ return ((await readStageHandoff({ runDir: input.runDir, stageId: input.dirStageId ?? input.stageId })) ??
82
+ LEGACY_CAPABILITIES);
83
+ }
84
+ /** The handoff for a stage's gates and login prompt. `dirStageId` is the manifest id when it differs from the gate's stage id. */
85
+ export async function stageGateHandoff(input) {
86
+ return gateHandoffFor({
87
+ runId: input.runId,
88
+ stageId: input.stageId,
89
+ capabilities: await capabilitiesOf(input),
90
+ });
91
+ }
92
+ /** Host-owned gate fields for a stage; undefined for a stage without a browser. */
93
+ export async function stageGateContext(input) {
94
+ if (input.browser === undefined)
95
+ return undefined;
96
+ return hostGateContextFor(input.browser, {
97
+ runId: input.runId,
98
+ stageId: input.stageId,
99
+ capabilities: await capabilitiesOf(input),
100
+ });
101
+ }
@@ -2,6 +2,7 @@ import type { StageBrowserSupport } from "./browserHost.js";
2
2
  import type { ProfileLock, ProfileLockOwner, RunLiveness } from "./profileLock.js";
3
3
  export declare function stageProfileLock(support: StageBrowserSupport): ProfileLock;
4
4
  export type ProfileWaitInput = {
5
+ scope: string;
5
6
  profile: string;
6
7
  owner: ProfileLockOwner;
7
8
  isRunLive?: RunLiveness;
@@ -1,5 +1,4 @@
1
1
  import { createLocalProfileLock } from "./localProfileLock.js";
2
- import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
3
2
  const DEFAULT_LOCK_POLL_MS = 250;
4
3
  let defaultLock;
5
4
  export function stageProfileLock(support) {
@@ -14,8 +13,7 @@ export function stageProfileLock(support) {
14
13
  */
15
14
  export async function acquireStageProfile(support, input) {
16
15
  const locks = stageProfileLock(support);
17
- // TODO(multi-tenant): take the scope from the run owner instead of the fixed local scope.
18
- const key = { scope: LOCAL_BROWSER_SCOPE, name: input.profile };
16
+ const key = { scope: input.scope, name: input.profile };
19
17
  const pollMs = support.lockPollMs ?? DEFAULT_LOCK_POLL_MS;
20
18
  let announced;
21
19
  for (;;) {
@@ -0,0 +1,7 @@
1
+ import { type ProfileStore } from "./profileStore.js";
2
+ /**
3
+ * Profiles that live in a volume the sandbox orchestrator provisions. The handle
4
+ * carries a reference, never a folder; creating, sizing and removing the volume
5
+ * belong to the orchestrator, so `delete` only forgets the reference.
6
+ */
7
+ export declare function createVolumeProfileStore(): ProfileStore;
@@ -0,0 +1,35 @@
1
+ import { validateProfileKey, validateProfileScope, } from "./profileStore.js";
2
+ /**
3
+ * Profiles that live in a volume the sandbox orchestrator provisions. The handle
4
+ * carries a reference, never a folder; creating, sizing and removing the volume
5
+ * belong to the orchestrator, so `delete` only forgets the reference.
6
+ */
7
+ export function createVolumeProfileStore() {
8
+ const scopes = new Map();
9
+ return {
10
+ async open(rawKey) {
11
+ const key = validateProfileKey(rawKey);
12
+ let profiles = scopes.get(key.scope);
13
+ if (!profiles) {
14
+ profiles = new Map();
15
+ scopes.set(key.scope, profiles);
16
+ }
17
+ let handle = profiles.get(key.name);
18
+ if (!handle) {
19
+ handle = { key, volumeRef: `${key.scope}/${key.name}` };
20
+ profiles.set(key.name, handle);
21
+ }
22
+ return handle;
23
+ },
24
+ async list(scope) {
25
+ return [...(scopes.get(validateProfileScope(scope))?.keys() ?? [])].sort();
26
+ },
27
+ async delete(rawKey) {
28
+ const key = validateProfileKey(rawKey);
29
+ scopes.get(key.scope)?.delete(key.name);
30
+ },
31
+ async deleteScope(scope) {
32
+ scopes.delete(validateProfileScope(scope));
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { ConnectionEntry, CredentialEntry } from "./types.js";
2
+ export declare const CONNECTIONS_FILE_LIMIT_BYTES = 262144;
3
+ export declare const MAX_CONNECTIONS = 100;
4
+ export declare const MAX_CREDENTIALS = 20;
5
+ export type ConnectionsFile = {
6
+ credentials: CredentialEntry[];
7
+ connections: ConnectionEntry[];
8
+ };
9
+ export declare function parseConnectionsYaml(text: string): ConnectionsFile;
10
+ /** A missing file is an empty configuration. Any other fault stops startup instead of half-applying. */
11
+ export declare function loadConnectionsFile(file: string): ConnectionsFile;
@@ -0,0 +1,95 @@
1
+ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
2
+ import { parseDocument } from "yaml";
3
+ import { z } from "zod";
4
+ import { ChannelError } from "./errors.js";
5
+ import { SECRET_REF_PATTERN } from "./secrets.js";
6
+ export const CONNECTIONS_FILE_LIMIT_BYTES = 262144;
7
+ export const MAX_CONNECTIONS = 100;
8
+ export const MAX_CREDENTIALS = 20;
9
+ const idSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/);
10
+ const providerSchema = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
11
+ const secretRefSchema = z.string().regex(SECRET_REF_PATTERN);
12
+ const secretFieldSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/);
13
+ const secretsSchema = z.record(secretFieldSchema, secretRefSchema).default({});
14
+ const credentialSchema = z.object({
15
+ id: idSchema,
16
+ provider: providerSchema,
17
+ secrets: secretsSchema,
18
+ }).strict();
19
+ const connectionSchema = z.object({
20
+ id: idSchema,
21
+ provider: providerSchema,
22
+ displayName: z.string().min(1).max(200).optional(),
23
+ enabled: z.boolean().default(true),
24
+ credential: idSchema.optional(),
25
+ config: z.record(z.string(), z.unknown()).default({}),
26
+ secrets: secretsSchema,
27
+ }).strict();
28
+ const fileSchema = z.object({
29
+ version: z.literal(1),
30
+ credentials: z.array(credentialSchema).max(MAX_CREDENTIALS).default([]),
31
+ connections: z.array(connectionSchema).max(MAX_CONNECTIONS).default([]),
32
+ }).strict();
33
+ export function parseConnectionsYaml(text) {
34
+ const document = parseDocument(text, { uniqueKeys: true, strict: true });
35
+ if (document.errors.length || document.warnings.length)
36
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
37
+ let raw;
38
+ try {
39
+ raw = document.toJS({ maxAliasCount: 0 });
40
+ }
41
+ catch {
42
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
43
+ }
44
+ const parsed = fileSchema.safeParse(raw);
45
+ if (!parsed.success)
46
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
47
+ assertUnique(parsed.data.credentials.map(entry => entry.id));
48
+ assertUnique(parsed.data.connections.map(entry => entry.id));
49
+ return {
50
+ credentials: parsed.data.credentials.map(entry => ({ ...entry, secrets: entry.secrets })),
51
+ connections: parsed.data.connections.map(entry => ({ ...entry, secrets: entry.secrets })),
52
+ };
53
+ }
54
+ function assertUnique(ids) {
55
+ if (new Set(ids).size !== ids.length)
56
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
57
+ }
58
+ /** A missing file is an empty configuration. Any other fault stops startup instead of half-applying. */
59
+ export function loadConnectionsFile(file) {
60
+ let descriptor;
61
+ try {
62
+ descriptor = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK);
63
+ }
64
+ catch (error) {
65
+ if (error.code === "ENOENT")
66
+ return { credentials: [], connections: [] };
67
+ throw new ChannelError("CHANNEL_STORAGE_FAILED");
68
+ }
69
+ try {
70
+ const stat = fstatSync(descriptor);
71
+ if (!stat.isFile())
72
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
73
+ if (stat.size > CONNECTIONS_FILE_LIMIT_BYTES)
74
+ throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
75
+ const bytes = Buffer.alloc(CONNECTIONS_FILE_LIMIT_BYTES + 1);
76
+ let length = 0;
77
+ while (length < bytes.length) {
78
+ const count = readSync(descriptor, bytes, length, bytes.length - length, null);
79
+ if (!count)
80
+ break;
81
+ length += count;
82
+ }
83
+ if (length > CONNECTIONS_FILE_LIMIT_BYTES)
84
+ throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
85
+ return parseConnectionsYaml(bytes.toString("utf8", 0, length));
86
+ }
87
+ catch (error) {
88
+ if (error instanceof ChannelError)
89
+ throw error;
90
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
91
+ }
92
+ finally {
93
+ closeSync(descriptor);
94
+ }
95
+ }
@@ -0,0 +1,40 @@
1
+ import type { ConnectionEntry, CredentialEntry } from "./types.js";
2
+ /** Plain-object form of an entry. Optional fields are omitted when unset. */
3
+ export type ConnectionYamlEntry = {
4
+ id: string;
5
+ provider: string;
6
+ displayName?: string;
7
+ enabled?: boolean;
8
+ credential?: string;
9
+ config?: Record<string, unknown>;
10
+ secrets?: Record<string, string>;
11
+ };
12
+ export declare function toYamlEntry(entry: Pick<ConnectionEntry, "id" | "provider" | "enabled" | "config" | "secrets"> & {
13
+ displayName?: string;
14
+ credential?: string;
15
+ }): ConnectionYamlEntry;
16
+ /** Edits keep comments and key order that the user wrote. */
17
+ export declare function addConnectionYaml(text: string, entry: ConnectionYamlEntry, credential?: {
18
+ id: string;
19
+ provider: string;
20
+ secrets: Record<string, string>;
21
+ }): string;
22
+ export type ConnectionPatch = {
23
+ displayName?: string | null;
24
+ enabled?: boolean;
25
+ config?: Record<string, unknown>;
26
+ secrets?: Record<string, string>;
27
+ };
28
+ export declare function updateConnectionYaml(text: string, id: string, patch: ConnectionPatch): string;
29
+ /** Removes the entry and any credential that no remaining connection uses. Returns what was dropped. */
30
+ export declare function removeConnectionYaml(text: string, id: string): {
31
+ text: string;
32
+ removedCredential?: string;
33
+ };
34
+ export declare function readConnectionsText(file: string): string;
35
+ /**
36
+ * Replaces the file atomically. A bind-mounted file cannot be renamed over, so that case falls
37
+ * back to writing in place.
38
+ */
39
+ export declare function writeConnectionsText(file: string, text: string): void;
40
+ export type { CredentialEntry };
@@ -0,0 +1,148 @@
1
+ import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isSeq, parseDocument } from "yaml";
4
+ import { CONNECTIONS_FILE_LIMIT_BYTES } from "./config.js";
5
+ import { ChannelError } from "./errors.js";
6
+ export function toYamlEntry(entry) {
7
+ return {
8
+ id: entry.id,
9
+ provider: entry.provider,
10
+ ...(entry.displayName ? { displayName: entry.displayName } : {}),
11
+ ...(entry.enabled === false ? { enabled: false } : {}),
12
+ ...(entry.credential ? { credential: entry.credential } : {}),
13
+ ...(Object.keys(entry.config).length ? { config: entry.config } : {}),
14
+ ...(Object.keys(entry.secrets).length ? { secrets: entry.secrets } : {}),
15
+ };
16
+ }
17
+ function open(text) {
18
+ const source = text.trim().length ? text : "version: 1\n";
19
+ const document = parseDocument(source, { uniqueKeys: true, strict: true });
20
+ if (document.errors.length)
21
+ throw new ChannelError("CHANNEL_INVALID_INPUT");
22
+ return document;
23
+ }
24
+ function sequence(document, key) {
25
+ let seq = document.get(key, true);
26
+ if (!isSeq(seq)) {
27
+ document.set(key, document.createNode([]));
28
+ seq = document.get(key, true);
29
+ }
30
+ const result = seq;
31
+ result.flow = false;
32
+ return result;
33
+ }
34
+ function indexOf(seq, id) {
35
+ return seq.items.findIndex(item => item?.get?.("id") === id);
36
+ }
37
+ /** Edits keep comments and key order that the user wrote. */
38
+ export function addConnectionYaml(text, entry, credential) {
39
+ const document = open(text);
40
+ if (credential) {
41
+ const credentials = sequence(document, "credentials");
42
+ if (indexOf(credentials, credential.id) < 0)
43
+ credentials.add(document.createNode({ id: credential.id, provider: credential.provider, secrets: credential.secrets }));
44
+ }
45
+ const connections = sequence(document, "connections");
46
+ if (indexOf(connections, entry.id) >= 0)
47
+ throw new ChannelError("CHANNEL_OPERATION_CONFLICT", false, { connectionId: entry.id });
48
+ connections.add(document.createNode(entry));
49
+ return document.toString();
50
+ }
51
+ export function updateConnectionYaml(text, id, patch) {
52
+ const document = open(text);
53
+ const connections = sequence(document, "connections");
54
+ const index = indexOf(connections, id);
55
+ if (index < 0)
56
+ throw new ChannelError("CHANNEL_NOT_FOUND", false, { connectionId: id });
57
+ const node = connections.items[index];
58
+ if (patch.displayName !== undefined) {
59
+ if (patch.displayName === null || patch.displayName === "")
60
+ node.delete("displayName");
61
+ else
62
+ node.set("displayName", patch.displayName);
63
+ }
64
+ if (patch.enabled !== undefined) {
65
+ if (patch.enabled)
66
+ node.delete("enabled");
67
+ else
68
+ node.set("enabled", false);
69
+ }
70
+ if (patch.config !== undefined) {
71
+ if (Object.keys(patch.config).length)
72
+ node.set("config", document.createNode(patch.config));
73
+ else
74
+ node.delete("config");
75
+ }
76
+ for (const [field, ref] of Object.entries(patch.secrets ?? {}))
77
+ node.setIn(["secrets", field], ref);
78
+ return document.toString();
79
+ }
80
+ /** Removes the entry and any credential that no remaining connection uses. Returns what was dropped. */
81
+ export function removeConnectionYaml(text, id) {
82
+ const document = open(text);
83
+ const connections = sequence(document, "connections");
84
+ const index = indexOf(connections, id);
85
+ if (index < 0)
86
+ throw new ChannelError("CHANNEL_NOT_FOUND", false, { connectionId: id });
87
+ const credentialId = connections.items[index].get("credential");
88
+ connections.items.splice(index, 1);
89
+ let removedCredential;
90
+ if (typeof credentialId === "string") {
91
+ const stillUsed = connections.items.some(item => item.get("credential") === credentialId);
92
+ const credentials = document.get("credentials", true);
93
+ if (!stillUsed && isSeq(credentials)) {
94
+ const at = indexOf(credentials, credentialId);
95
+ if (at >= 0) {
96
+ credentials.items.splice(at, 1);
97
+ removedCredential = credentialId;
98
+ }
99
+ }
100
+ }
101
+ return { text: document.toString(), ...(removedCredential ? { removedCredential } : {}) };
102
+ }
103
+ export function readConnectionsText(file) {
104
+ if (!existsSync(file))
105
+ return "";
106
+ const text = readFileSync(file, "utf8");
107
+ if (Buffer.byteLength(text) > CONNECTIONS_FILE_LIMIT_BYTES)
108
+ throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
109
+ return text;
110
+ }
111
+ /**
112
+ * Replaces the file atomically. A bind-mounted file cannot be renamed over, so that case falls
113
+ * back to writing in place.
114
+ */
115
+ export function writeConnectionsText(file, text) {
116
+ if (Buffer.byteLength(text) > CONNECTIONS_FILE_LIMIT_BYTES)
117
+ throw new ChannelError("CHANNEL_RESOURCE_LIMIT");
118
+ try {
119
+ mkdirSync(path.dirname(file), { recursive: true });
120
+ const temporary = `${file}.${process.pid}.tmp`;
121
+ try {
122
+ writeFileSync(temporary, text, { mode: 0o644 });
123
+ renameSync(temporary, file);
124
+ }
125
+ catch (error) {
126
+ const code = error.code;
127
+ try {
128
+ unlinkSync(temporary);
129
+ }
130
+ catch { /* nothing to clean */ }
131
+ if (code !== "EBUSY" && code !== "EXDEV" && code !== "EPERM")
132
+ throw error;
133
+ const descriptor = openSync(file, constants.O_WRONLY | constants.O_TRUNC);
134
+ try {
135
+ writeSync(descriptor, text);
136
+ fsyncSync(descriptor);
137
+ }
138
+ finally {
139
+ closeSync(descriptor);
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ if (error instanceof ChannelError)
145
+ throw error;
146
+ throw new ChannelError("CHANNEL_STORAGE_FAILED");
147
+ }
148
+ }
@@ -0,0 +1,34 @@
1
+ /** What the edit module needs from the secret store. `ChannelSecretStore` satisfies it. */
2
+ export interface SecretWriter {
3
+ get(name: string): string | undefined;
4
+ set(name: string, value: string): void;
5
+ delete(name: string): void;
6
+ deleteWithPrefix(prefix: string): void;
7
+ }
8
+ export type ConnectionEdit = {
9
+ /** Secret store name to value. Written before the file so a reference never points at nothing. */
10
+ secrets?: Readonly<Record<string, string>>;
11
+ /** Pure transform of the file text. May name secret prefixes to drop once the edit has committed. */
12
+ rewrite(text: string): {
13
+ text: string;
14
+ dropSecretPrefixes?: readonly string[];
15
+ };
16
+ };
17
+ export type ConnectionEditsOptions = {
18
+ file: () => string;
19
+ store: SecretWriter;
20
+ /** Throws if the new text would not load. Runs before anything is written to the file. */
21
+ validate: (text: string) => void;
22
+ };
23
+ /**
24
+ * One atomic change to connections: secrets and file together. Either both change or neither
25
+ * does. A secret that already existed is restored to its previous value, not deleted, and a
26
+ * failure while writing, validating or saving leaves nothing half-applied. Dropping the secrets
27
+ * of a removed connection happens only after the file has been saved, and a failure there only
28
+ * leaves an orphaned secret behind, never a broken connection.
29
+ */
30
+ export declare class ConnectionEdits {
31
+ private readonly options;
32
+ constructor(options: ConnectionEditsOptions);
33
+ apply(edit: ConnectionEdit): void;
34
+ }