stageflow 0.27.0 → 0.31.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 (270) 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 +10 -0
  5. package/dist/agent/cursorProvider.js +83 -8
  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/providerAuth.d.ts +7 -3
  14. package/dist/agent/providerAuth.js +5 -3
  15. package/dist/agent/streamLogRedact.d.ts +6 -0
  16. package/dist/agent/streamLogRedact.js +23 -1
  17. package/dist/browser/anchor.d.ts +27 -0
  18. package/dist/browser/anchor.js +127 -0
  19. package/dist/browser/auditSink.d.ts +38 -0
  20. package/dist/browser/auditSink.js +34 -0
  21. package/dist/browser/browserHost.d.ts +86 -0
  22. package/dist/browser/browserHost.js +7 -0
  23. package/dist/browser/browserSweep.d.ts +14 -0
  24. package/dist/browser/browserSweep.js +54 -0
  25. package/dist/browser/browserTeardown.d.ts +56 -0
  26. package/dist/browser/browserTeardown.js +235 -0
  27. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
  28. package/dist/browser/fakeRemoteBrowserHost.js +34 -0
  29. package/dist/browser/gateHandoff.d.ts +25 -0
  30. package/dist/browser/gateHandoff.js +83 -0
  31. package/dist/browser/humanLogin.d.ts +18 -0
  32. package/dist/browser/humanLogin.js +34 -0
  33. package/dist/browser/keyProvider.d.ts +10 -0
  34. package/dist/browser/keyProvider.js +39 -0
  35. package/dist/browser/localBrowserHost.d.ts +10 -0
  36. package/dist/browser/localBrowserHost.js +152 -0
  37. package/dist/browser/localProfileLock.d.ts +2 -0
  38. package/dist/browser/localProfileLock.js +218 -0
  39. package/dist/browser/localProfileStore.d.ts +5 -0
  40. package/dist/browser/localProfileStore.js +91 -0
  41. package/dist/browser/loginCheck.d.ts +53 -0
  42. package/dist/browser/loginCheck.js +147 -0
  43. package/dist/browser/memoryProfileLock.d.ts +2 -0
  44. package/dist/browser/memoryProfileLock.js +53 -0
  45. package/dist/browser/memoryProfileStore.d.ts +2 -0
  46. package/dist/browser/memoryProfileStore.js +31 -0
  47. package/dist/browser/navigationAudit.d.ts +16 -0
  48. package/dist/browser/navigationAudit.js +53 -0
  49. package/dist/browser/persistedEnv.d.ts +3 -0
  50. package/dist/browser/persistedEnv.js +15 -0
  51. package/dist/browser/profileLock.d.ts +40 -0
  52. package/dist/browser/profileLock.js +10 -0
  53. package/dist/browser/profileStore.d.ts +23 -0
  54. package/dist/browser/profileStore.js +39 -0
  55. package/dist/browser/runLiveness.d.ts +13 -0
  56. package/dist/browser/runLiveness.js +39 -0
  57. package/dist/browser/sitePolicy.d.ts +10 -0
  58. package/dist/browser/sitePolicy.js +61 -0
  59. package/dist/browser/stageBrowserEnv.d.ts +22 -0
  60. package/dist/browser/stageBrowserEnv.js +147 -0
  61. package/dist/browser/stageProfileLock.d.ts +17 -0
  62. package/dist/browser/stageProfileLock.js +39 -0
  63. package/dist/cli/browserCommand.d.ts +44 -0
  64. package/dist/cli/browserCommand.js +472 -0
  65. package/dist/cli/doctorCommand.js +7 -0
  66. package/dist/cli/hostClient.d.ts +20 -0
  67. package/dist/cli/hostClient.js +36 -2
  68. package/dist/cli/initCommand.d.ts +1 -0
  69. package/dist/cli/initCommand.js +23 -7
  70. package/dist/cli/initTemplates.d.ts +5 -2
  71. package/dist/cli/initTemplates.js +25 -2
  72. package/dist/cli/providersCommand.d.ts +1 -1
  73. package/dist/cli/providersCommand.js +4 -4
  74. package/dist/cli/runsCommand.js +6 -2
  75. package/dist/cli/triggerCommand.d.ts +12 -0
  76. package/dist/cli/triggerCommand.js +183 -0
  77. package/dist/cli/validateCommand.d.ts +1 -1
  78. package/dist/cli/validateCommand.js +23 -6
  79. package/dist/cli.js +32 -3
  80. package/dist/config/browseCatalog.d.ts +1 -0
  81. package/dist/config/browseCatalog.js +5 -4
  82. package/dist/config/builtinSkills.d.ts +4 -0
  83. package/dist/config/builtinSkills.js +23 -0
  84. package/dist/config/catalogRelativePath.d.ts +1 -0
  85. package/dist/config/catalogRelativePath.js +10 -2
  86. package/dist/config/createTrigger.d.ts +27 -0
  87. package/dist/config/createTrigger.js +232 -0
  88. package/dist/config/draftPackage.d.ts +117 -0
  89. package/dist/config/draftPackage.js +550 -0
  90. package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
  91. package/dist/config/ensureCatalogScanRoot.js +73 -0
  92. package/dist/config/hostConfig.d.ts +2 -0
  93. package/dist/config/hostConfig.js +27 -0
  94. package/dist/config/listSkills.d.ts +2 -2
  95. package/dist/config/listSkills.js +17 -3
  96. package/dist/config/loadOutcome.d.ts +6 -0
  97. package/dist/config/loadPipeline.js +16 -0
  98. package/dist/config/loadStage.js +7 -0
  99. package/dist/config/loadStageflowManifest.js +25 -0
  100. package/dist/config/loadTrigger.d.ts +9 -0
  101. package/dist/config/loadTrigger.js +120 -0
  102. package/dist/config/multiProjectCatalog.d.ts +2 -0
  103. package/dist/config/multiProjectCatalog.js +26 -18
  104. package/dist/config/normalizePipelineStageEntry.js +8 -1
  105. package/dist/config/parseCompletionContract.js +6 -0
  106. package/dist/config/pipelineStageKeys.js +1 -0
  107. package/dist/config/publishDraftPackage.d.ts +7 -0
  108. package/dist/config/publishDraftPackage.js +25 -0
  109. package/dist/config/resolveCatalogContext.js +1 -1
  110. package/dist/config/resolvePipelineDag.js +1 -1
  111. package/dist/config/scanCatalogPaths.d.ts +1 -1
  112. package/dist/config/scanCatalogPaths.js +6 -2
  113. package/dist/config/stageBrowser.d.ts +12 -0
  114. package/dist/config/stageBrowser.js +118 -0
  115. package/dist/config/triggerCatalog.d.ts +10 -0
  116. package/dist/config/triggerCatalog.js +37 -0
  117. package/dist/config/validateCatalog.d.ts +13 -3
  118. package/dist/config/validateCatalog.js +83 -2
  119. package/dist/config/validateCompletionContract.js +8 -0
  120. package/dist/envelope/preEmitChecks.d.ts +1 -1
  121. package/dist/envelope/preEmitChecks.js +8 -0
  122. package/dist/logging/redact.d.ts +4 -0
  123. package/dist/logging/redact.js +17 -0
  124. package/dist/mcp/catalogTools.js +43 -8
  125. package/dist/mcp/controlTools.js +3 -1
  126. package/dist/mcp/deps.d.ts +2 -0
  127. package/dist/mcp/server.js +1 -0
  128. package/dist/mcp/tools.js +2 -0
  129. package/dist/mcp/triggerTools.d.ts +3 -0
  130. package/dist/mcp/triggerTools.js +176 -0
  131. package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
  132. package/dist/operatorAgent/docsRetrieval.js +269 -0
  133. package/dist/operatorAgent/draftContext.d.ts +16 -0
  134. package/dist/operatorAgent/draftContext.js +68 -0
  135. package/dist/operatorAgent/fakeHost.d.ts +39 -0
  136. package/dist/operatorAgent/fakeHost.js +92 -0
  137. package/dist/operatorAgent/host.d.ts +40 -0
  138. package/dist/operatorAgent/host.js +179 -0
  139. package/dist/operatorAgent/index.d.ts +9 -0
  140. package/dist/operatorAgent/index.js +8 -0
  141. package/dist/operatorAgent/piModel.d.ts +80 -0
  142. package/dist/operatorAgent/piModel.js +528 -0
  143. package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
  144. package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
  145. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
  146. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
  147. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
  148. package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
  149. package/dist/operatorAgent/proposals.d.ts +32 -0
  150. package/dist/operatorAgent/proposals.js +144 -0
  151. package/dist/operatorAgent/types.d.ts +128 -0
  152. package/dist/operatorAgent/types.js +1 -0
  153. package/dist/package-meta.d.ts +1 -1
  154. package/dist/package-meta.js +1 -1
  155. package/dist/project/findManifestRoot.d.ts +3 -0
  156. package/dist/project/findManifestRoot.js +35 -0
  157. package/dist/project/resolveStageflowContext.d.ts +7 -0
  158. package/dist/project/resolveStageflowContext.js +44 -28
  159. package/dist/runstore/assertTmpdir.js +6 -2
  160. package/dist/runstore/diskUsage.d.ts +1 -1
  161. package/dist/runstore/diskUsage.js +2 -7
  162. package/dist/runstore/port.d.ts +28 -0
  163. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -1
  164. package/dist/runstore/sqlite/SqliteRunStore.js +90 -0
  165. package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
  166. package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
  167. package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
  168. package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
  169. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
  170. package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
  171. package/dist/runstore/sqlite/migrations/index.js +6 -0
  172. package/dist/runtime/answerResume.d.ts +1 -0
  173. package/dist/runtime/answerResume.js +3 -1
  174. package/dist/runtime/completionCheckRunner.d.ts +12 -1
  175. package/dist/runtime/completionCheckRunner.js +22 -0
  176. package/dist/runtime/credentialBinding.d.ts +7 -2
  177. package/dist/runtime/credentialBinding.js +20 -28
  178. package/dist/runtime/emailSource.d.ts +72 -0
  179. package/dist/runtime/emailSource.js +304 -0
  180. package/dist/runtime/githubPollSource.d.ts +86 -0
  181. package/dist/runtime/githubPollSource.js +284 -0
  182. package/dist/runtime/pipelineRunner.d.ts +4 -0
  183. package/dist/runtime/pipelineRunner.js +3 -0
  184. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  185. package/dist/runtime/pipelineScheduler.js +86 -1
  186. package/dist/runtime/repositoryMaterialize.d.ts +1 -1
  187. package/dist/runtime/runManager.d.ts +11 -0
  188. package/dist/runtime/runManager.js +161 -3
  189. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  190. package/dist/runtime/runRetryCoordinator.js +1 -0
  191. package/dist/runtime/scheduleSource.d.ts +38 -0
  192. package/dist/runtime/scheduleSource.js +119 -0
  193. package/dist/runtime/settingsFile.d.ts +7 -2
  194. package/dist/runtime/settingsFile.js +66 -56
  195. package/dist/runtime/stageAttemptBootstrap.js +55 -3
  196. package/dist/runtime/stageEnvironment.js +1 -0
  197. package/dist/runtime/stageHitl.d.ts +1 -0
  198. package/dist/runtime/stageHitl.js +5 -1
  199. package/dist/runtime/stageProcessLauncher.d.ts +3 -0
  200. package/dist/runtime/stageProcessLauncher.js +52 -5
  201. package/dist/runtime/stageRunner.d.ts +5 -0
  202. package/dist/runtime/stageRunner.js +30 -6
  203. package/dist/runtime/stageStreamLog.d.ts +1 -0
  204. package/dist/runtime/stageStreamLog.js +2 -1
  205. package/dist/runtime/triggerCatalog.d.ts +6 -0
  206. package/dist/runtime/triggerCatalog.js +24 -0
  207. package/dist/runtime/triggerEventMatch.d.ts +2 -0
  208. package/dist/runtime/triggerEventMatch.js +6 -0
  209. package/dist/runtime/triggerPort.d.ts +14 -0
  210. package/dist/runtime/triggerPort.js +1 -0
  211. package/dist/runtime/triggerRunner.d.ts +17 -0
  212. package/dist/runtime/triggerRunner.js +89 -0
  213. package/dist/runtime/verifiedStageExecution.d.ts +7 -0
  214. package/dist/runtime/verifiedStageExecution.js +14 -0
  215. package/dist/runtime/webhookSignature.d.ts +1 -0
  216. package/dist/runtime/webhookSignature.js +12 -0
  217. package/dist/server/bootstrap.d.ts +6 -0
  218. package/dist/server/bootstrap.js +70 -0
  219. package/dist/server/controlToken.js +13 -0
  220. package/dist/server/createHttpHost.js +3 -0
  221. package/dist/server/healthSurfaces.js +31 -6
  222. package/dist/server/http.d.ts +15 -0
  223. package/dist/server/http.js +1251 -74
  224. package/dist/server/requestAuthContext.d.ts +1 -1
  225. package/dist/server/triggerRoutes.d.ts +15 -0
  226. package/dist/server/triggerRoutes.js +268 -0
  227. package/dist/tools/askOperator.d.ts +3 -2
  228. package/dist/types/completion.d.ts +3 -0
  229. package/dist/types/pipeline.d.ts +3 -1
  230. package/dist/types/preEmitCheck.d.ts +7 -0
  231. package/dist/types/stage.d.ts +14 -0
  232. package/dist/types/stageflowManifest.d.ts +3 -0
  233. package/dist/types/trigger.d.ts +18 -0
  234. package/dist/types/trigger.js +1 -0
  235. package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
  236. package/dist/ui/assets/index-p8Iou3zk.js +133 -0
  237. package/dist/ui/index.html +2 -2
  238. package/dist/workshop/autosave.d.ts +58 -0
  239. package/dist/workshop/autosave.js +222 -0
  240. package/dist/workshop/buildStore.d.ts +79 -0
  241. package/dist/workshop/buildStore.js +326 -0
  242. package/dist/workshop/chatTurn.d.ts +232 -0
  243. package/dist/workshop/chatTurn.js +475 -0
  244. package/dist/workshop/modelSettings.d.ts +18 -0
  245. package/dist/workshop/modelSettings.js +44 -0
  246. package/dist/workshop/sessionStore.d.ts +61 -0
  247. package/dist/workshop/sessionStore.js +254 -0
  248. package/dist/workshop/workshopPin.d.ts +72 -0
  249. package/dist/workshop/workshopPin.js +193 -0
  250. package/examples/README.md +2 -1
  251. package/examples/browser-session/README.md +77 -0
  252. package/examples/browser-session/browser-session.pipeline.yaml +50 -0
  253. package/examples/browser-session/browser-session.task.yaml +6 -0
  254. package/examples/browser-session/check-login.yaml +22 -0
  255. package/examples/browser-session/fixture-server.mjs +64 -0
  256. package/examples/browser-session/human-login.yaml +31 -0
  257. package/examples/browser-session/merge.yaml +36 -0
  258. package/examples/browser-session/work-a.yaml +41 -0
  259. package/examples/browser-session/work-b.yaml +41 -0
  260. package/examples/conditional-fork/decide.yaml +1 -1
  261. package/examples/github-release/README.md +17 -3
  262. package/examples/stage-mcp/README.md +3 -2
  263. package/examples/stageflow.yaml +51 -0
  264. package/package.json +9 -4
  265. package/skills/stageflow/references/control-surface.md +3 -1
  266. package/skills/stageflow-author/references/stage-prompt-template.md +4 -4
  267. package/skills/stageflow-run/SKILL.md +2 -0
  268. package/skills/stageflow-run/references/task-and-pipeline-selection.md +3 -1
  269. package/skills/stageflow-setup/SKILL.md +11 -0
  270. package/dist/ui/assets/index-B6hlSXXn.js +0 -120
