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,218 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { globalStageflowHome } from "../project/globalHome.js";
5
+ import { lockKey, ownerMatches, sameOwner, } from "./profileLock.js";
6
+ function locksRoot() {
7
+ return path.join(globalStageflowHome(), "browser", "locks");
8
+ }
9
+ function lockFile(key) {
10
+ return path.join(locksRoot(), key.scope, `${key.name}.lock`);
11
+ }
12
+ async function readHolder(file) {
13
+ try {
14
+ const parsed = JSON.parse(await readFile(file, "utf8"));
15
+ if (typeof parsed.runId === "string") {
16
+ return typeof parsed.stageId === "string"
17
+ ? { runId: parsed.runId, stageId: parsed.stageId }
18
+ : { runId: parsed.runId };
19
+ }
20
+ }
21
+ catch {
22
+ // missing or unreadable: treated as free by callers that re-check
23
+ }
24
+ return undefined;
25
+ }
26
+ /** A lock file that exists but cannot be parsed (crash, disk full, manual edit). */
27
+ async function isCorrupt(file) {
28
+ let raw;
29
+ try {
30
+ raw = await readFile(file, "utf8");
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ return readHolderFrom(raw) === undefined;
36
+ }
37
+ function readHolderFrom(raw) {
38
+ try {
39
+ const parsed = JSON.parse(raw);
40
+ if (typeof parsed.runId === "string") {
41
+ return typeof parsed.stageId === "string"
42
+ ? { runId: parsed.runId, stageId: parsed.stageId }
43
+ : { runId: parsed.runId };
44
+ }
45
+ }
46
+ catch {
47
+ // unparseable
48
+ }
49
+ return undefined;
50
+ }
51
+ /** Removes a corrupt lock file via rename-away; restores it if it turned out valid. */
52
+ async function dropCorrupt(file) {
53
+ const tomb = `${file}.${process.pid}.${randomUUID()}.stale`;
54
+ try {
55
+ await rename(file, tomb);
56
+ }
57
+ catch {
58
+ return true;
59
+ }
60
+ if ((await readHolder(tomb)) !== undefined) {
61
+ await link(tomb, file).catch(() => undefined);
62
+ await rm(tomb, { force: true });
63
+ return false;
64
+ }
65
+ await rm(tomb, { force: true });
66
+ return true;
67
+ }
68
+ /** `link` fails with EEXIST when the lock exists, and the content is complete at creation. */
69
+ async function createExclusive(file, owner) {
70
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
71
+ const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
72
+ await writeFile(tmp, `${JSON.stringify(owner)}\n`, { mode: 0o600 });
73
+ try {
74
+ await link(tmp, file);
75
+ return true;
76
+ }
77
+ catch (err) {
78
+ if (err.code === "EEXIST")
79
+ return false;
80
+ throw err;
81
+ }
82
+ finally {
83
+ await rm(tmp, { force: true });
84
+ }
85
+ }
86
+ /**
87
+ * Removes a lock judged stale. The file is renamed away first and re-read, so
88
+ * a lock another process re-created in the meantime is not dropped.
89
+ */
90
+ async function dropStale(file, stale) {
91
+ const tomb = `${file}.${process.pid}.${randomUUID()}.stale`;
92
+ try {
93
+ await rename(file, tomb);
94
+ }
95
+ catch {
96
+ return true;
97
+ }
98
+ const moved = await readHolder(tomb);
99
+ if (moved !== undefined && !sameOwner(moved, stale)) {
100
+ await link(tomb, file).catch(() => undefined);
101
+ await rm(tomb, { force: true });
102
+ return false;
103
+ }
104
+ await rm(tomb, { force: true });
105
+ return true;
106
+ }
107
+ async function lockFiles() {
108
+ const files = [];
109
+ let scopes;
110
+ try {
111
+ scopes = await readdir(locksRoot());
112
+ }
113
+ catch {
114
+ return files;
115
+ }
116
+ for (const scope of scopes) {
117
+ let names;
118
+ try {
119
+ names = await readdir(path.join(locksRoot(), scope));
120
+ }
121
+ catch {
122
+ continue;
123
+ }
124
+ for (const name of names) {
125
+ if (name.endsWith(".lock"))
126
+ files.push(path.join(locksRoot(), scope, name));
127
+ }
128
+ }
129
+ return files;
130
+ }
131
+ export function createLocalProfileLock(options = {}) {
132
+ // Serializes this process's operations per lock file; other processes rely on link/rename atomicity.
133
+ const chains = new Map();
134
+ const serialized = (file, task) => {
135
+ const next = (chains.get(file) ?? Promise.resolve())
136
+ .catch(() => undefined)
137
+ .then(task);
138
+ chains.set(file, next);
139
+ void next.finally(() => {
140
+ if (chains.get(file) === next)
141
+ chains.delete(file);
142
+ });
143
+ return next;
144
+ };
145
+ const releaseFile = (file, owner) => serialized(file, async () => {
146
+ const current = await readHolder(file);
147
+ if (current !== undefined && sameOwner(current, owner)) {
148
+ await rm(file, { force: true });
149
+ }
150
+ });
151
+ return {
152
+ async acquire(rawKey, owner) {
153
+ const file = lockFile(lockKey(rawKey));
154
+ return serialized(file, async () => {
155
+ for (let attempt = 0; attempt < 3; attempt += 1) {
156
+ if (await createExclusive(file, owner)) {
157
+ return {
158
+ status: "acquired",
159
+ release: () => releaseFile(file, owner),
160
+ };
161
+ }
162
+ const current = await readHolder(file);
163
+ if (current === undefined) {
164
+ if (await isCorrupt(file))
165
+ await dropCorrupt(file);
166
+ continue;
167
+ }
168
+ if (sameOwner(current, owner)) {
169
+ return {
170
+ status: "acquired",
171
+ release: () => releaseFile(file, owner),
172
+ };
173
+ }
174
+ if (options.isRunLive !== undefined &&
175
+ !(await options.isRunLive(current.runId))) {
176
+ if (await dropStale(file, current))
177
+ continue;
178
+ }
179
+ return { status: "queued", holder: current };
180
+ }
181
+ const holder = await readHolder(file);
182
+ return {
183
+ status: "queued",
184
+ holder: holder ?? { runId: "unknown" },
185
+ };
186
+ });
187
+ },
188
+ async holder(rawKey) {
189
+ return readHolder(lockFile(lockKey(rawKey)));
190
+ },
191
+ async releaseOwner(target) {
192
+ for (const file of await lockFiles()) {
193
+ const current = await readHolder(file);
194
+ if (current !== undefined && ownerMatches(current, target)) {
195
+ await releaseFile(file, current);
196
+ }
197
+ }
198
+ },
199
+ async reclaimStale(isRunLive) {
200
+ let dropped = 0;
201
+ for (const file of await lockFiles()) {
202
+ const current = await readHolder(file);
203
+ if (current === undefined) {
204
+ if (await isCorrupt(file)) {
205
+ if (await serialized(file, () => dropCorrupt(file)))
206
+ dropped += 1;
207
+ }
208
+ continue;
209
+ }
210
+ if (await isRunLive(current.runId))
211
+ continue;
212
+ if (await serialized(file, () => dropStale(file, current)))
213
+ dropped += 1;
214
+ }
215
+ return dropped;
216
+ },
217
+ };
218
+ }
@@ -0,0 +1,5 @@
1
+ import { type AuditSink } from "./auditSink.js";
2
+ import { type ProfileStore } from "./profileStore.js";
3
+ export declare function createLocalProfileStore(options?: {
4
+ audit?: AuditSink;
5
+ }): ProfileStore;
@@ -0,0 +1,91 @@
1
+ import { access, chmod, mkdir, readdir, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { globalStageflowHome } from "../project/globalHome.js";
4
+ import { createLocalAuditSink, safeAudit } from "./auditSink.js";
5
+ import { validateProfileKey, validateProfileScope, } from "./profileStore.js";
6
+ function browserRoot() {
7
+ return path.join(globalStageflowHome(), "browser");
8
+ }
9
+ function scopeDir(scope) {
10
+ return path.join(browserRoot(), scope);
11
+ }
12
+ async function ensurePrivateDir(dir) {
13
+ await mkdir(dir, { recursive: true, mode: 0o700 });
14
+ await chmod(dir, 0o700);
15
+ }
16
+ async function exists(p) {
17
+ try {
18
+ await access(p);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ export function createLocalProfileStore(options = {}) {
26
+ const audit = options.audit ?? createLocalAuditSink();
27
+ return {
28
+ async open(rawKey) {
29
+ const key = validateProfileKey(rawKey);
30
+ const base = path.join(scopeDir(key.scope), key.name);
31
+ const profileDir = path.join(base, "profile");
32
+ const stateDir = path.join(base, "state");
33
+ const created = !(await exists(base));
34
+ await ensurePrivateDir(browserRoot());
35
+ await ensurePrivateDir(scopeDir(key.scope));
36
+ await ensurePrivateDir(base);
37
+ await ensurePrivateDir(profileDir);
38
+ await ensurePrivateDir(stateDir);
39
+ if (created) {
40
+ await safeAudit(audit, {
41
+ event: "profile_created",
42
+ scope: key.scope,
43
+ profile: key.name,
44
+ });
45
+ }
46
+ return { key, profileDir, stateDir };
47
+ },
48
+ async list(scope) {
49
+ const dir = scopeDir(validateProfileScope(scope));
50
+ try {
51
+ const entries = await readdir(dir, { withFileTypes: true });
52
+ return entries
53
+ .filter((e) => e.isDirectory())
54
+ .map((e) => e.name)
55
+ .sort();
56
+ }
57
+ catch (err) {
58
+ if (err.code === "ENOENT")
59
+ return [];
60
+ throw err;
61
+ }
62
+ },
63
+ async delete(rawKey) {
64
+ const key = validateProfileKey(rawKey);
65
+ const target = path.join(scopeDir(key.scope), key.name);
66
+ const existed = await exists(target);
67
+ await rm(target, { recursive: true, force: true });
68
+ if (existed) {
69
+ await safeAudit(audit, {
70
+ event: "profile_deleted",
71
+ scope: key.scope,
72
+ profile: key.name,
73
+ });
74
+ }
75
+ },
76
+ async deleteScope(scope) {
77
+ const valid = validateProfileScope(scope);
78
+ for (const name of await this.list(valid)) {
79
+ await safeAudit(audit, {
80
+ event: "profile_deleted",
81
+ scope: valid,
82
+ profile: name,
83
+ });
84
+ }
85
+ await rm(scopeDir(valid), {
86
+ recursive: true,
87
+ force: true,
88
+ });
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,53 @@
1
+ import type { StageBrowserConfig } from "../types/stage.js";
2
+ import type { BrowserEnv, BrowserRunner } from "./browserHost.js";
3
+ export declare const BROWSER_LOGIN_CHECK_FILENAME = "browser-login-check.json";
4
+ export type LoginCheckConfig = NonNullable<StageBrowserConfig["check"]>;
5
+ export type LoginState = "logged_in" | "logged_out" | "unknown";
6
+ export type LoginCheckMatch = {
7
+ state: LoginState;
8
+ /** `null` when the patterns cannot decide. */
9
+ logged_in: boolean | null;
10
+ };
11
+ export type LoginCheckResult = LoginCheckMatch & {
12
+ url: string;
13
+ attempt: number;
14
+ check_url: string;
15
+ };
16
+ export declare const OPEN_COMMAND_TIMEOUT_MS = 60000;
17
+ /** Pure: `logged_out_url` wins over `logged_in_url`; no match is `unknown`. */
18
+ export declare function matchLoginState(input: {
19
+ finalUrl: string;
20
+ check: Pick<LoginCheckConfig, "logged_in_url" | "logged_out_url">;
21
+ }): LoginCheckMatch;
22
+ export type LoginCheckOptions = {
23
+ waitMs?: number;
24
+ settleMs?: number;
25
+ };
26
+ /**
27
+ * Opens `check.url` in the stage's own browser session (same persisted env, so
28
+ * no second browser is launched) and matches the settled final address. The
29
+ * session stays open for the stage; its teardown closes it later.
30
+ */
31
+ export declare function runLoginCheck(env: BrowserEnv, check: LoginCheckConfig, runner: BrowserRunner, options?: LoginCheckOptions): Promise<LoginCheckMatch & {
32
+ url: string;
33
+ }>;
34
+ export declare function readStageLoginCheck(runDir: string, stageId: string): Promise<LoginCheckResult | undefined>;
35
+ /**
36
+ * Computes the Host result once per attempt and persists it in the stage run
37
+ * dir. A resume of the same attempt reuses the stored value, so the result the
38
+ * agent saw is the result the emit is validated against.
39
+ */
40
+ export declare function ensureStageLoginCheck(input: {
41
+ runDir: string;
42
+ stageId: string;
43
+ attempt: number;
44
+ env: BrowserEnv;
45
+ check: LoginCheckConfig;
46
+ runner?: BrowserRunner;
47
+ options?: LoginCheckOptions;
48
+ }): Promise<LoginCheckResult>;
49
+ export type ExpectedLoginResult = Pick<LoginCheckResult, "state" | "logged_in" | "url">;
50
+ /** Prompt block that hands the Host result to the agent. */
51
+ export declare function loginCheckPromptBlock(result: ExpectedLoginResult): string;
52
+ /** Emit-time validation of the payload against the Host result; `undefined` when valid. */
53
+ export declare function loginCheckIssue(payload: unknown, expected: ExpectedLoginResult): string | undefined;
@@ -0,0 +1,147 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stageDir } from "../runstore/paths.js";
4
+ import { defaultBrowserRunner } from "./browserTeardown.js";
5
+ export const BROWSER_LOGIN_CHECK_FILENAME = "browser-login-check.json";
6
+ const DEFAULT_WAIT_MS = 15_000;
7
+ const WAIT_TIMEOUT_MARGIN_MS = 15_000;
8
+ export const OPEN_COMMAND_TIMEOUT_MS = 60_000;
9
+ const DEFAULT_SETTLE_MS = 300;
10
+ const MAX_URL_READS = 4;
11
+ /** `*` matches any run of characters (including `/`), `?` one character. Whole-URL match. */
12
+ function globToRegExp(glob) {
13
+ let source = "";
14
+ for (const ch of glob.replace(/\*+/g, "*")) {
15
+ if (ch === "*")
16
+ source += ".*";
17
+ else if (ch === "?")
18
+ source += ".";
19
+ else
20
+ source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
21
+ }
22
+ return new RegExp(`^${source}$`, "s");
23
+ }
24
+ function asList(value) {
25
+ if (value === undefined)
26
+ return [];
27
+ return Array.isArray(value) ? value : [value];
28
+ }
29
+ function matchesAny(url, globs) {
30
+ return globs.some((glob) => globToRegExp(glob).test(url));
31
+ }
32
+ /** Pure: `logged_out_url` wins over `logged_in_url`; no match is `unknown`. */
33
+ export function matchLoginState(input) {
34
+ const { finalUrl, check } = input;
35
+ if (matchesAny(finalUrl, asList(check.logged_out_url))) {
36
+ return { state: "logged_out", logged_in: false };
37
+ }
38
+ if (matchesAny(finalUrl, asList(check.logged_in_url))) {
39
+ return { state: "logged_in", logged_in: true };
40
+ }
41
+ return { state: "unknown", logged_in: null };
42
+ }
43
+ async function readUrl(env, runner) {
44
+ const got = await runner(["get", "url"], env);
45
+ if (got.code !== 0) {
46
+ throw new Error("login check: could not read the page address");
47
+ }
48
+ return (got.stdout ?? "").trim();
49
+ }
50
+ /**
51
+ * Opens `check.url` in the stage's own browser session (same persisted env, so
52
+ * no second browser is launched) and matches the settled final address. The
53
+ * session stays open for the stage; its teardown closes it later.
54
+ */
55
+ export async function runLoginCheck(env, check, runner, options = {}) {
56
+ const opened = await runner(["open", check.url], env, {
57
+ timeoutMs: OPEN_COMMAND_TIMEOUT_MS,
58
+ });
59
+ if (opened.code !== 0) {
60
+ throw new Error(`login check: could not open ${check.url}`);
61
+ }
62
+ const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
63
+ await runner(["wait", "--load", "networkidle", "--timeout", String(waitMs)], env, {
64
+ timeoutMs: waitMs + WAIT_TIMEOUT_MARGIN_MS,
65
+ }).catch(() => undefined);
66
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
67
+ let url = await readUrl(env, runner);
68
+ for (let i = 1; i < MAX_URL_READS; i++) {
69
+ if (settleMs > 0)
70
+ await new Promise((r) => setTimeout(r, settleMs));
71
+ const next = await readUrl(env, runner);
72
+ if (next === url)
73
+ break;
74
+ url = next;
75
+ }
76
+ if (url === "") {
77
+ throw new Error("login check: the browser reported an empty address");
78
+ }
79
+ return { ...matchLoginState({ finalUrl: url, check }), url };
80
+ }
81
+ function resultFile(runDir, stageId) {
82
+ return path.join(stageDir(runDir, stageId), BROWSER_LOGIN_CHECK_FILENAME);
83
+ }
84
+ export async function readStageLoginCheck(runDir, stageId) {
85
+ try {
86
+ const parsed = JSON.parse(await readFile(resultFile(runDir, stageId), "utf8"));
87
+ if (parsed && typeof parsed === "object" && typeof parsed.url === "string") {
88
+ return parsed;
89
+ }
90
+ }
91
+ catch {
92
+ // not computed yet
93
+ }
94
+ return undefined;
95
+ }
96
+ /**
97
+ * Computes the Host result once per attempt and persists it in the stage run
98
+ * dir. A resume of the same attempt reuses the stored value, so the result the
99
+ * agent saw is the result the emit is validated against.
100
+ */
101
+ export async function ensureStageLoginCheck(input) {
102
+ const existing = await readStageLoginCheck(input.runDir, input.stageId);
103
+ if (existing !== undefined &&
104
+ existing.attempt === input.attempt &&
105
+ existing.check_url === input.check.url) {
106
+ return existing;
107
+ }
108
+ const probed = await runLoginCheck(input.env, input.check, input.runner ?? defaultBrowserRunner, input.options);
109
+ const result = {
110
+ ...probed,
111
+ attempt: input.attempt,
112
+ check_url: input.check.url,
113
+ };
114
+ const file = resultFile(input.runDir, input.stageId);
115
+ await mkdir(path.dirname(file), { recursive: true });
116
+ const tmp = `${file}.${process.pid}.tmp`;
117
+ await writeFile(tmp, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
118
+ await rename(tmp, file);
119
+ return result;
120
+ }
121
+ /** Prompt block that hands the Host result to the agent. */
122
+ export function loginCheckPromptBlock(result) {
123
+ const lines = ["## Host login check", ""];
124
+ if (result.state === "unknown") {
125
+ lines.push(`The Host opened the check page and its final address is ${JSON.stringify(result.url)}, but the configured patterns cannot decide whether the session is logged in (result: unknown).`, "The browser session is already open on that page. Use the browser skill to inspect the page (read-only), decide whether the profile is logged in, and emit payload `{ \"logged_in\": <true|false>, \"url\": <that exact address> }`.", "The Host only checks that `logged_in` is a boolean and that `url` equals the address above.");
126
+ }
127
+ else {
128
+ lines.push(`Host login check result: ${JSON.stringify({ logged_in: result.logged_in, url: result.url })}`, "Report exactly this in your envelope payload. Do not browse. The Host rejects any other value.");
129
+ }
130
+ return lines.join("\n");
131
+ }
132
+ /** Emit-time validation of the payload against the Host result; `undefined` when valid. */
133
+ export function loginCheckIssue(payload, expected) {
134
+ const value = payload && typeof payload === "object"
135
+ ? payload
136
+ : {};
137
+ if (typeof value.logged_in !== "boolean") {
138
+ return "login check payload.logged_in must be a boolean";
139
+ }
140
+ if (value.url !== expected.url) {
141
+ return `login check payload.url must equal the Host-observed address ${JSON.stringify(expected.url)}`;
142
+ }
143
+ if (expected.logged_in !== null && value.logged_in !== expected.logged_in) {
144
+ return `login check payload.logged_in must be ${expected.logged_in} (Host result)`;
145
+ }
146
+ return undefined;
147
+ }
@@ -0,0 +1,2 @@
1
+ import { type ProfileLock, type ProfileLockOptions } from "./profileLock.js";
2
+ export declare function createInMemoryProfileLock(options?: ProfileLockOptions): ProfileLock;
@@ -0,0 +1,53 @@
1
+ import { lockKey, ownerMatches, sameOwner, } from "./profileLock.js";
2
+ export function createInMemoryProfileLock(options = {}) {
3
+ const held = new Map();
4
+ const id = (key) => `${key.scope}\u0000${key.name}`;
5
+ const releaseIfHeldBy = (k, owner) => {
6
+ const current = held.get(k);
7
+ if (current !== undefined && sameOwner(current, owner))
8
+ held.delete(k);
9
+ };
10
+ return {
11
+ async acquire(rawKey, owner) {
12
+ const key = lockKey(rawKey);
13
+ const k = id(key);
14
+ let current = held.get(k);
15
+ if (current !== undefined &&
16
+ !sameOwner(current, owner) &&
17
+ options.isRunLive !== undefined &&
18
+ !(await options.isRunLive(current.runId))) {
19
+ held.delete(k);
20
+ current = undefined;
21
+ }
22
+ if (current !== undefined && !sameOwner(current, owner)) {
23
+ return { status: "queued", holder: { ...current } };
24
+ }
25
+ if (current === undefined)
26
+ held.set(k, { ...owner });
27
+ return {
28
+ status: "acquired",
29
+ release: async () => releaseIfHeldBy(k, owner),
30
+ };
31
+ },
32
+ async holder(rawKey) {
33
+ const current = held.get(id(lockKey(rawKey)));
34
+ return current === undefined ? undefined : { ...current };
35
+ },
36
+ async releaseOwner(target) {
37
+ for (const [k, owner] of [...held]) {
38
+ if (ownerMatches(owner, target))
39
+ held.delete(k);
40
+ }
41
+ },
42
+ async reclaimStale(isRunLive) {
43
+ let dropped = 0;
44
+ for (const [k, owner] of [...held]) {
45
+ if (!(await isRunLive(owner.runId))) {
46
+ held.delete(k);
47
+ dropped += 1;
48
+ }
49
+ }
50
+ return dropped;
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,2 @@
1
+ import { type ProfileStore } from "./profileStore.js";
2
+ export declare function createInMemoryProfileStore(): ProfileStore;
@@ -0,0 +1,31 @@
1
+ import { validateProfileKey, validateProfileScope, } from "./profileStore.js";
2
+ export function createInMemoryProfileStore() {
3
+ const scopes = new Map();
4
+ return {
5
+ async open(rawKey) {
6
+ const key = validateProfileKey(rawKey);
7
+ let profiles = scopes.get(key.scope);
8
+ if (!profiles) {
9
+ profiles = new Map();
10
+ scopes.set(key.scope, profiles);
11
+ }
12
+ let handle = profiles.get(key.name);
13
+ if (!handle) {
14
+ const base = `memory://${key.scope}/${key.name}`;
15
+ handle = { key, profileDir: `${base}/profile`, stateDir: `${base}/state` };
16
+ profiles.set(key.name, handle);
17
+ }
18
+ return handle;
19
+ },
20
+ async list(scope) {
21
+ return [...(scopes.get(validateProfileScope(scope))?.keys() ?? [])].sort();
22
+ },
23
+ async delete(rawKey) {
24
+ const key = validateProfileKey(rawKey);
25
+ scopes.get(key.scope)?.delete(key.name);
26
+ },
27
+ async deleteScope(scope) {
28
+ scopes.delete(validateProfileScope(scope));
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,16 @@
1
+ import type { StageLogEvent } from "../runstore/port.js";
2
+ import { type AuditSink } from "./auditSink.js";
3
+ export type NavigationPolicy = {
4
+ runId: string;
5
+ stageId: string;
6
+ profile?: string;
7
+ allow_domains: string[];
8
+ };
9
+ /**
10
+ * Soft allowlist check for profile stages (agent-browser refuses
11
+ * --allowed-domains with --profile). The only cheap signal is the persisted
12
+ * bash tool activity: it sees explicit `agent-browser open|goto|navigate <url>`
13
+ * commands, not redirects, link clicks or in-page navigation. Hosts only are
14
+ * recorded, never full URLs.
15
+ */
16
+ export declare function auditStageNavigations(policy: NavigationPolicy, events: (() => Promise<StageLogEvent[]>) | undefined, sink: AuditSink | undefined): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { safeAudit } from "./auditSink.js";
2
+ import { hostOf, normalizeDomain } from "./sitePolicy.js";
3
+ const NAV_COMMAND = /\bagent-browser\b[^\n|;&]*?\s(?:open|goto|navigate|tab\s+new)\s+(?:--?\S+\s+)*["']?([^\s"'|;&]+)/g;
4
+ function allowed(host, allow) {
5
+ return allow.some((entry) => {
6
+ const d = normalizeDomain(entry);
7
+ return host === d || host.endsWith(`.${d}`);
8
+ });
9
+ }
10
+ /**
11
+ * Soft allowlist check for profile stages (agent-browser refuses
12
+ * --allowed-domains with --profile). The only cheap signal is the persisted
13
+ * bash tool activity: it sees explicit `agent-browser open|goto|navigate <url>`
14
+ * commands, not redirects, link clicks or in-page navigation. Hosts only are
15
+ * recorded, never full URLs.
16
+ */
17
+ export async function auditStageNavigations(policy, events, sink) {
18
+ if (policy.allow_domains.length === 0)
19
+ return;
20
+ const base = {
21
+ runId: policy.runId,
22
+ stageId: policy.stageId,
23
+ ...(policy.profile !== undefined ? { profile: policy.profile } : {}),
24
+ };
25
+ let log;
26
+ try {
27
+ if (events === undefined)
28
+ throw new Error("no activity log available");
29
+ log = await events();
30
+ }
31
+ catch {
32
+ await safeAudit(sink, {
33
+ event: "allowlist_unverified",
34
+ ...base,
35
+ reason: "stage activity log unavailable; navigations were not checked",
36
+ });
37
+ return;
38
+ }
39
+ const seen = new Set();
40
+ for (const event of log) {
41
+ if (event.event !== "tool_start" || !event.argsPreview)
42
+ continue;
43
+ for (const match of event.argsPreview.matchAll(NAV_COMMAND)) {
44
+ const host = hostOf(match[1] ?? "");
45
+ if (host === undefined || allowed(host, policy.allow_domains))
46
+ continue;
47
+ if (seen.has(host))
48
+ continue;
49
+ seen.add(host);
50
+ await safeAudit(sink, { event: "navigation_outside_allowlist", ...base, host });
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ import type { BrowserEnv } from "./browserHost.js";
2
+ /** Reads a persisted browser env; undefined when absent, throws on other I/O errors. */
3
+ export declare function readPersistedBrowserEnv(file: string): Promise<BrowserEnv | undefined>;