stageflow 0.27.0 → 0.30.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 (261) hide show
  1. package/README.md +4 -0
  2. package/builtin-skills/browser/SKILL.md +42 -0
  3. package/dist/agent/claudeAdapter.js +2 -2
  4. package/dist/agent/cursorProvider.d.ts +2 -0
  5. package/dist/agent/cursorProvider.js +30 -5
  6. package/dist/agent/fakeAgent.d.ts +1 -0
  7. package/dist/agent/fakeAgent.js +13 -0
  8. package/dist/agent/piAdapter.d.ts +3 -24
  9. package/dist/agent/piAdapter.js +49 -47
  10. package/dist/agent/piSessionFactory.d.ts +38 -0
  11. package/dist/agent/piSessionFactory.js +60 -0
  12. package/dist/agent/port.d.ts +2 -0
  13. package/dist/agent/streamLogRedact.d.ts +6 -0
  14. package/dist/agent/streamLogRedact.js +23 -1
  15. package/dist/browser/anchor.d.ts +27 -0
  16. package/dist/browser/anchor.js +127 -0
  17. package/dist/browser/auditSink.d.ts +38 -0
  18. package/dist/browser/auditSink.js +34 -0
  19. package/dist/browser/browserHost.d.ts +86 -0
  20. package/dist/browser/browserHost.js +7 -0
  21. package/dist/browser/browserSweep.d.ts +14 -0
  22. package/dist/browser/browserSweep.js +54 -0
  23. package/dist/browser/browserTeardown.d.ts +56 -0
  24. package/dist/browser/browserTeardown.js +235 -0
  25. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  26. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  27. package/dist/browser/gateHandoff.d.ts +25 -0
  28. package/dist/browser/gateHandoff.js +83 -0
  29. package/dist/browser/humanLogin.d.ts +18 -0
  30. package/dist/browser/humanLogin.js +34 -0
  31. package/dist/browser/keyProvider.d.ts +10 -0
  32. package/dist/browser/keyProvider.js +39 -0
  33. package/dist/browser/localBrowserHost.d.ts +10 -0
  34. package/dist/browser/localBrowserHost.js +152 -0
  35. package/dist/browser/localProfileLock.d.ts +2 -0
  36. package/dist/browser/localProfileLock.js +218 -0
  37. package/dist/browser/localProfileStore.d.ts +5 -0
  38. package/dist/browser/localProfileStore.js +91 -0
  39. package/dist/browser/loginCheck.d.ts +53 -0
  40. package/dist/browser/loginCheck.js +147 -0
  41. package/dist/browser/memoryProfileLock.d.ts +2 -0
  42. package/dist/browser/memoryProfileLock.js +53 -0
  43. package/dist/browser/memoryProfileStore.d.ts +2 -0
  44. package/dist/browser/memoryProfileStore.js +31 -0
  45. package/dist/browser/navigationAudit.d.ts +16 -0
  46. package/dist/browser/navigationAudit.js +53 -0
  47. package/dist/browser/persistedEnv.d.ts +3 -0
  48. package/dist/browser/persistedEnv.js +15 -0
  49. package/dist/browser/profileLock.d.ts +40 -0
  50. package/dist/browser/profileLock.js +10 -0
  51. package/dist/browser/profileStore.d.ts +23 -0
  52. package/dist/browser/profileStore.js +39 -0
  53. package/dist/browser/runLiveness.d.ts +13 -0
  54. package/dist/browser/runLiveness.js +39 -0
  55. package/dist/browser/sitePolicy.d.ts +10 -0
  56. package/dist/browser/sitePolicy.js +61 -0
  57. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  58. package/dist/browser/stageBrowserEnv.js +147 -0
  59. package/dist/browser/stageProfileLock.d.ts +17 -0
  60. package/dist/browser/stageProfileLock.js +39 -0
  61. package/dist/cli/browserCommand.d.ts +44 -0
  62. package/dist/cli/browserCommand.js +472 -0
  63. package/dist/cli/hostClient.d.ts +20 -0
  64. package/dist/cli/hostClient.js +36 -2
  65. package/dist/cli/initCommand.d.ts +1 -0
  66. package/dist/cli/initCommand.js +23 -7
  67. package/dist/cli/initTemplates.d.ts +5 -2
  68. package/dist/cli/initTemplates.js +25 -2
  69. package/dist/cli/runsCommand.js +6 -2
  70. package/dist/cli/triggerCommand.d.ts +12 -0
  71. package/dist/cli/triggerCommand.js +183 -0
  72. package/dist/cli/validateCommand.d.ts +1 -1
  73. package/dist/cli/validateCommand.js +23 -6
  74. package/dist/cli.js +31 -2
  75. package/dist/config/browseCatalog.d.ts +1 -0
  76. package/dist/config/browseCatalog.js +5 -4
  77. package/dist/config/builtinSkills.d.ts +4 -0
  78. package/dist/config/builtinSkills.js +23 -0
  79. package/dist/config/catalogRelativePath.d.ts +1 -0
  80. package/dist/config/catalogRelativePath.js +10 -2
  81. package/dist/config/createTrigger.d.ts +27 -0
  82. package/dist/config/createTrigger.js +232 -0
  83. package/dist/config/draftPackage.d.ts +117 -0
  84. package/dist/config/draftPackage.js +550 -0
  85. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  86. package/dist/config/ensureCatalogScanRoot.js +73 -0
  87. package/dist/config/hostConfig.d.ts +2 -0
  88. package/dist/config/hostConfig.js +27 -0
  89. package/dist/config/listSkills.d.ts +2 -2
  90. package/dist/config/listSkills.js +17 -3
  91. package/dist/config/loadOutcome.d.ts +6 -0
  92. package/dist/config/loadPipeline.js +16 -0
  93. package/dist/config/loadStage.js +7 -0
  94. package/dist/config/loadStageflowManifest.js +25 -0
  95. package/dist/config/loadTrigger.d.ts +9 -0
  96. package/dist/config/loadTrigger.js +120 -0
  97. package/dist/config/multiProjectCatalog.d.ts +2 -0
  98. package/dist/config/multiProjectCatalog.js +26 -18
  99. package/dist/config/normalizePipelineStageEntry.js +8 -1
  100. package/dist/config/parseCompletionContract.js +6 -0
  101. package/dist/config/pipelineStageKeys.js +1 -0
  102. package/dist/config/publishDraftPackage.d.ts +7 -0
  103. package/dist/config/publishDraftPackage.js +25 -0
  104. package/dist/config/resolveCatalogContext.js +1 -1
  105. package/dist/config/resolvePipelineDag.js +1 -1
  106. package/dist/config/scanCatalogPaths.d.ts +1 -1
  107. package/dist/config/scanCatalogPaths.js +6 -2
  108. package/dist/config/stageBrowser.d.ts +12 -0
  109. package/dist/config/stageBrowser.js +118 -0
  110. package/dist/config/triggerCatalog.d.ts +10 -0
  111. package/dist/config/triggerCatalog.js +37 -0
  112. package/dist/config/validateCatalog.d.ts +13 -3
  113. package/dist/config/validateCatalog.js +83 -2
  114. package/dist/config/validateCompletionContract.js +8 -0
  115. package/dist/envelope/preEmitChecks.d.ts +1 -1
  116. package/dist/envelope/preEmitChecks.js +8 -0
  117. package/dist/logging/redact.d.ts +4 -0
  118. package/dist/logging/redact.js +17 -0
  119. package/dist/mcp/catalogTools.js +43 -8
  120. package/dist/mcp/controlTools.js +3 -1
  121. package/dist/mcp/deps.d.ts +2 -0
  122. package/dist/mcp/server.js +1 -0
  123. package/dist/mcp/tools.js +2 -0
  124. package/dist/mcp/triggerTools.d.ts +3 -0
  125. package/dist/mcp/triggerTools.js +176 -0
  126. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  127. package/dist/operatorAgent/docsRetrieval.js +269 -0
  128. package/dist/operatorAgent/draftContext.d.ts +16 -0
  129. package/dist/operatorAgent/draftContext.js +68 -0
  130. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  131. package/dist/operatorAgent/fakeHost.js +92 -0
  132. package/dist/operatorAgent/host.d.ts +40 -0
  133. package/dist/operatorAgent/host.js +179 -0
  134. package/dist/operatorAgent/index.d.ts +9 -0
  135. package/dist/operatorAgent/index.js +8 -0
  136. package/dist/operatorAgent/piModel.d.ts +80 -0
  137. package/dist/operatorAgent/piModel.js +528 -0
  138. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  139. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  140. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  141. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  142. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  143. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  144. package/dist/operatorAgent/proposals.d.ts +32 -0
  145. package/dist/operatorAgent/proposals.js +144 -0
  146. package/dist/operatorAgent/types.d.ts +128 -0
  147. package/dist/operatorAgent/types.js +1 -0
  148. package/dist/package-meta.d.ts +1 -1
  149. package/dist/package-meta.js +1 -1
  150. package/dist/project/findManifestRoot.d.ts +3 -0
  151. package/dist/project/findManifestRoot.js +35 -0
  152. package/dist/project/resolveStageflowContext.d.ts +7 -0
  153. package/dist/project/resolveStageflowContext.js +44 -28
  154. package/dist/runstore/assertTmpdir.js +6 -2
  155. package/dist/runstore/port.d.ts +28 -0
  156. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  157. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  158. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  159. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  160. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  161. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  162. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  163. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  164. package/dist/runstore/sqlite/migrations/index.js +6 -0
  165. package/dist/runtime/answerResume.d.ts +1 -0
  166. package/dist/runtime/answerResume.js +3 -1
  167. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  168. package/dist/runtime/completionCheckRunner.js +22 -0
  169. package/dist/runtime/credentialBinding.d.ts +2 -0
  170. package/dist/runtime/credentialBinding.js +3 -0
  171. package/dist/runtime/emailSource.d.ts +72 -0
  172. package/dist/runtime/emailSource.js +304 -0
  173. package/dist/runtime/githubPollSource.d.ts +86 -0
  174. package/dist/runtime/githubPollSource.js +284 -0
  175. package/dist/runtime/pipelineRunner.d.ts +4 -0
  176. package/dist/runtime/pipelineRunner.js +3 -0
  177. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  178. package/dist/runtime/pipelineScheduler.js +85 -1
  179. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  180. package/dist/runtime/runManager.d.ts +11 -0
  181. package/dist/runtime/runManager.js +149 -1
  182. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  183. package/dist/runtime/runRetryCoordinator.js +1 -0
  184. package/dist/runtime/scheduleSource.d.ts +38 -0
  185. package/dist/runtime/scheduleSource.js +119 -0
  186. package/dist/runtime/settingsFile.d.ts +5 -0
  187. package/dist/runtime/settingsFile.js +61 -54
  188. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  189. package/dist/runtime/stageEnvironment.js +1 -0
  190. package/dist/runtime/stageHitl.d.ts +1 -0
  191. package/dist/runtime/stageHitl.js +5 -1
  192. package/dist/runtime/stageProcessLauncher.d.ts +2 -0
  193. package/dist/runtime/stageProcessLauncher.js +44 -5
  194. package/dist/runtime/stageRunner.d.ts +5 -0
  195. package/dist/runtime/stageRunner.js +30 -6
  196. package/dist/runtime/stageStreamLog.d.ts +1 -0
  197. package/dist/runtime/stageStreamLog.js +2 -1
  198. package/dist/runtime/triggerCatalog.d.ts +6 -0
  199. package/dist/runtime/triggerCatalog.js +24 -0
  200. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  201. package/dist/runtime/triggerEventMatch.js +6 -0
  202. package/dist/runtime/triggerPort.d.ts +14 -0
  203. package/dist/runtime/triggerPort.js +1 -0
  204. package/dist/runtime/triggerRunner.d.ts +17 -0
  205. package/dist/runtime/triggerRunner.js +89 -0
  206. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  207. package/dist/runtime/verifiedStageExecution.js +14 -0
  208. package/dist/runtime/webhookSignature.d.ts +1 -0
  209. package/dist/runtime/webhookSignature.js +12 -0
  210. package/dist/server/bootstrap.d.ts +6 -0
  211. package/dist/server/bootstrap.js +64 -0
  212. package/dist/server/controlToken.js +13 -0
  213. package/dist/server/createHttpHost.js +3 -0
  214. package/dist/server/healthSurfaces.js +31 -6
  215. package/dist/server/http.d.ts +15 -0
  216. package/dist/server/http.js +1251 -74
  217. package/dist/server/requestAuthContext.d.ts +1 -1
  218. package/dist/server/triggerRoutes.d.ts +15 -0
  219. package/dist/server/triggerRoutes.js +268 -0
  220. package/dist/tools/askOperator.d.ts +3 -2
  221. package/dist/types/completion.d.ts +3 -0
  222. package/dist/types/pipeline.d.ts +3 -1
  223. package/dist/types/preEmitCheck.d.ts +7 -0
  224. package/dist/types/stage.d.ts +14 -0
  225. package/dist/types/stageflowManifest.d.ts +3 -0
  226. package/dist/types/trigger.d.ts +18 -0
  227. package/dist/types/trigger.js +1 -0
  228. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  229. package/dist/ui/assets/index-CntB4bZR.js +133 -0
  230. package/dist/ui/index.html +2 -2
  231. package/dist/workshop/autosave.d.ts +58 -0
  232. package/dist/workshop/autosave.js +222 -0
  233. package/dist/workshop/buildStore.d.ts +79 -0
  234. package/dist/workshop/buildStore.js +326 -0
  235. package/dist/workshop/chatTurn.d.ts +232 -0
  236. package/dist/workshop/chatTurn.js +475 -0
  237. package/dist/workshop/modelSettings.d.ts +18 -0
  238. package/dist/workshop/modelSettings.js +44 -0
  239. package/dist/workshop/sessionStore.d.ts +61 -0
  240. package/dist/workshop/sessionStore.js +254 -0
  241. package/dist/workshop/workshopPin.d.ts +72 -0
  242. package/dist/workshop/workshopPin.js +193 -0
  243. package/examples/README.md +2 -1
  244. package/examples/browser-session/README.md +77 -0
  245. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  246. package/examples/browser-session/browser-session.task.yaml +6 -0
  247. package/examples/browser-session/check-login.yaml +22 -0
  248. package/examples/browser-session/fixture-server.mjs +64 -0
  249. package/examples/browser-session/human-login.yaml +31 -0
  250. package/examples/browser-session/merge.yaml +36 -0
  251. package/examples/browser-session/work-a.yaml +41 -0
  252. package/examples/browser-session/work-b.yaml +41 -0
  253. package/examples/stage-mcp/README.md +3 -2
  254. package/examples/stageflow.yaml +51 -0
  255. package/package.json +5 -1
  256. package/skills/stageflow/references/control-surface.md +3 -1
  257. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  258. package/skills/stageflow-run/SKILL.md +2 -0
  259. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  260. package/skills/stageflow-setup/SKILL.md +11 -0
  261. package/dist/ui/assets/index-B6hlSXXn.js +0 -120