@@ -0,0 +1,15 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /** Reads a persisted browser env; undefined when absent, throws on other I/O errors. */
3
+ export async function readPersistedBrowserEnv(file) {
4
+ try {
5
+ const parsed = JSON.parse(await readFile(file, "utf8"));
6
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
7
+ return parsed;
8
+ }
9
+ }
10
+ catch (err) {
11
+ if (err.code !== "ENOENT")
12
+ throw err;
13
+ }
14
+ return undefined;
15
+ }
@@ -0,0 +1,40 @@
1
+ import { type ProfileKey } from "./profileStore.js";
2
+ /**
3
+ * A profile is leased to a run. `stageId` is informational (the stage that
4
+ * first took the lease) and never part of identity.
5
+ */
6
+ export type ProfileLockOwner = {
7
+ runId: string;
8
+ stageId?: string;
9
+ };
10
+ export type ProfileLockAcquireResult = {
11
+ status: "acquired";
12
+ release(): Promise<void>;
13
+ } | {
14
+ status: "queued";
15
+ holder: ProfileLockOwner;
16
+ };
17
+ export type RunLiveness = (runId: string) => Promise<boolean>;
18
+ export interface ProfileLock {
19
+ /**
20
+ * Any stage of the holder run joins the lease and succeeds at once. A
21
+ * profile leased to another run returns `queued` with the holder; callers retry.
22
+ */
23
+ acquire(key: ProfileKey, owner: ProfileLockOwner): Promise<ProfileLockAcquireResult>;
24
+ holder(key: ProfileKey): Promise<ProfileLockOwner | undefined>;
25
+ /** Releases every lease held by this run (run end, cancel, abandon, failure). */
26
+ releaseOwner(owner: {
27
+ runId: string;
28
+ }): Promise<void>;
29
+ /** Drops locks whose holder run is no longer live; returns the count dropped. */
30
+ reclaimStale(isRunLive: RunLiveness): Promise<number>;
31
+ }
32
+ export type ProfileLockOptions = {
33
+ /** When set, `acquire` reclaims a lock whose holder run is not live. */
34
+ isRunLive?: RunLiveness;
35
+ };
36
+ export declare function sameOwner(a: ProfileLockOwner, b: ProfileLockOwner): boolean;
37
+ export declare function lockKey(key: ProfileKey): ProfileKey;
38
+ export declare function ownerMatches(held: ProfileLockOwner, target: {
39
+ runId: string;
40
+ }): boolean;
@@ -0,0 +1,10 @@
1
+ import { validateProfileKey } from "./profileStore.js";
2
+ export function sameOwner(a, b) {
3
+ return a.runId === b.runId;
4
+ }
5
+ export function lockKey(key) {
6
+ return validateProfileKey(key);
7
+ }
8
+ export function ownerMatches(held, target) {
9
+ return held.runId === target.runId;
10
+ }
@@ -0,0 +1,23 @@
1
+ export declare const LOCAL_BROWSER_SCOPE = "local";
2
+ export interface ProfileKey {
3
+ scope: string;
4
+ name: string;
5
+ }
6
+ export interface ProfileHandle {
7
+ key: ProfileKey;
8
+ profileDir: string;
9
+ stateDir: string;
10
+ }
11
+ export interface ProfileStore {
12
+ open(key: ProfileKey): Promise<ProfileHandle>;
13
+ list(scope: string): Promise<string[]>;
14
+ delete(key: ProfileKey): Promise<void>;
15
+ deleteScope(scope: string): Promise<void>;
16
+ }
17
+ export declare class InvalidProfileKeyError extends Error {
18
+ readonly code = "invalid_profile_key";
19
+ constructor(message: string);
20
+ }
21
+ export declare function validateProfileName(name: unknown): string;
22
+ export declare function validateProfileScope(scope: unknown): string;
23
+ export declare function validateProfileKey(key: ProfileKey): ProfileKey;
@@ -0,0 +1,39 @@
1
+ // TODO(multi-tenant): this fixed scope is the single local owner. Callers import it instead of
2
+ // receiving a scope from the run, so profiles, locks and CLI commands always use "local".
3
+ // A hosted service must give every run an owner scope (set by the Host from the authenticated
4
+ // user, never from YAML) and pass it down. Find every call site with: grep -rn "LOCAL_BROWSER_SCOPE" src
5
+ // See docs/browser-internals.md, "Known limitations" item 3.
6
+ export const LOCAL_BROWSER_SCOPE = "local";
7
+ export class InvalidProfileKeyError extends Error {
8
+ code = "invalid_profile_key";
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "InvalidProfileKeyError";
12
+ }
13
+ }
14
+ const MAX_SEGMENT_LENGTH = 64;
15
+ const SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
16
+ function validateSegment(label, value) {
17
+ if (typeof value !== "string" || value.length === 0) {
18
+ throw new InvalidProfileKeyError(`Profile ${label} must be a non-empty string`);
19
+ }
20
+ if (value.length > MAX_SEGMENT_LENGTH) {
21
+ throw new InvalidProfileKeyError(`Profile ${label} must be at most ${MAX_SEGMENT_LENGTH} characters`);
22
+ }
23
+ if (!SEGMENT_PATTERN.test(value) || value.includes("..")) {
24
+ throw new InvalidProfileKeyError(`Profile ${label} "${value}" may only use letters, digits, ".", "_" and "-", must start with a letter or digit, and must not contain ".."`);
25
+ }
26
+ return value;
27
+ }
28
+ export function validateProfileName(name) {
29
+ return validateSegment("name", name);
30
+ }
31
+ export function validateProfileScope(scope) {
32
+ return validateSegment("scope", scope);
33
+ }
34
+ export function validateProfileKey(key) {
35
+ return {
36
+ scope: validateProfileScope(key?.scope),
37
+ name: validateProfileName(key?.name),
38
+ };
39
+ }
@@ -0,0 +1,13 @@
1
+ import type { RunStatus } from "../runstore/port.js";
2
+ import type { RunLiveness } from "./profileLock.js";
3
+ /**
4
+ * A run is live while it can still use its browser: not yet terminal. Runs
5
+ * waiting at a gate keep status "running", so they stay live.
6
+ */
7
+ export declare function createRunLiveness(source: {
8
+ readRunMeta(runId: string): Promise<{
9
+ status?: RunStatus;
10
+ }>;
11
+ }): RunLiveness;
12
+ /** `cli-<pid>-...` runs are live while that process is; undefined for other run ids. */
13
+ export declare function cliRunLive(runId: string): boolean | undefined;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A run is live while it can still use its browser: not yet terminal. Runs
3
+ * waiting at a gate keep status "running", so they stay live.
4
+ */
5
+ export function createRunLiveness(source) {
6
+ return async (runId) => {
7
+ const cli = cliRunLive(runId);
8
+ if (cli !== undefined)
9
+ return cli;
10
+ try {
11
+ const status = (await source.readRunMeta(runId)).status;
12
+ return (status === undefined ||
13
+ status === "created" ||
14
+ status === "queued" ||
15
+ status === "running");
16
+ }
17
+ catch (err) {
18
+ if (err.code === "ENOENT")
19
+ return false;
20
+ if (/not found/i.test(String(err?.message)))
21
+ return false;
22
+ throw err;
23
+ }
24
+ };
25
+ }
26
+ /** `cli-<pid>-...` runs are live while that process is; undefined for other run ids. */
27
+ export function cliRunLive(runId) {
28
+ const match = /^cli-(\d+)-/.exec(runId);
29
+ return match === null ? undefined : pidAlive(Number(match[1]));
30
+ }
31
+ function pidAlive(pid) {
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ }
36
+ catch (err) {
37
+ return err.code === "EPERM";
38
+ }
39
+ }
@@ -0,0 +1,10 @@
1
+ import type { StageBrowserConfig } from "../types/stage.js";
2
+ export declare class BlockedSiteError extends Error {
3
+ readonly code = "browser_site_blocked";
4
+ constructor(message: string);
5
+ }
6
+ export declare function normalizeDomain(raw: string): string;
7
+ export declare function hostOf(urlOrHost: string): string | undefined;
8
+ export declare function isBlockedHost(host: string, blocked: readonly string[]): string | undefined;
9
+ /** Throws when the stage's allow_domains or check URLs touch a Host-blocked site. */
10
+ export declare function assertBrowserSitesAllowed(stageId: string, browser: StageBrowserConfig, blocked: readonly string[]): void;
@@ -0,0 +1,61 @@
1
+ export class BlockedSiteError extends Error {
2
+ code = "browser_site_blocked";
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "BlockedSiteError";
6
+ }
7
+ }
8
+ export function normalizeDomain(raw) {
9
+ return raw
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/^\*\./, "")
13
+ .replace(/^\./, "")
14
+ .replace(/\.$/, "");
15
+ }
16
+ export function hostOf(urlOrHost) {
17
+ const text = urlOrHost.trim();
18
+ if (text.length === 0)
19
+ return undefined;
20
+ try {
21
+ const parsed = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(text) ? text : `https://${text}`);
22
+ return parsed.hostname.toLowerCase().replace(/\.$/, "") || undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ function within(host, domain) {
29
+ return host === domain || host.endsWith(`.${domain}`);
30
+ }
31
+ export function isBlockedHost(host, blocked) {
32
+ const h = normalizeDomain(host);
33
+ return blocked.map(normalizeDomain).find((d) => d.length > 0 && within(h, d));
34
+ }
35
+ /** Throws when the stage's allow_domains or check URLs touch a Host-blocked site. */
36
+ export function assertBrowserSitesAllowed(stageId, browser, blocked) {
37
+ if (blocked.length === 0)
38
+ return;
39
+ const norm = blocked.map(normalizeDomain).filter((d) => d.length > 0);
40
+ const fail = (site, where) => {
41
+ throw new BlockedSiteError(`Stage "${stageId}": browser ${where} "${site}" is blocked by Host policy (browser.blocked_sites)`);
42
+ };
43
+ for (const entry of browser.allow_domains ?? []) {
44
+ const d = normalizeDomain(entry);
45
+ if (norm.some((b) => within(d, b) || within(b, d)))
46
+ fail(entry, "allow_domains entry");
47
+ }
48
+ const urls = [
49
+ ["login_url", browser.login_url],
50
+ ["check.url", browser.check?.url],
51
+ ["check.logged_in_url", browser.check?.logged_in_url],
52
+ ...[browser.check?.logged_out_url].flat().map((u) => ["check.logged_out_url", u]),
53
+ ];
54
+ for (const [where, url] of urls) {
55
+ if (!url)
56
+ continue;
57
+ const host = hostOf(url);
58
+ if (host !== undefined && isBlockedHost(host, norm) !== undefined)
59
+ fail(host, where);
60
+ }
61
+ }
@@ -0,0 +1,22 @@
1
+ import type { StageBrowserConfig } from "../types/stage.js";
2
+ import { BROWSER_ENV_FILENAME, type BrowserEnv, type StageBrowserSupport } from "./browserHost.js";
3
+ export { BROWSER_ENV_FILENAME };
4
+ export declare function readStagePersistedBrowserEnv(runDir: string, stageId: string): Promise<BrowserEnv | undefined>;
5
+ export declare function defaultStageBrowserSupport(): StageBrowserSupport;
6
+ /**
7
+ * Computed once per stage and persisted in the run dir: agent-browser silently
8
+ * relaunches the browser when a command's launch env differs from the daemon's,
9
+ * so every attempt and resume worker must see byte-identical values.
10
+ */
11
+ export declare function resolveStageBrowserEnv(support: StageBrowserSupport, input: {
12
+ runId: string;
13
+ stageId: string;
14
+ runDir: string;
15
+ browser: StageBrowserConfig | undefined;
16
+ /** Attempt the login check result belongs to; defaults to 1. */
17
+ attempt?: number;
18
+ /** Human login stage: needs a screen, opens the login page, skips the pre-agent check. */
19
+ humanLogin?: boolean;
20
+ /** Resume worker: the operator's window is already open; do not navigate it. */
21
+ resuming?: boolean;
22
+ }): Promise<BrowserEnv | undefined>;
@@ -0,0 +1,147 @@
1
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadHostConfig } from "../config/hostConfig.js";
4
+ import { stageDir } from "../runstore/paths.js";
5
+ import { safeAudit } from "./auditSink.js";
6
+ import { assertBrowserSitesAllowed } from "./sitePolicy.js";
7
+ import { BROWSER_ENV_FILENAME, BROWSER_POLICY_FILENAME, } from "./browserHost.js";
8
+ import { ensureRunProfileBrowser } from "./anchor.js";
9
+ import { BROWSER_CLOSED_FILENAME, closeBrowserSession, defaultBrowserRunner, writeSessionOwner, } from "./browserTeardown.js";
10
+ import { defaultDisplayProbe, loginPageUrl, noScreenError } from "./humanLogin.js";
11
+ import { ensureStageLoginCheck, OPEN_COMMAND_TIMEOUT_MS } from "./loginCheck.js";
12
+ import { readPersistedBrowserEnv } from "./persistedEnv.js";
13
+ import { createLocalBrowserHost } from "./localBrowserHost.js";
14
+ import { createLocalProfileStore } from "./localProfileStore.js";
15
+ import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
16
+ export { BROWSER_ENV_FILENAME };
17
+ export async function readStagePersistedBrowserEnv(runDir, stageId) {
18
+ return readPersistedBrowserEnv(path.join(stageDir(runDir, stageId), BROWSER_ENV_FILENAME));
19
+ }
20
+ let defaultSupport;
21
+ export function defaultStageBrowserSupport() {
22
+ defaultSupport ??= {
23
+ host: createLocalBrowserHost(),
24
+ profiles: createLocalProfileStore(),
25
+ };
26
+ return defaultSupport;
27
+ }
28
+ /**
29
+ * Computed once per stage and persisted in the run dir: agent-browser silently
30
+ * relaunches the browser when a command's launch env differs from the daemon's,
31
+ * so every attempt and resume worker must see byte-identical values.
32
+ */
33
+ export async function resolveStageBrowserEnv(support, input) {
34
+ const { browser } = input;
35
+ if (browser === undefined)
36
+ return undefined;
37
+ if (input.humanLogin === true) {
38
+ const screen = (support.display ?? defaultDisplayProbe)();
39
+ if (!screen.hasDisplay)
40
+ throw noScreenError(screen.docker);
41
+ }
42
+ if ((browser.allow_domains?.length ?? 0) > 0 ||
43
+ browser.check !== undefined ||
44
+ browser.login_url !== undefined) {
45
+ assertBrowserSitesAllowed(input.stageId, browser, support.blockedSites ?? loadHostConfig().browserBlockedSites);
46
+ }
47
+ const dir = stageDir(input.runDir, input.stageId);
48
+ 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.
50
+ const profile = browser.profile !== undefined
51
+ ? await support.profiles.open({
52
+ scope: LOCAL_BROWSER_SCOPE,
53
+ name: browser.profile,
54
+ })
55
+ : undefined;
56
+ const anchor = profile !== undefined
57
+ ? await ensureRunProfileBrowser(support, {
58
+ runId: input.runId,
59
+ runDir: input.runDir,
60
+ browser,
61
+ profile,
62
+ ...(input.humanLogin === true ? { humanLogin: true } : {}),
63
+ })
64
+ : undefined;
65
+ const fresh = await support.host.stageEnv({
66
+ runId: input.runId,
67
+ stageId: input.stageId,
68
+ browser,
69
+ ...(profile !== undefined ? { profile } : {}),
70
+ ...(anchor !== undefined ? { cdpAddress: anchor.cdpAddress } : {}),
71
+ ...(input.humanLogin === true ? { humanLogin: true } : {}),
72
+ });
73
+ await rm(path.join(dir, BROWSER_CLOSED_FILENAME), { force: true });
74
+ const persisted = await readPersistedBrowserEnv(file);
75
+ let env = persisted ?? fresh;
76
+ const writeEnv = async (value) => {
77
+ await mkdir(dir, { recursive: true });
78
+ const tmp = `${file}.${process.pid}.tmp`;
79
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
80
+ await rename(tmp, file);
81
+ };
82
+ if (persisted !== undefined &&
83
+ anchor !== undefined &&
84
+ persisted.AGENT_BROWSER_CDP !== anchor.cdpAddress) {
85
+ // The shared browser was restarted: only the attach address changes. The
86
+ // old stage daemon is still attached to the dead address, so close it first.
87
+ await closeBrowserSession(persisted, {
88
+ ...(support.runner !== undefined ? { runner: support.runner } : {}),
89
+ ...(support.closeWaitMs !== undefined ? { closeWaitMs: support.closeWaitMs } : {}),
90
+ }).catch(() => undefined);
91
+ if (persisted.AGENT_BROWSER_SOCKET_DIR !== undefined) {
92
+ await mkdir(persisted.AGENT_BROWSER_SOCKET_DIR, { recursive: true, mode: 0o700 });
93
+ }
94
+ env = { ...persisted, AGENT_BROWSER_CDP: anchor.cdpAddress };
95
+ await writeEnv(env);
96
+ }
97
+ if (persisted === undefined) {
98
+ await writeEnv(fresh);
99
+ if (browser.profile !== undefined) {
100
+ // TODO(multi-tenant): audit the run owner's scope.
101
+ await safeAudit(support.audit, {
102
+ event: "profile_used",
103
+ scope: LOCAL_BROWSER_SCOPE,
104
+ profile: browser.profile,
105
+ runId: input.runId,
106
+ stageId: input.stageId,
107
+ });
108
+ }
109
+ }
110
+ if (browser.profile !== undefined && (browser.allow_domains?.length ?? 0) > 0) {
111
+ await writeFile(path.join(dir, BROWSER_POLICY_FILENAME), `${JSON.stringify({
112
+ runId: input.runId,
113
+ stageId: input.stageId,
114
+ profile: browser.profile,
115
+ allow_domains: browser.allow_domains,
116
+ })}\n`, { mode: 0o600 });
117
+ }
118
+ await writeSessionOwner({
119
+ runId: input.runId,
120
+ stageId: input.stageId,
121
+ runDir: input.runDir,
122
+ env,
123
+ }).catch(() => undefined);
124
+ if (input.humanLogin === true) {
125
+ const url = input.resuming === true ? undefined : loginPageUrl(browser);
126
+ if (url !== undefined) {
127
+ const opened = await (support.runner ?? defaultBrowserRunner)(["open", url], env, {
128
+ timeoutMs: OPEN_COMMAND_TIMEOUT_MS,
129
+ });
130
+ if (opened.code !== 0) {
131
+ throw new Error(`could not open the login page ${url}`);
132
+ }
133
+ }
134
+ }
135
+ else if (browser.check !== undefined) {
136
+ await ensureStageLoginCheck({
137
+ runDir: input.runDir,
138
+ stageId: input.stageId,
139
+ attempt: input.attempt ?? 1,
140
+ env,
141
+ check: browser.check,
142
+ ...(support.runner !== undefined ? { runner: support.runner } : {}),
143
+ ...(support.loginCheck !== undefined ? { options: support.loginCheck } : {}),
144
+ });
145
+ }
146
+ return env;
147
+ }
@@ -0,0 +1,17 @@
1
+ import type { StageBrowserSupport } from "./browserHost.js";
2
+ import type { ProfileLock, ProfileLockOwner, RunLiveness } from "./profileLock.js";
3
+ export declare function stageProfileLock(support: StageBrowserSupport): ProfileLock;
4
+ export type ProfileWaitInput = {
5
+ profile: string;
6
+ owner: ProfileLockOwner;
7
+ isRunLive?: RunLiveness;
8
+ halted?: () => boolean;
9
+ /** Called when the holder is first seen or changes. */
10
+ onWaiting?: (holder: ProfileLockOwner) => Promise<void> | void;
11
+ };
12
+ /**
13
+ * Joins the run's lease on the profile, or takes it, waiting while another run
14
+ * holds it. The lease is released at run end, never at stage end.
15
+ */
16
+ export declare function acquireStageProfile(support: StageBrowserSupport, input: ProfileWaitInput): Promise<"acquired" | "halted">;
17
+ export declare function profileWaitingMessage(profile: string, holder: ProfileLockOwner): string;
@@ -0,0 +1,39 @@
1
+ import { createLocalProfileLock } from "./localProfileLock.js";
2
+ import { LOCAL_BROWSER_SCOPE } from "./profileStore.js";
3
+ const DEFAULT_LOCK_POLL_MS = 250;
4
+ let defaultLock;
5
+ export function stageProfileLock(support) {
6
+ if (support.locks !== undefined)
7
+ return support.locks;
8
+ defaultLock ??= createLocalProfileLock();
9
+ return defaultLock;
10
+ }
11
+ /**
12
+ * Joins the run's lease on the profile, or takes it, waiting while another run
13
+ * holds it. The lease is released at run end, never at stage end.
14
+ */
15
+ export async function acquireStageProfile(support, input) {
16
+ 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 };
19
+ const pollMs = support.lockPollMs ?? DEFAULT_LOCK_POLL_MS;
20
+ let announced;
21
+ for (;;) {
22
+ const result = await locks.acquire(key, input.owner);
23
+ if (result.status === "acquired")
24
+ return "acquired";
25
+ if (input.halted?.())
26
+ return "halted";
27
+ if (announced === undefined || announced.runId !== result.holder.runId) {
28
+ announced = result.holder;
29
+ await input.onWaiting?.(result.holder);
30
+ }
31
+ if (input.isRunLive !== undefined) {
32
+ await locks.reclaimStale(input.isRunLive).catch(() => 0);
33
+ }
34
+ await new Promise((r) => setTimeout(r, pollMs));
35
+ }
36
+ }
37
+ export function profileWaitingMessage(profile, holder) {
38
+ return `waiting for browser profile "${profile}" held by run ${holder.runId}`;
39
+ }
@@ -0,0 +1,44 @@
1
+ import { type AuditSink } from "../browser/auditSink.js";
2
+ import type { BrowserEnv, BrowserHost, BrowserRunner } from "../browser/browserHost.js";
3
+ import { type DisplayProbe } from "../browser/humanLogin.js";
4
+ import type { ProfileLock, RunLiveness } from "../browser/profileLock.js";
5
+ import { type ProfileStore } from "../browser/profileStore.js";
6
+ export declare const BROWSER_USAGE = "Usage:\n sf browser profiles [--json]\n sf browser status <name> [--json]\n sf browser check <name> --url <url> --logged-in <glob> [--logged-out <glob>]... [--headless] [--json]\n sf browser login <name> --url <login-url> --logged-in <glob> [--timeout-sec <n>] [--json]\n sf browser clear <name> [--yes] [--json]\n\nManage saved browser logins (profiles) on this computer. Output never shows profile paths or cookie values.\nExit codes: 0 ok / logged in; 1 error; 2 profile in use by a live run; 3 logged out; 4 unknown; 5 login not completed (timeout or window closed); 130 interrupted.";
7
+ export declare const BROWSER_EXIT: {
8
+ readonly ok: 0;
9
+ readonly error: 1;
10
+ readonly busy: 2;
11
+ readonly loggedOut: 3;
12
+ readonly unknown: 4;
13
+ readonly notCompleted: 5;
14
+ readonly interrupted: 130;
15
+ };
16
+ export type BrowserCommandDeps = {
17
+ log: (line: string) => void;
18
+ error: (line: string) => void;
19
+ store: ProfileStore;
20
+ host: BrowserHost;
21
+ locks: ProfileLock;
22
+ runner: BrowserRunner;
23
+ audit?: AuditSink;
24
+ /** Host-blocked sites; defaults to `browser.blocked_sites` from Host config. */
25
+ blockedSites?: readonly string[];
26
+ display: DisplayProbe;
27
+ /** Liveness of the run holding a profile lock. */
28
+ isRunLive: RunLiveness;
29
+ /** True when a daemon session for this env is open. */
30
+ sessionOpen: (env: BrowserEnv) => boolean;
31
+ /** profile name -> ISO time of last recorded use (from the audit log). */
32
+ auditLastUsed: () => Promise<Record<string, string>>;
33
+ confirm: (question: string) => Promise<boolean>;
34
+ interactive: boolean;
35
+ signal?: AbortSignal;
36
+ sleep: (ms: number) => Promise<void>;
37
+ closeWaitMs?: number;
38
+ loginCheck?: {
39
+ waitMs?: number;
40
+ settleMs?: number;
41
+ };
42
+ loginPollMs?: number;
43
+ };
44
+ export declare function runBrowserCommand(args: string[], deps?: Partial<BrowserCommandDeps>): Promise<number>;