@@ -1,4 +1,7 @@
1
1
  import { bindingKindForOrigin, catalogOrSeededOrigin, decideWorkspaceConfigTrust, } from "../config/configOrigin.js";
2
+ import { loginCheckPromptBlock, readStageLoginCheck, } from "../browser/loginCheck.js";
3
+ import { humanLoginPromptBlock, isHumanLoginStage } from "../browser/humanLogin.js";
4
+ import { BROWSER_SKILL_NAME, resolveBuiltinSkillFile, } from "../config/builtinSkills.js";
2
5
  import { resolveSkillByName, } from "../config/listSkills.js";
3
6
  import { loadHostConfig } from "../config/hostConfig.js";
4
7
  import { defaultSeededRoots } from "../config/seededCatalog.js";
@@ -7,6 +10,7 @@ import { mkdir } from "node:fs/promises";
7
10
  import { runSkillsDir } from "./runSkills.js";
8
11
  import { MCP_CATALOG_FILENAME, STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV, StageMcpError, mcpCatalogExists, mcpCatalogPath, resolveStageMcpServers, stampStagePromptArtifactsDir, } from "../config/resolveStageMcpServers.js";
9
12
  import { attemptArtifactsDir, attemptStreamLogPath } from "../runstore/workspaceLayout.js";
13
+ import { redactBrowserActivityEvent, redactBrowserSecrets, } from "../agent/streamLogRedact.js";
10
14
  import { createStageStreamLogWriter, } from "./stageStreamLog.js";
11
15
  import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
12
16
  import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
@@ -68,6 +72,17 @@ async function resolveStageSkillForRun(stage, catalog, options) {
68
72
  skillOrigin: resolved.origin,
69
73
  };
70
74
  }
75
+ /** Browser stages get the `browser` skill (run > checkout > host > built-in). A missing skill is not an error. */
76
+ async function resolveBrowserSkillFile(stage, catalog, options) {
77
+ if (stage.browser === undefined || stage.skill === BROWSER_SKILL_NAME) {
78
+ return undefined;
79
+ }
80
+ const resolved = await resolveStageSkillForRun({ skill: BROWSER_SKILL_NAME }, catalog, options);
81
+ if (resolved.ok && resolved.skillFilePath !== undefined) {
82
+ return resolved.skillFilePath;
83
+ }
84
+ return resolveBuiltinSkillFile(BROWSER_SKILL_NAME);
85
+ }
71
86
  async function resolveAttemptMcpServers(allowlist, factoryCwd, artifactsDir, options) {
72
87
  const names = allowlist ?? [];
73
88
  if (names.length === 0)
@@ -151,6 +166,13 @@ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, a
151
166
  });
152
167
  if (!skill.ok)
153
168
  return skill;
169
+ const browserSkillFilePath = await resolveBrowserSkillFile(input.stage, catalog, {
170
+ bindingKind: resolveOptions.bindingKind,
171
+ checkoutRoot: resolveOptions.checkoutRoot,
172
+ factoryCwd,
173
+ trustWorkspaceConfig: resolveOptions.trustWorkspaceConfig,
174
+ workspaceDir: resolveOptions.workspaceDir,
175
+ });
154
176
  let resolvedMcpServers;
155
177
  const origins = [];
156
178
  try {
@@ -207,16 +229,41 @@ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, a
207
229
  })).catch(() => null);
208
230
  }
209
231
  }
232
+ let loginCheck;
233
+ const humanLogin = isHumanLoginStage(resolveOptions.dag, input.stage.id, input.stage.browser);
234
+ if (input.stage.browser?.check !== undefined && !humanLogin) {
235
+ loginCheck = await readStageLoginCheck(resolveOptions.workspaceDir, stageIdForManifest);
236
+ if (loginCheck === undefined) {
237
+ return { ok: false, reason: "browser login check result is missing" };
238
+ }
239
+ }
210
240
  try {
211
241
  const handle = agent.openStage({
212
242
  ...input,
213
243
  stage: {
214
244
  ...input.stage,
215
- system_prompt: stampStagePromptArtifactsDir(input.stage.system_prompt, artifactsDir),
245
+ system_prompt: `${stampStagePromptArtifactsDir(input.stage.system_prompt, artifactsDir)}${loginCheck !== undefined ? `\n\n${loginCheckPromptBlock(loginCheck)}` : ""}${humanLogin ? `\n\n${humanLoginPromptBlock(input.stage.browser?.login_url ?? input.stage.browser?.check?.url)}` : ""}`,
246
+ ...(loginCheck !== undefined
247
+ ? {
248
+ pre_emit_checks: [
249
+ ...(input.stage.pre_emit_checks ?? []),
250
+ {
251
+ id: "browser_login_check",
252
+ type: "browser_login_check",
253
+ state: loginCheck.state,
254
+ logged_in: loginCheck.logged_in,
255
+ url: loginCheck.url,
256
+ },
257
+ ],
258
+ }
259
+ : {}),
216
260
  },
217
261
  ...(skill.skillFilePath !== undefined
218
262
  ? { skillFilePath: skill.skillFilePath }
219
263
  : {}),
264
+ ...(browserSkillFilePath !== undefined
265
+ ? { browserSkillFilePath }
266
+ : {}),
220
267
  ...(resolvedMcpServers !== undefined ? { resolvedMcpServers } : {}),
221
268
  onResolvedModel: async (info) => {
222
269
  await patchRunManifest(resolveOptions.store, resolveOptions.runId, (m) => withStageResolvedModel(m, info.stageId, {
@@ -357,7 +404,10 @@ export async function openStageAttempt(input) {
357
404
  return priorInput;
358
405
  const roots = resolveAttemptRoots(input, stageId);
359
406
  const attempt = input.attemptCtx?.attempt ?? 1;
360
- const streamWriter = (input.streamLogWriterFactory ?? createStageStreamLogWriter)(attemptStreamLogPath(input.workspaceDir, stageId, attempt));
407
+ const streamLogPath = attemptStreamLogPath(input.workspaceDir, stageId, attempt);
408
+ const streamWriter = input.streamLogWriterFactory
409
+ ? input.streamLogWriterFactory(streamLogPath)
410
+ : createStageStreamLogWriter(streamLogPath, input.stage.browser !== undefined ? { redact: redactBrowserSecrets } : {});
361
411
  const resumeToken = input.resumeToken ??
362
412
  resumeSessionFilePath(input.workspaceDir, stageId, attempt);
363
413
  const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
@@ -394,7 +444,9 @@ export async function openStageAttempt(input) {
394
444
  resumeToken,
395
445
  ...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
396
446
  onActivity: (event) => {
397
- input.onActivity?.(event);
447
+ input.onActivity?.(input.stage.browser !== undefined
448
+ ? redactBrowserActivityEvent(event)
449
+ : event);
398
450
  void streamWriter.flush();
399
451
  },
400
452
  onAssistantTextDelta: streamWriter.onDelta,
@@ -25,6 +25,7 @@ export const STAGE_ENV_ALLOWLIST = [
25
25
  "https_proxy",
26
26
  "no_proxy",
27
27
  "STAGEFLOW_HOME",
28
+ "STAGEFLOW_CURSOR_EXTENSION",
28
29
  ];
29
30
  const PROVIDER_AND_CLOUD_NAMES = [
30
31
  "ANTHROPIC_API_KEY",
@@ -4,6 +4,7 @@ import { type StageAttemptContext } from "./stageAttemptContext.js";
4
4
  import { type AskOperatorPrompt } from "../tools/askOperator.js";
5
5
  export type DeliverAnswerResult = {
6
6
  ok: true;
7
+ verification?: "failed_retrying";
7
8
  } | {
8
9
  ok: false;
9
10
  reason: string;
@@ -1,3 +1,4 @@
1
+ import { parseHostGateContext } from "../browser/gateHandoff.js";
1
2
  import { appendOperatorAnswer, appendOperatorPrompt, derivePendingPrompt, } from "../hitl/qaTrail.js";
2
3
  import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
3
4
  import { attemptContext } from "./stageAttemptContext.js";
@@ -7,7 +8,10 @@ export function tryParsePendingPrompt(pendingRequest) {
7
8
  return undefined;
8
9
  }
9
10
  try {
10
- return normalizePromptIds(parseAskOperatorParams(pendingRequest));
11
+ return {
12
+ ...normalizePromptIds(parseAskOperatorParams(pendingRequest)),
13
+ ...parseHostGateContext(pendingRequest),
14
+ };
11
15
  }
12
16
  catch {
13
17
  return undefined;
@@ -17,6 +17,7 @@ export type StageLaunchInput = {
17
17
  bindingKind?: DerivedBindingKind;
18
18
  grants?: ResolvedStageGrants;
19
19
  attemptHome?: string;
20
+ browserEnv?: Record<string, string>;
20
21
  };
21
22
  export type StageLaunchResult = {
22
23
  type: "succeeded";
@@ -45,6 +46,7 @@ export declare class StageProcessLauncher {
45
46
  private readonly env;
46
47
  private readonly explicitChildExtras;
47
48
  private readonly cliEntry;
49
+ private readonly workerExecArgv;
48
50
  private readonly logger;
49
51
  private readonly forkFn;
50
52
  private readonly active;
@@ -1,7 +1,9 @@
1
1
  import * as childProcess from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { logger as rootLogger, resolveLogMaxLineBytes, } from "../logging/logger.js";
4
+ import { BROWSER_ENV_PREFIX } from "../browser/browserHost.js";
4
5
  import { PACKAGE_VERSION } from "../package-meta.js";
6
+ import { PI_HOME_AUTH_PATH_ENV, piHomeAuthPath } from "./credentialBinding.js";
5
7
  import { redactString } from "../logging/redact.js";
6
8
  import { getNamedSecrets } from "../logging/namedSecrets.js";
7
9
  import { readMaxActiveStageProcesses, } from "./stageConcurrency.js";
@@ -10,6 +12,28 @@ import { buildStageEnvironment, isAmbientBlockedEnv, isForeverDeniedSecret, } fr
10
12
  import { ensureStageCacheDirs } from "./stageCacheEnv.js";
11
13
  import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
12
14
  import { overlayStageBindingEnv, } from "./stageRoots.js";
15
+ function defaultStageCliEntry() {
16
+ const source = import.meta.url.split("?")[0]?.endsWith(".ts") === true;
17
+ return fileURLToPath(new URL(source ? "../cli.ts" : "../cli.js", import.meta.url));
18
+ }
19
+ function isTypeScriptEntry(entry) {
20
+ return /\.(?:m|c)?ts$/.test(entry);
21
+ }
22
+ /**
23
+ * A compiled `dist/cli.js` child is plain node. `npm run dev` is TypeScript,
24
+ * and this launcher sets execArgv explicitly so the parent's tsx loader is
25
+ * not inherited. Load that entry with the same tsx hooks the dev CLI uses.
26
+ */
27
+ function tsxLoaderExecArgv() {
28
+ try {
29
+ const preflight = fileURLToPath(import.meta.resolve("tsx/preflight"));
30
+ const loader = import.meta.resolve("tsx");
31
+ return ["--require", preflight, "--import", loader];
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
13
37
  function flushCappedPartial(log, event, text, maxLineBytes) {
14
38
  let remaining = redactString(text, { namedSecrets: getNamedSecrets() });
15
39
  while (Buffer.byteLength(remaining, "utf8") > maxLineBytes) {
@@ -109,6 +133,7 @@ export class StageProcessLauncher {
109
133
  env;
110
134
  explicitChildExtras;
111
135
  cliEntry;
136
+ workerExecArgv;
112
137
  logger;
113
138
  forkFn;
114
139
  active = new Map();
@@ -126,9 +151,11 @@ export class StageProcessLauncher {
126
151
  : undefined;
127
152
  this.maxActive = readMaxActiveStageProcesses(this.env, options.maxActiveStageProcesses);
128
153
  this.heapMb = getContainerLimits().maxOldSpaceSizeMb;
129
- this.cliEntry =
130
- options.cliEntry ??
131
- fileURLToPath(new URL("../cli.js", import.meta.url));
154
+ this.cliEntry = options.cliEntry ?? defaultStageCliEntry();
155
+ this.workerExecArgv = [
156
+ `--max-old-space-size=${this.heapMb}`,
157
+ ...(isTypeScriptEntry(this.cliEntry) ? tsxLoaderExecArgv() : []),
158
+ ];
132
159
  this.logger =
133
160
  options.logger ?? rootLogger.child({ component: "runtime" });
134
161
  this.forkFn = options.forkFn ?? childProcess.fork;
@@ -290,6 +317,7 @@ export class StageProcessLauncher {
290
317
  cacheVars: ensureStageCacheDirs(),
291
318
  grants: input.grants,
292
319
  attemptHome: input.attemptHome,
320
+ runVars: input.browserEnv,
293
321
  packageVersion: PACKAGE_VERSION,
294
322
  });
295
323
  for (const warning of built.warnings) {
@@ -301,14 +329,25 @@ export class StageProcessLauncher {
301
329
  const withExtras = this.explicitChildExtras !== undefined
302
330
  ? { ...built.env, ...this.explicitChildExtras }
303
331
  : built.env;
304
- const childEnv = overlayStageBindingEnv(withExtras, input.env ?? {}, input.bindingKind ?? "unbound");
332
+ const overlaid = overlayStageBindingEnv(withExtras, input.env ?? {}, input.bindingKind ?? "unbound");
333
+ const withBrowser = input.browserEnv !== undefined
334
+ ? {
335
+ ...Object.fromEntries(Object.entries(overlaid).filter(([name]) => !name.startsWith(BROWSER_ENV_PREFIX))),
336
+ ...input.browserEnv,
337
+ }
338
+ : overlaid;
339
+ const childEnv = {
340
+ ...withBrowser,
341
+ [PI_HOME_AUTH_PATH_ENV]: piHomeAuthPath(),
342
+ };
305
343
  const child = this.forkFn(this.cliEntry, args, {
306
344
  cwd: input.rootDir,
307
345
  env: { ...childEnv, [SF_STAGE_WORKER]: "1" },
308
346
  stdio: ["pipe", "pipe", "pipe", "ipc"],
309
347
  detached: true,
310
348
  // Explicit: do not inherit Host execArgv; set heap from cgroup budget.
311
- execArgv: [`--max-old-space-size=${this.heapMb}`],
349
+ // TypeScript entries also get the tsx loader, since that inheritance is off.
350
+ execArgv: this.workerExecArgv,
312
351
  });
313
352
  const key = activeKey(input.runId, input.stageId);
314
353
  const tracked = {
@@ -1,4 +1,6 @@
1
1
  import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunResult, StageSessionMode } from "../agent/port.js";
2
+ import { type HostGateContext } from "../browser/gateHandoff.js";
3
+ import type { StageBrowserSupport } from "../browser/browserHost.js";
2
4
  import type { StageEnvelope } from "../types/envelope.js";
3
5
  import type { LoadedStageConfig } from "../types/stage.js";
4
6
  import type { TaskFile } from "../types/task.js";
@@ -49,6 +51,8 @@ export type RunStageOptions = {
49
51
  feedbackLoopContext?: FeedbackLoopContext;
50
52
  resumeToken?: string;
51
53
  stageEnv?: Record<string, string>;
54
+ /** Test seam for the Host login check run by a `browser_login` verify item. */
55
+ browser?: Pick<StageBrowserSupport, "runner" | "loginCheck">;
52
56
  };
53
57
  /**
54
58
  * Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
@@ -62,5 +66,6 @@ export declare function runStageYieldLoop(options: {
62
66
  store?: RunStore;
63
67
  attemptCtx?: StageAttemptContext;
64
68
  skipGates?: boolean;
69
+ gateContext?: HostGateContext;
65
70
  }): Promise<RunStageYieldLoopResult>;
66
71
  export declare function runStage(options: RunStageOptions): Promise<RunStageOutcome>;
@@ -1,8 +1,11 @@
1
+ import { hostGateContextFor, stampGateRequest, } from "../browser/gateHandoff.js";
1
2
  import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
2
3
  import { buildStageRoots, withResolvedAuthPath, } from "./stageRoots.js";
3
4
  import { createDefaultHitlQaHooks, recordWaitEnter, } from "./stageHitl.js";
4
5
  import { openStageAttempt, } from "./stageAttemptBootstrap.js";
5
6
  import { createVerifiedStageExecution } from "./verifiedStageExecution.js";
7
+ import { BROWSER_STAGE_PATTERNS, redact } from "../logging/redact.js";
8
+ import { getNamedSecrets } from "../logging/namedSecrets.js";
6
9
  import { isStageTimeoutReason } from "../agent/stageTimeout.js";
7
10
  export function isRunStageWaiting(outcome) {
8
11
  return "waiting" in outcome && outcome.waiting === true;
@@ -44,11 +47,16 @@ async function appendLifecycleEventAndPatchExecution(options) {
44
47
  }
45
48
  }
46
49
  async function finalizeStageResult(options) {
47
- const { store, runId, stageId, result, activityChain, attemptCtx } = options;
50
+ const { store, runId, stageId, result, activityChain, attemptCtx, browser } = options;
48
51
  const attemptOpt = attemptCtx?.eventOptions();
49
52
  await activityChain;
50
53
  if (result.envelope) {
51
- await store.writeEnvelope(runId, stageId, result.envelope, attemptOpt);
54
+ await store.writeEnvelope(runId, stageId, browser
55
+ ? redact(result.envelope, {
56
+ patterns: BROWSER_STAGE_PATTERNS,
57
+ namedSecrets: getNamedSecrets(),
58
+ })
59
+ : result.envelope, attemptOpt);
52
60
  }
53
61
  if (!result.ok) {
54
62
  await appendLifecycleEventAndPatchExecution({
@@ -75,10 +83,11 @@ async function finalizeStageResult(options) {
75
83
  * Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
76
84
  */
77
85
  export async function runStageYieldLoop(options) {
78
- const { handle, runId, stageId, hitl, workerMode, store, attemptCtx, skipGates } = options;
86
+ const { handle, runId, stageId, hitl, workerMode, store, attemptCtx, skipGates, gateContext } = options;
79
87
  while (true) {
80
88
  const event = await handle.next();
81
89
  if (event.status === "waiting_for_input") {
90
+ const request = stampGateRequest(event.request, gateContext);
82
91
  if (skipGates) {
83
92
  return {
84
93
  ok: false,
@@ -96,7 +105,7 @@ export async function runStageYieldLoop(options) {
96
105
  store,
97
106
  runId,
98
107
  stageId,
99
- request: event.request,
108
+ request,
100
109
  attemptCtx,
101
110
  qaHooks: createDefaultHitlQaHooks(store),
102
111
  });
@@ -108,14 +117,14 @@ export async function runStageYieldLoop(options) {
108
117
  reason: "stage requested wait but no HITL controller is configured",
109
118
  };
110
119
  }
111
- await hitl.enterWait(runId, stageId, handle, event.request, attemptCtx);
120
+ await hitl.enterWait(runId, stageId, handle, request, attemptCtx);
112
121
  continue;
113
122
  }
114
123
  return event.result;
115
124
  }
116
125
  }
117
126
  export async function runStage(options) {
118
- const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, stageEnv, } = options;
127
+ const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, stageEnv, browser: browserSeams, } = options;
119
128
  const stageId = options.stageId ?? stage.id;
120
129
  const attemptOpt = attemptCtx?.eventOptions();
121
130
  const baseRoots = rootsOverride ??
@@ -144,6 +153,19 @@ export async function runStage(options) {
144
153
  dag,
145
154
  roots,
146
155
  commandEnv: stageEnv ?? process.env,
156
+ ...(stage.browser?.check !== undefined
157
+ ? {
158
+ browserLogin: {
159
+ check: stage.browser.check,
160
+ ...(browserSeams?.runner !== undefined
161
+ ? { runner: browserSeams.runner }
162
+ : {}),
163
+ ...(browserSeams?.loginCheck !== undefined
164
+ ? { options: browserSeams.loginCheck }
165
+ : {}),
166
+ },
167
+ }
168
+ : {}),
147
169
  });
148
170
  await verifiedExecution.prepare();
149
171
  console.error(`Running stage ${stageId} (${stage.model})...`);
@@ -209,6 +231,7 @@ export async function runStage(options) {
209
231
  store: workerMode ? store : undefined,
210
232
  attemptCtx,
211
233
  skipGates,
234
+ gateContext: hostGateContextFor(stage.browser),
212
235
  });
213
236
  }
214
237
  catch (err) {
@@ -253,6 +276,7 @@ export async function runStage(options) {
253
276
  result,
254
277
  activityChain,
255
278
  attemptCtx,
279
+ browser: stage.browser !== undefined,
256
280
  });
257
281
  }
258
282
  finally {
@@ -14,4 +14,5 @@ export type StageStreamLogWriter = {
14
14
  export declare function createStageStreamLogWriter(streamLogPath: string, options?: {
15
15
  flushThrottleMs?: number;
16
16
  capBytes?: number;
17
+ redact?: (text: string) => string;
17
18
  }): StageStreamLogWriter;
@@ -54,6 +54,7 @@ async function appendChunk(streamLogPath, chunk, capBytes) {
54
54
  export function createStageStreamLogWriter(streamLogPath, options = {}) {
55
55
  const flushThrottleMs = options.flushThrottleMs ?? STREAM_FLUSH_THROTTLE_MS;
56
56
  const capBytes = options.capBytes ?? DEFAULT_CAP_BYTES;
57
+ const scrub = options.redact ?? redactSecrets;
57
58
  let buffer = "";
58
59
  let timer = null;
59
60
  // Serializes disk writes so the throttle timer and an eager flush() never race.
@@ -65,7 +66,7 @@ export function createStageStreamLogWriter(streamLogPath, options = {}) {
65
66
  }
66
67
  if (!buffer)
67
68
  return writeChain;
68
- const chunk = redactSecrets(buffer);
69
+ const chunk = scrub(buffer);
69
70
  buffer = "";
70
71
  writeChain = writeChain.then(() => appendChunk(streamLogPath, chunk, capBytes));
71
72
  return writeChain;
@@ -0,0 +1,6 @@
1
+ import type { TriggerFile } from "../types/trigger.js";
2
+ export declare function findCatalogPathById(paths: string[], id: string): Promise<string | undefined>;
3
+ export declare function findTriggerDefinition(triggerPaths: string[], triggerId: string): Promise<{
4
+ path: string;
5
+ definition: TriggerFile;
6
+ } | undefined>;
@@ -0,0 +1,24 @@
1
+ import { readYamlObject } from "../config/readYamlObject.js";
2
+ import { loadTriggerOutcome } from "../config/loadTrigger.js";
3
+ export async function findCatalogPathById(paths, id) {
4
+ for (const filePath of paths) {
5
+ try {
6
+ const raw = await readYamlObject(filePath);
7
+ if (raw?.id === id)
8
+ return filePath;
9
+ }
10
+ catch {
11
+ // Unreadable/invalid catalog files are surfaced by `sf validate`; skip here.
12
+ }
13
+ }
14
+ return undefined;
15
+ }
16
+ export async function findTriggerDefinition(triggerPaths, triggerId) {
17
+ for (const filePath of triggerPaths) {
18
+ const outcome = await loadTriggerOutcome(filePath);
19
+ if (outcome.ok && outcome.value.id === triggerId) {
20
+ return { path: filePath, definition: outcome.value };
21
+ }
22
+ }
23
+ return undefined;
24
+ }
@@ -0,0 +1,2 @@
1
+ /** Subset-match: every key/value in `match` must equal-match `payload`. No `match` matches everything. */
2
+ export declare function matchesEventFilter(match: Record<string, unknown> | undefined, payload: Record<string, unknown>): boolean;
@@ -0,0 +1,6 @@
1
+ /** Subset-match: every key/value in `match` must equal-match `payload`. No `match` matches everything. */
2
+ export function matchesEventFilter(match, payload) {
3
+ if (!match)
4
+ return true;
5
+ return Object.entries(match).every(([key, value]) => payload[key] === value);
6
+ }
@@ -0,0 +1,14 @@
1
+ import type { TaskFile } from "../types/task.js";
2
+ export type TriggerFireEvent = {
3
+ triggerId: string;
4
+ task?: TaskFile;
5
+ };
6
+ /**
7
+ * Abstract seam for anything that decides "fire trigger X now" — a croner-backed
8
+ * schedule loop, a GitHub/email poller, a webhook receiver. No adapter implements
9
+ * this yet; it exists so those later adapters build against a locked contract.
10
+ */
11
+ export interface TriggerSourcePort {
12
+ start(onFire: (event: TriggerFireEvent) => Promise<void>): Promise<void>;
13
+ stop(): Promise<void>;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { RunManager, StartRunResult } from "./runManager.js";
3
+ import type { TaskFile } from "../types/task.js";
4
+ /**
5
+ * Resolves a trigger id to its `*.trigger.yaml` catalog definition, resolves its
6
+ * `pipeline`/`task` refs to catalog files, and starts a real run through
7
+ * `RunManager.startRun` — the same internal entry point the CLI/MCP/console use.
8
+ * On a successful start, records the fire on the trigger's store row.
9
+ *
10
+ * `task` presence on the trigger definition is the either/or mode switch: a
11
+ * catalog-mode trigger (`task` set) rejects a supplied `options.task`; a
12
+ * dynamic-mode trigger (`task` absent) requires one.
13
+ */
14
+ export declare function fireTrigger(triggerId: string, store: RunStore, runManager: RunManager, options?: {
15
+ cwd?: string;
16
+ task?: TaskFile;
17
+ }): Promise<StartRunResult>;
@@ -0,0 +1,89 @@
1
+ import path from "node:path";
2
+ import { catalogContextFromStageflow } from "../config/resolveCatalogContext.js";
3
+ import { getCatalogScanPaths } from "../config/browseCatalog.js";
4
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
5
+ import { findCatalogPathById, findTriggerDefinition } from "./triggerCatalog.js";
6
+ /**
7
+ * Resolves a trigger id to its `*.trigger.yaml` catalog definition, resolves its
8
+ * `pipeline`/`task` refs to catalog files, and starts a real run through
9
+ * `RunManager.startRun` — the same internal entry point the CLI/MCP/console use.
10
+ * On a successful start, records the fire on the trigger's store row.
11
+ *
12
+ * `task` presence on the trigger definition is the either/or mode switch: a
13
+ * catalog-mode trigger (`task` set) rejects a supplied `options.task`; a
14
+ * dynamic-mode trigger (`task` absent) requires one.
15
+ */
16
+ export async function fireTrigger(triggerId, store, runManager, options = {}) {
17
+ const cwd = options.cwd ?? process.cwd();
18
+ const ctx = catalogContextFromStageflow(await resolveStageflowContext(cwd));
19
+ const scanPaths = await getCatalogScanPaths(ctx);
20
+ if (!scanPaths) {
21
+ return { ok: false, reason: "No Stageflow catalog found", status: 404 };
22
+ }
23
+ const found = await findTriggerDefinition(scanPaths.triggerPaths, triggerId);
24
+ if (!found) {
25
+ return { ok: false, reason: `Trigger not found: ${triggerId}`, status: 404 };
26
+ }
27
+ const { path: triggerPath, definition: trigger } = found;
28
+ if (!trigger.enabled) {
29
+ return { ok: false, reason: `Trigger is disabled: ${triggerId}`, status: 409 };
30
+ }
31
+ const suppliedTask = options.task;
32
+ if (trigger.task !== undefined && suppliedTask !== undefined) {
33
+ return {
34
+ ok: false,
35
+ reason: `Trigger "${triggerId}" already has a catalog task "${trigger.task}"; task override is not allowed`,
36
+ status: 422,
37
+ code: "trigger.task_override_not_allowed",
38
+ };
39
+ }
40
+ if (trigger.task === undefined && suppliedTask === undefined) {
41
+ return {
42
+ ok: false,
43
+ reason: `Trigger "${triggerId}" has no catalog task; a task must be supplied when firing`,
44
+ status: 422,
45
+ code: "trigger.task_required",
46
+ };
47
+ }
48
+ const pipelinePath = await findCatalogPathById(scanPaths.pipelinePaths, trigger.pipeline);
49
+ if (!pipelinePath) {
50
+ return {
51
+ ok: false,
52
+ reason: `Trigger "${triggerId}" references unknown pipeline "${trigger.pipeline}"`,
53
+ status: 400,
54
+ };
55
+ }
56
+ let taskInput;
57
+ if (trigger.task !== undefined) {
58
+ const taskPath = await findCatalogPathById(scanPaths.taskPaths, trigger.task);
59
+ if (!taskPath) {
60
+ return {
61
+ ok: false,
62
+ reason: `Trigger "${triggerId}" references unknown task "${trigger.task}"`,
63
+ status: 400,
64
+ };
65
+ }
66
+ taskInput = taskPath;
67
+ }
68
+ else {
69
+ taskInput = suppliedTask;
70
+ }
71
+ const projectRoot = ctx.projectRoot ?? undefined;
72
+ const definitionRef = projectRoot !== undefined
73
+ ? path.relative(projectRoot, triggerPath).replace(/\\/g, "/")
74
+ : triggerPath;
75
+ await store.upsertTrigger({
76
+ id: triggerId,
77
+ definitionRef,
78
+ enabled: trigger.enabled,
79
+ });
80
+ const started = await runManager.startRun({
81
+ pipeline: pipelinePath,
82
+ task: taskInput,
83
+ ...(projectRoot !== undefined ? { projectRoot } : {}),
84
+ });
85
+ if (started.ok) {
86
+ await store.recordTriggerFired(triggerId, started.runId);
87
+ }
88
+ return started;
89
+ }
@@ -2,6 +2,8 @@ import type { StageRunResult } from "../agent/port.js";
2
2
  import type { RunStore } from "../runstore/port.js";
3
3
  import type { ResolvedPipelineDag } from "../types/pipeline.js";
4
4
  import type { StageConfig } from "../types/stage.js";
5
+ import type { BrowserRunner } from "../browser/browserHost.js";
6
+ import { type LoginCheckConfig, type LoginCheckOptions } from "../browser/loginCheck.js";
5
7
  import type { StageRoots } from "./stageRoots.js";
6
8
  /** Sidecar written before completion command checks so they can inspect fork_choice. */
7
9
  export declare const COMPLETION_CANDIDATE_ENVELOPE_FILENAME = "completion-candidate-envelope.json";
@@ -29,4 +31,9 @@ export declare function createVerifiedStageExecution(options: {
29
31
  dag?: ResolvedPipelineDag;
30
32
  roots: StageRoots;
31
33
  commandEnv?: NodeJS.ProcessEnv;
34
+ browserLogin?: {
35
+ check: LoginCheckConfig;
36
+ runner?: BrowserRunner;
37
+ options?: LoginCheckOptions;
38
+ };
32
39
  }): VerifiedStageExecution;
@@ -9,6 +9,9 @@ import { COMPLETION_VERIFICATION_FAILURE_PREFIX, } from "../types/completion.js"
9
9
  import { runCompletionContract, } from "./completionCheckRunner.js";
10
10
  import { readCompletionCheckoutBaseline, writeCompletionCheckoutBaseline, } from "./completionCheckoutBaseline.js";
11
11
  import { createGitCheckoutCapability } from "./gitCheckoutCapability.js";
12
+ import { readStagePersistedBrowserEnv } from "../browser/stageBrowserEnv.js";
13
+ import { defaultBrowserRunner } from "../browser/browserTeardown.js";
14
+ import { runLoginCheck, } from "../browser/loginCheck.js";
12
15
  /** Sidecar written before completion command checks so they can inspect fork_choice. */
13
16
  export const COMPLETION_CANDIDATE_ENVELOPE_FILENAME = "completion-candidate-envelope.json";
14
17
  export function completionVerificationFailureReason(failedCheckIds) {
@@ -88,6 +91,17 @@ export function createVerifiedStageExecution(options) {
88
91
  checkoutBefore,
89
92
  checkoutError,
90
93
  checklistAttestations: candidate.envelope.checklist_attestations,
94
+ ...(options.browserLogin !== undefined
95
+ ? {
96
+ browserLogin: async () => {
97
+ const env = await readStagePersistedBrowserEnv(roots.runWorkspaceDir, stageId);
98
+ if (env === undefined) {
99
+ throw new Error("browser session env is missing for this stage");
100
+ }
101
+ return runLoginCheck(env, options.browserLogin.check, options.browserLogin.runner ?? defaultBrowserRunner, options.browserLogin.options);
102
+ },
103
+ }
104
+ : {}),
91
105
  onCheckStart: async (check) => {
92
106
  await store.upsertVerificationCheckResult(runId, stageId, {
93
107
  check_id: check.id,
@@ -0,0 +1 @@
1
+ export declare function verifyHmacSignature(rawBody: Buffer, headerValue: string, secret: string, scheme: "hex" | "base64"): boolean;
@@ -0,0 +1,12 @@
1
+ import crypto from "node:crypto";
2
+ export function verifyHmacSignature(rawBody, headerValue, secret, scheme) {
3
+ const eqIndex = headerValue.indexOf("=");
4
+ const hasPrefix = eqIndex !== -1 && eqIndex < headerValue.length - 1;
5
+ const provided = hasPrefix ? headerValue.slice(eqIndex + 1) : headerValue;
6
+ const computed = crypto.createHmac("sha256", secret).update(rawBody).digest(scheme);
7
+ const providedBuffer = Buffer.from(provided, scheme);
8
+ const computedBuffer = Buffer.from(computed, scheme);
9
+ if (providedBuffer.length !== computedBuffer.length)
10
+ return false;
11
+ return crypto.timingSafeEqual(providedBuffer, computedBuffer);
12
+ }