@oscharko-dev/keiko 0.1.0-beta.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 (450) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +621 -0
  4. package/TRADEMARKS.md +41 -0
  5. package/dist/audit/aggregate.d.ts +5 -0
  6. package/dist/audit/aggregate.js +25 -0
  7. package/dist/audit/build.d.ts +2 -0
  8. package/dist/audit/build.js +224 -0
  9. package/dist/audit/errors.d.ts +25 -0
  10. package/dist/audit/errors.js +39 -0
  11. package/dist/audit/index-api.d.ts +14 -0
  12. package/dist/audit/index-api.js +131 -0
  13. package/dist/audit/index.d.ts +12 -0
  14. package/dist/audit/index.js +17 -0
  15. package/dist/audit/persist.d.ts +8 -0
  16. package/dist/audit/persist.js +40 -0
  17. package/dist/audit/redaction.d.ts +3 -0
  18. package/dist/audit/redaction.js +61 -0
  19. package/dist/audit/report.d.ts +18 -0
  20. package/dist/audit/report.js +50 -0
  21. package/dist/audit/retention.d.ts +3 -0
  22. package/dist/audit/retention.js +95 -0
  23. package/dist/audit/runid.d.ts +1 -0
  24. package/dist/audit/runid.js +29 -0
  25. package/dist/audit/side-file.d.ts +12 -0
  26. package/dist/audit/side-file.js +82 -0
  27. package/dist/audit/store.d.ts +12 -0
  28. package/dist/audit/store.js +198 -0
  29. package/dist/audit/types.d.ts +188 -0
  30. package/dist/audit/types.js +8 -0
  31. package/dist/audit/workflow-evidence.d.ts +27 -0
  32. package/dist/audit/workflow-evidence.js +145 -0
  33. package/dist/cli/context.d.ts +2 -0
  34. package/dist/cli/context.js +102 -0
  35. package/dist/cli/evaluate.d.ts +7 -0
  36. package/dist/cli/evaluate.js +207 -0
  37. package/dist/cli/evidence.d.ts +8 -0
  38. package/dist/cli/evidence.js +88 -0
  39. package/dist/cli/gateway-config.d.ts +10 -0
  40. package/dist/cli/gateway-config.js +12 -0
  41. package/dist/cli/gen-tests.d.ts +7 -0
  42. package/dist/cli/gen-tests.js +208 -0
  43. package/dist/cli/index.d.ts +2 -0
  44. package/dist/cli/index.js +14 -0
  45. package/dist/cli/investigate.d.ts +8 -0
  46. package/dist/cli/investigate.js +242 -0
  47. package/dist/cli/models.d.ts +3 -0
  48. package/dist/cli/models.js +64 -0
  49. package/dist/cli/run.d.ts +7 -0
  50. package/dist/cli/run.js +187 -0
  51. package/dist/cli/runner.d.ts +6 -0
  52. package/dist/cli/runner.js +83 -0
  53. package/dist/cli/ui.d.ts +31 -0
  54. package/dist/cli/ui.js +240 -0
  55. package/dist/cli/verify.d.ts +2 -0
  56. package/dist/cli/verify.js +103 -0
  57. package/dist/evaluations/fixtures/bug-investigation/happy-path.d.ts +2 -0
  58. package/dist/evaluations/fixtures/bug-investigation/happy-path.js +66 -0
  59. package/dist/evaluations/fixtures/bug-investigation/investigation-only.d.ts +2 -0
  60. package/dist/evaluations/fixtures/bug-investigation/investigation-only.js +39 -0
  61. package/dist/evaluations/fixtures/bug-investigation/unsafe-action.d.ts +2 -0
  62. package/dist/evaluations/fixtures/bug-investigation/unsafe-action.js +37 -0
  63. package/dist/evaluations/fixtures/index.d.ts +7 -0
  64. package/dist/evaluations/fixtures/index.js +35 -0
  65. package/dist/evaluations/fixtures/support.d.ts +5 -0
  66. package/dist/evaluations/fixtures/support.js +42 -0
  67. package/dist/evaluations/fixtures/unit-tests/happy-path.d.ts +2 -0
  68. package/dist/evaluations/fixtures/unit-tests/happy-path.js +40 -0
  69. package/dist/evaluations/fixtures/unit-tests/retry-then-accept.d.ts +2 -0
  70. package/dist/evaluations/fixtures/unit-tests/retry-then-accept.js +39 -0
  71. package/dist/evaluations/fixtures/unit-tests/unsafe-action.d.ts +2 -0
  72. package/dist/evaluations/fixtures/unit-tests/unsafe-action.js +32 -0
  73. package/dist/evaluations/index.d.ts +12 -0
  74. package/dist/evaluations/index.js +12 -0
  75. package/dist/evaluations/manifest-check.d.ts +1 -0
  76. package/dist/evaluations/manifest-check.js +48 -0
  77. package/dist/evaluations/model-provider.d.ts +12 -0
  78. package/dist/evaluations/model-provider.js +26 -0
  79. package/dist/evaluations/render.d.ts +2 -0
  80. package/dist/evaluations/render.js +59 -0
  81. package/dist/evaluations/runner-support.d.ts +27 -0
  82. package/dist/evaluations/runner-support.js +163 -0
  83. package/dist/evaluations/runner.d.ts +20 -0
  84. package/dist/evaluations/runner.js +174 -0
  85. package/dist/evaluations/scorer.d.ts +14 -0
  86. package/dist/evaluations/scorer.js +131 -0
  87. package/dist/evaluations/scripted-model.d.ts +6 -0
  88. package/dist/evaluations/scripted-model.js +26 -0
  89. package/dist/evaluations/surface-parity.d.ts +2 -0
  90. package/dist/evaluations/surface-parity.js +184 -0
  91. package/dist/evaluations/types.d.ts +74 -0
  92. package/dist/evaluations/types.js +16 -0
  93. package/dist/gateway/capabilities.d.ts +11 -0
  94. package/dist/gateway/capabilities.data.d.ts +2 -0
  95. package/dist/gateway/capabilities.data.js +203 -0
  96. package/dist/gateway/capabilities.js +41 -0
  97. package/dist/gateway/config.d.ts +15 -0
  98. package/dist/gateway/config.js +154 -0
  99. package/dist/gateway/errors.d.ts +72 -0
  100. package/dist/gateway/errors.js +82 -0
  101. package/dist/gateway/gateway.d.ts +19 -0
  102. package/dist/gateway/gateway.js +94 -0
  103. package/dist/gateway/index.d.ts +10 -0
  104. package/dist/gateway/index.js +11 -0
  105. package/dist/gateway/model-selection.d.ts +9 -0
  106. package/dist/gateway/model-selection.js +36 -0
  107. package/dist/gateway/normalize.d.ts +7 -0
  108. package/dist/gateway/normalize.js +93 -0
  109. package/dist/gateway/openai-adapter.d.ts +20 -0
  110. package/dist/gateway/openai-adapter.js +263 -0
  111. package/dist/gateway/redaction.d.ts +1 -0
  112. package/dist/gateway/redaction.js +51 -0
  113. package/dist/gateway/resilience.d.ts +24 -0
  114. package/dist/gateway/resilience.js +166 -0
  115. package/dist/gateway/types.d.ts +108 -0
  116. package/dist/gateway/types.js +2 -0
  117. package/dist/harness/adapters.d.ts +23 -0
  118. package/dist/harness/adapters.js +38 -0
  119. package/dist/harness/context.d.ts +33 -0
  120. package/dist/harness/context.js +21 -0
  121. package/dist/harness/emitter.d.ts +15 -0
  122. package/dist/harness/emitter.js +72 -0
  123. package/dist/harness/errors.d.ts +21 -0
  124. package/dist/harness/errors.js +39 -0
  125. package/dist/harness/executor.d.ts +3 -0
  126. package/dist/harness/executor.js +211 -0
  127. package/dist/harness/fingerprint.d.ts +6 -0
  128. package/dist/harness/fingerprint.js +43 -0
  129. package/dist/harness/index.d.ts +9 -0
  130. package/dist/harness/index.js +13 -0
  131. package/dist/harness/loop.d.ts +3 -0
  132. package/dist/harness/loop.js +159 -0
  133. package/dist/harness/patcher.d.ts +4 -0
  134. package/dist/harness/patcher.js +49 -0
  135. package/dist/harness/planner.d.ts +3 -0
  136. package/dist/harness/planner.js +21 -0
  137. package/dist/harness/ports.d.ts +61 -0
  138. package/dist/harness/ports.js +4 -0
  139. package/dist/harness/session.d.ts +25 -0
  140. package/dist/harness/session.js +116 -0
  141. package/dist/harness/sinks.d.ts +30 -0
  142. package/dist/harness/sinks.js +72 -0
  143. package/dist/harness/tasks/explain-plan.d.ts +3 -0
  144. package/dist/harness/tasks/explain-plan.js +29 -0
  145. package/dist/harness/tasks/generate-unit-tests.d.ts +3 -0
  146. package/dist/harness/tasks/generate-unit-tests.js +28 -0
  147. package/dist/harness/tasks/investigate-bug.d.ts +3 -0
  148. package/dist/harness/tasks/investigate-bug.js +31 -0
  149. package/dist/harness/tasks/policy.d.ts +11 -0
  150. package/dist/harness/tasks/policy.js +22 -0
  151. package/dist/harness/tasks/verify.d.ts +3 -0
  152. package/dist/harness/tasks/verify.js +16 -0
  153. package/dist/harness/types.d.ts +270 -0
  154. package/dist/harness/types.js +33 -0
  155. package/dist/index.d.ts +11 -0
  156. package/dist/index.js +36 -0
  157. package/dist/sdk/index.d.ts +9 -0
  158. package/dist/sdk/index.js +37 -0
  159. package/dist/sdk/run-agent.d.ts +16 -0
  160. package/dist/sdk/run-agent.js +56 -0
  161. package/dist/tools/browser/cdp-client.d.ts +35 -0
  162. package/dist/tools/browser/cdp-client.js +218 -0
  163. package/dist/tools/browser/errors.d.ts +25 -0
  164. package/dist/tools/browser/errors.js +55 -0
  165. package/dist/tools/browser/index.d.ts +5 -0
  166. package/dist/tools/browser/index.js +6 -0
  167. package/dist/tools/browser/session.d.ts +44 -0
  168. package/dist/tools/browser/session.js +748 -0
  169. package/dist/tools/browser/types.d.ts +48 -0
  170. package/dist/tools/browser/types.js +2 -0
  171. package/dist/tools/browser/validators.d.ts +5 -0
  172. package/dist/tools/browser/validators.js +97 -0
  173. package/dist/tools/errors.d.ts +59 -0
  174. package/dist/tools/errors.js +94 -0
  175. package/dist/tools/exec.d.ts +42 -0
  176. package/dist/tools/exec.js +327 -0
  177. package/dist/tools/index.d.ts +11 -0
  178. package/dist/tools/index.js +14 -0
  179. package/dist/tools/patch-content.d.ts +10 -0
  180. package/dist/tools/patch-content.js +126 -0
  181. package/dist/tools/patch-normalize.d.ts +1 -0
  182. package/dist/tools/patch-normalize.js +80 -0
  183. package/dist/tools/patch-parse.d.ts +8 -0
  184. package/dist/tools/patch-parse.js +201 -0
  185. package/dist/tools/patch.d.ts +18 -0
  186. package/dist/tools/patch.js +403 -0
  187. package/dist/tools/registry.d.ts +36 -0
  188. package/dist/tools/registry.js +231 -0
  189. package/dist/tools/sandbox.d.ts +8 -0
  190. package/dist/tools/sandbox.js +121 -0
  191. package/dist/tools/schemas.d.ts +2 -0
  192. package/dist/tools/schemas.js +51 -0
  193. package/dist/tools/terminal-policy.d.ts +9 -0
  194. package/dist/tools/terminal-policy.js +313 -0
  195. package/dist/tools/types.d.ts +99 -0
  196. package/dist/tools/types.js +103 -0
  197. package/dist/tools/writer.d.ts +7 -0
  198. package/dist/tools/writer.js +20 -0
  199. package/dist/ui/browser.d.ts +10 -0
  200. package/dist/ui/browser.js +231 -0
  201. package/dist/ui/chat-handlers.d.ts +4 -0
  202. package/dist/ui/chat-handlers.js +281 -0
  203. package/dist/ui/csp-hashes.json +17 -0
  204. package/dist/ui/csp.d.ts +2 -0
  205. package/dist/ui/csp.js +66 -0
  206. package/dist/ui/deps.d.ts +34 -0
  207. package/dist/ui/deps.js +137 -0
  208. package/dist/ui/evidence.d.ts +27 -0
  209. package/dist/ui/evidence.js +142 -0
  210. package/dist/ui/files-deny.d.ts +2 -0
  211. package/dist/ui/files-deny.js +12 -0
  212. package/dist/ui/files.d.ts +65 -0
  213. package/dist/ui/files.js +492 -0
  214. package/dist/ui/headers.d.ts +2 -0
  215. package/dist/ui/headers.js +21 -0
  216. package/dist/ui/host-check.d.ts +2 -0
  217. package/dist/ui/host-check.js +58 -0
  218. package/dist/ui/index.d.ts +20 -0
  219. package/dist/ui/index.js +23 -0
  220. package/dist/ui/load-csp.d.ts +1 -0
  221. package/dist/ui/load-csp.js +28 -0
  222. package/dist/ui/read-handlers.d.ts +8 -0
  223. package/dist/ui/read-handlers.js +247 -0
  224. package/dist/ui/routes.d.ts +36 -0
  225. package/dist/ui/routes.js +129 -0
  226. package/dist/ui/run-engine.d.ts +20 -0
  227. package/dist/ui/run-engine.js +345 -0
  228. package/dist/ui/run-handlers.d.ts +8 -0
  229. package/dist/ui/run-handlers.js +431 -0
  230. package/dist/ui/run-request.d.ts +13 -0
  231. package/dist/ui/run-request.js +219 -0
  232. package/dist/ui/runs.d.ts +43 -0
  233. package/dist/ui/runs.js +92 -0
  234. package/dist/ui/server.d.ts +11 -0
  235. package/dist/ui/server.js +143 -0
  236. package/dist/ui/sink.d.ts +27 -0
  237. package/dist/ui/sink.js +80 -0
  238. package/dist/ui/sse.d.ts +7 -0
  239. package/dist/ui/sse.js +27 -0
  240. package/dist/ui/static/404.html +1 -0
  241. package/dist/ui/static/_next/static/ca-A01hy9W98aRvMZKdAw/_buildManifest.js +1 -0
  242. package/dist/ui/static/_next/static/ca-A01hy9W98aRvMZKdAw/_ssgManifest.js +1 -0
  243. package/dist/ui/static/_next/static/chunks/255-d47fd57964443afe.js +1 -0
  244. package/dist/ui/static/_next/static/chunks/4-be1fef693af8e088.js +1 -0
  245. package/dist/ui/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
  246. package/dist/ui/static/_next/static/chunks/app/_not-found/page-75825b09bcecad97.js +1 -0
  247. package/dist/ui/static/_next/static/chunks/app/launch/page-9c86a13c29884245.js +1 -0
  248. package/dist/ui/static/_next/static/chunks/app/layout-bdea63fe87947d50.js +1 -0
  249. package/dist/ui/static/_next/static/chunks/app/page-4168c12c68b7a853.js +1 -0
  250. package/dist/ui/static/_next/static/chunks/framework-a6e0b7e30f98059a.js +1 -0
  251. package/dist/ui/static/_next/static/chunks/main-778a50aebff02192.js +1 -0
  252. package/dist/ui/static/_next/static/chunks/main-app-30679af7240d63e9.js +1 -0
  253. package/dist/ui/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  254. package/dist/ui/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  255. package/dist/ui/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  256. package/dist/ui/static/_next/static/chunks/webpack-4a462cecab786e93.js +1 -0
  257. package/dist/ui/static/_next/static/css/be7cb54d5c5673b6.css +1 -0
  258. package/dist/ui/static/assets/editors/goland.svg +35 -0
  259. package/dist/ui/static/assets/editors/intellij.svg +39 -0
  260. package/dist/ui/static/assets/editors/pycharm.svg +58 -0
  261. package/dist/ui/static/assets/editors/rustrover.svg +19 -0
  262. package/dist/ui/static/assets/editors/vscode.svg +1 -0
  263. package/dist/ui/static/assets/editors/webstorm.svg +21 -0
  264. package/dist/ui/static/assets/icons/anthropic.svg +1 -0
  265. package/dist/ui/static/assets/icons/brave.svg +1 -0
  266. package/dist/ui/static/assets/icons/css3.svg +1 -0
  267. package/dist/ui/static/assets/icons/docker.svg +1 -0
  268. package/dist/ui/static/assets/icons/git.svg +1 -0
  269. package/dist/ui/static/assets/icons/github.svg +1 -0
  270. package/dist/ui/static/assets/icons/go.svg +1 -0
  271. package/dist/ui/static/assets/icons/gradle.svg +1 -0
  272. package/dist/ui/static/assets/icons/grafana.svg +1 -0
  273. package/dist/ui/static/assets/icons/graphql.svg +1 -0
  274. package/dist/ui/static/assets/icons/html5.svg +1 -0
  275. package/dist/ui/static/assets/icons/image.svg +1 -0
  276. package/dist/ui/static/assets/icons/java.svg +1 -0
  277. package/dist/ui/static/assets/icons/javascript.svg +1 -0
  278. package/dist/ui/static/assets/icons/json.svg +1 -0
  279. package/dist/ui/static/assets/icons/kafka.svg +1 -0
  280. package/dist/ui/static/assets/icons/kubernetes.svg +1 -0
  281. package/dist/ui/static/assets/icons/linear.svg +1 -0
  282. package/dist/ui/static/assets/icons/markdown.svg +1 -0
  283. package/dist/ui/static/assets/icons/nginx.svg +1 -0
  284. package/dist/ui/static/assets/icons/nodejs.svg +1 -0
  285. package/dist/ui/static/assets/icons/notion.svg +1 -0
  286. package/dist/ui/static/assets/icons/openai.svg +1 -0
  287. package/dist/ui/static/assets/icons/playwright.svg +1 -0
  288. package/dist/ui/static/assets/icons/postgresql.svg +1 -0
  289. package/dist/ui/static/assets/icons/prometheus.svg +1 -0
  290. package/dist/ui/static/assets/icons/properties.svg +1 -0
  291. package/dist/ui/static/assets/icons/puppeteer.svg +1 -0
  292. package/dist/ui/static/assets/icons/python.svg +1 -0
  293. package/dist/ui/static/assets/icons/react.svg +1 -0
  294. package/dist/ui/static/assets/icons/redis.svg +1 -0
  295. package/dist/ui/static/assets/icons/rust.svg +1 -0
  296. package/dist/ui/static/assets/icons/sentry.svg +1 -0
  297. package/dist/ui/static/assets/icons/slack.svg +1 -0
  298. package/dist/ui/static/assets/icons/spring.svg +1 -0
  299. package/dist/ui/static/assets/icons/typescript.svg +1 -0
  300. package/dist/ui/static/assets/icons/upstash.svg +1 -0
  301. package/dist/ui/static/assets/icons/yaml.svg +1 -0
  302. package/dist/ui/static/assets/keiko-logo.svg +10 -0
  303. package/dist/ui/static/index.html +1 -0
  304. package/dist/ui/static/index.txt +19 -0
  305. package/dist/ui/static/keiko-logo.svg +10 -0
  306. package/dist/ui/static/launch.html +1 -0
  307. package/dist/ui/static/launch.txt +19 -0
  308. package/dist/ui/static.d.ts +3 -0
  309. package/dist/ui/static.js +72 -0
  310. package/dist/ui/store/chats.d.ts +14 -0
  311. package/dist/ui/store/chats.js +110 -0
  312. package/dist/ui/store/db.d.ts +6 -0
  313. package/dist/ui/store/db.js +182 -0
  314. package/dist/ui/store/errors.d.ts +12 -0
  315. package/dist/ui/store/errors.js +30 -0
  316. package/dist/ui/store/index.d.ts +6 -0
  317. package/dist/ui/store/index.js +6 -0
  318. package/dist/ui/store/messages.d.ts +5 -0
  319. package/dist/ui/store/messages.js +137 -0
  320. package/dist/ui/store/paths.d.ts +4 -0
  321. package/dist/ui/store/paths.js +69 -0
  322. package/dist/ui/store/projects.d.ts +7 -0
  323. package/dist/ui/store/projects.js +61 -0
  324. package/dist/ui/store/schema.d.ts +3 -0
  325. package/dist/ui/store/schema.js +77 -0
  326. package/dist/ui/store/types.d.ts +80 -0
  327. package/dist/ui/store/types.js +3 -0
  328. package/dist/ui/store/validation.d.ts +4 -0
  329. package/dist/ui/store/validation.js +72 -0
  330. package/dist/ui/store-handlers.d.ts +16 -0
  331. package/dist/ui/store-handlers.js +465 -0
  332. package/dist/ui/terminal-errors.d.ts +21 -0
  333. package/dist/ui/terminal-errors.js +45 -0
  334. package/dist/ui/terminal-evidence.d.ts +20 -0
  335. package/dist/ui/terminal-evidence.js +65 -0
  336. package/dist/ui/terminal-routes.d.ts +9 -0
  337. package/dist/ui/terminal-routes.js +219 -0
  338. package/dist/ui/terminal.d.ts +67 -0
  339. package/dist/ui/terminal.js +835 -0
  340. package/dist/verification/classify.d.ts +10 -0
  341. package/dist/verification/classify.js +53 -0
  342. package/dist/verification/detect.d.ts +4 -0
  343. package/dist/verification/detect.js +81 -0
  344. package/dist/verification/errors.d.ts +11 -0
  345. package/dist/verification/errors.js +21 -0
  346. package/dist/verification/index.d.ts +17 -0
  347. package/dist/verification/index.js +13 -0
  348. package/dist/verification/limits.d.ts +3 -0
  349. package/dist/verification/limits.js +40 -0
  350. package/dist/verification/monitor.d.ts +4 -0
  351. package/dist/verification/monitor.js +58 -0
  352. package/dist/verification/orchestrator.d.ts +16 -0
  353. package/dist/verification/orchestrator.js +363 -0
  354. package/dist/verification/plan.d.ts +9 -0
  355. package/dist/verification/plan.js +125 -0
  356. package/dist/verification/summary.d.ts +40 -0
  357. package/dist/verification/summary.js +67 -0
  358. package/dist/verification/types.d.ts +63 -0
  359. package/dist/verification/types.js +13 -0
  360. package/dist/workflows/bug-investigation/context.d.ts +7 -0
  361. package/dist/workflows/bug-investigation/context.js +119 -0
  362. package/dist/workflows/bug-investigation/descriptor.d.ts +3 -0
  363. package/dist/workflows/bug-investigation/descriptor.js +46 -0
  364. package/dist/workflows/bug-investigation/emit.d.ts +12 -0
  365. package/dist/workflows/bug-investigation/emit.js +35 -0
  366. package/dist/workflows/bug-investigation/events.d.ts +81 -0
  367. package/dist/workflows/bug-investigation/events.js +9 -0
  368. package/dist/workflows/bug-investigation/failure-parse.d.ts +3 -0
  369. package/dist/workflows/bug-investigation/failure-parse.js +154 -0
  370. package/dist/workflows/bug-investigation/guard.d.ts +2 -0
  371. package/dist/workflows/bug-investigation/guard.js +69 -0
  372. package/dist/workflows/bug-investigation/index.d.ts +7 -0
  373. package/dist/workflows/bug-investigation/index.js +13 -0
  374. package/dist/workflows/bug-investigation/internal.d.ts +37 -0
  375. package/dist/workflows/bug-investigation/internal.js +64 -0
  376. package/dist/workflows/bug-investigation/model-loop.d.ts +4 -0
  377. package/dist/workflows/bug-investigation/model-loop.js +223 -0
  378. package/dist/workflows/bug-investigation/parse.d.ts +3 -0
  379. package/dist/workflows/bug-investigation/parse.js +123 -0
  380. package/dist/workflows/bug-investigation/prompt.d.ts +4 -0
  381. package/dist/workflows/bug-investigation/prompt.js +107 -0
  382. package/dist/workflows/bug-investigation/report.d.ts +23 -0
  383. package/dist/workflows/bug-investigation/report.js +151 -0
  384. package/dist/workflows/bug-investigation/stages.d.ts +13 -0
  385. package/dist/workflows/bug-investigation/stages.js +242 -0
  386. package/dist/workflows/bug-investigation/types.d.ts +91 -0
  387. package/dist/workflows/bug-investigation/types.js +14 -0
  388. package/dist/workflows/bug-investigation/verify-stage.d.ts +10 -0
  389. package/dist/workflows/bug-investigation/verify-stage.js +91 -0
  390. package/dist/workflows/bug-investigation/workflow.d.ts +2 -0
  391. package/dist/workflows/bug-investigation/workflow.js +74 -0
  392. package/dist/workflows/descriptor.d.ts +20 -0
  393. package/dist/workflows/descriptor.js +8 -0
  394. package/dist/workflows/index.d.ts +3 -0
  395. package/dist/workflows/index.js +2 -0
  396. package/dist/workflows/unit-tests/context.d.ts +7 -0
  397. package/dist/workflows/unit-tests/context.js +129 -0
  398. package/dist/workflows/unit-tests/conventions.d.ts +4 -0
  399. package/dist/workflows/unit-tests/conventions.js +87 -0
  400. package/dist/workflows/unit-tests/descriptor.d.ts +4 -0
  401. package/dist/workflows/unit-tests/descriptor.js +43 -0
  402. package/dist/workflows/unit-tests/emit.d.ts +12 -0
  403. package/dist/workflows/unit-tests/emit.js +35 -0
  404. package/dist/workflows/unit-tests/events.d.ts +78 -0
  405. package/dist/workflows/unit-tests/events.js +7 -0
  406. package/dist/workflows/unit-tests/index.d.ts +6 -0
  407. package/dist/workflows/unit-tests/index.js +10 -0
  408. package/dist/workflows/unit-tests/internal.d.ts +35 -0
  409. package/dist/workflows/unit-tests/internal.js +43 -0
  410. package/dist/workflows/unit-tests/model-loop.d.ts +4 -0
  411. package/dist/workflows/unit-tests/model-loop.js +95 -0
  412. package/dist/workflows/unit-tests/parse.d.ts +6 -0
  413. package/dist/workflows/unit-tests/parse.js +68 -0
  414. package/dist/workflows/unit-tests/prompt.d.ts +4 -0
  415. package/dist/workflows/unit-tests/prompt.js +71 -0
  416. package/dist/workflows/unit-tests/report.d.ts +21 -0
  417. package/dist/workflows/unit-tests/report.js +90 -0
  418. package/dist/workflows/unit-tests/stages.d.ts +9 -0
  419. package/dist/workflows/unit-tests/stages.js +155 -0
  420. package/dist/workflows/unit-tests/types.d.ts +70 -0
  421. package/dist/workflows/unit-tests/types.js +11 -0
  422. package/dist/workflows/unit-tests/verify-stage.d.ts +9 -0
  423. package/dist/workflows/unit-tests/verify-stage.js +56 -0
  424. package/dist/workflows/unit-tests/workflow.d.ts +2 -0
  425. package/dist/workflows/unit-tests/workflow.js +58 -0
  426. package/dist/workspace/contextPack.d.ts +9 -0
  427. package/dist/workspace/contextPack.js +94 -0
  428. package/dist/workspace/detect.d.ts +3 -0
  429. package/dist/workspace/detect.js +135 -0
  430. package/dist/workspace/discovery.d.ts +9 -0
  431. package/dist/workspace/discovery.js +167 -0
  432. package/dist/workspace/errors.d.ts +39 -0
  433. package/dist/workspace/errors.js +66 -0
  434. package/dist/workspace/fs.d.ts +21 -0
  435. package/dist/workspace/fs.js +36 -0
  436. package/dist/workspace/ignore.d.ts +14 -0
  437. package/dist/workspace/ignore.js +176 -0
  438. package/dist/workspace/index.d.ts +11 -0
  439. package/dist/workspace/index.js +13 -0
  440. package/dist/workspace/paths.d.ts +2 -0
  441. package/dist/workspace/paths.js +38 -0
  442. package/dist/workspace/realpath.d.ts +7 -0
  443. package/dist/workspace/realpath.js +72 -0
  444. package/dist/workspace/retrieval.d.ts +9 -0
  445. package/dist/workspace/retrieval.js +74 -0
  446. package/dist/workspace/summary.d.ts +3 -0
  447. package/dist/workspace/summary.js +54 -0
  448. package/dist/workspace/types.d.ts +103 -0
  449. package/dist/workspace/types.js +27 -0
  450. package/package.json +58 -0
@@ -0,0 +1,748 @@
1
+ // ADR-0017 D3/D6/D9 — browser session manager. In-memory Map of sessionId→record, 4-session cap,
2
+ // 30-min idle TTL, fresh Target.createTarget only (never attaches to user tabs), post-navigate
3
+ // frameNavigated origin re-check, dry-run-by-default screenshots, redactor over captured HTML.
4
+ //
5
+ // Security invariants:
6
+ // • webSocketDebuggerUrl from /json/version is validated for loopback host + matching port (H1).
7
+ // • pendingScreenshots per session is capped at MAX_PENDING_SCREENSHOTS (1) with insertion-order
8
+ // eviction to bound memory use (M1).
9
+ // • Session slot is reserved synchronously before any await to prevent TOCTOU on the limit (M2).
10
+ //
11
+ // Composition: validators (M1) + CDP client (M2) + side-file writer (M3) + the existing audit
12
+ // redactor. No new safety primitives — every guard is reused.
13
+ import { randomUUID } from "node:crypto";
14
+ import { CdpClient } from "./cdp-client.js";
15
+ import { BrowserToolError } from "./errors.js";
16
+ import { isLoopbackHost, isLoopbackUrl, normalizeCdpPort, normalizeNavigateUrl, } from "./validators.js";
17
+ import { resolveCostClass } from "../../audit/aggregate.js";
18
+ import { writeSideFile } from "../../audit/side-file.js";
19
+ import { EVIDENCE_SCHEMA_VERSION, } from "../../audit/types.js";
20
+ import { HARNESS_VERSION } from "../../harness/session.js";
21
+ const MAX_SESSIONS = 4;
22
+ const SESSION_IDLE_TTL_MS = 30 * 60 * 1000;
23
+ const MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
24
+ const MAX_CONTENT_BYTES = 2 * 1024 * 1024;
25
+ const MAX_VERSION_BODY_BYTES = 64_000;
26
+ const VERSION_FETCH_TIMEOUT_MS = 5000;
27
+ // M1: cap per-session pending screenshots; oldest (insertion-order) is evicted on overflow.
28
+ const MAX_PENDING_SCREENSHOTS = 1;
29
+ const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
30
+ const FRAGMENT_RECHECK_TIMEOUT_MS = 5000;
31
+ const VERSION_PATH = "/json/version";
32
+ function defaultRedactor(value) {
33
+ return value;
34
+ }
35
+ // H1: validate webSocketDebuggerUrl host+port so a malicious /json/version responder
36
+ // cannot redirect the WebSocket to a non-loopback host (ADR-0017 D2 layer-1).
37
+ function assertWsUrlTrusted(ws, expectedPort) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(ws);
41
+ }
42
+ catch {
43
+ throw new BrowserToolError("CDP_TRANSPORT_REFUSED", "CDP endpoint returned an invalid WebSocket URL.");
44
+ }
45
+ const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]")
46
+ ? parsed.hostname.slice(1, -1)
47
+ : parsed.hostname;
48
+ const port = parsed.port === "" ? (parsed.protocol === "wss:" ? 443 : 80) : Number.parseInt(parsed.port, 10);
49
+ if (!isLoopbackHost(host) || port !== expectedPort) {
50
+ throw new BrowserToolError("CDP_TRANSPORT_REFUSED", "CDP endpoint returned a WebSocket URL that is not on the expected loopback address.");
51
+ }
52
+ }
53
+ function browserWsUrlFromVersion(version, fallbackPort) {
54
+ if (typeof version !== "object" || version === null) {
55
+ return defaultVersionInfo(fallbackPort);
56
+ }
57
+ const rec = version;
58
+ const ws = rec.webSocketDebuggerUrl;
59
+ if (typeof ws !== "string" || !ws.startsWith("ws://")) {
60
+ return defaultVersionInfo(fallbackPort);
61
+ }
62
+ assertWsUrlTrusted(ws, fallbackPort);
63
+ const ua = rec["User-Agent"] ?? rec.userAgent;
64
+ const product = rec.Browser ?? rec.product;
65
+ return {
66
+ url: ws,
67
+ userAgent: typeof ua === "string" ? ua : null,
68
+ browserVersion: typeof product === "string" ? product : null,
69
+ };
70
+ }
71
+ function defaultVersionInfo(port) {
72
+ return {
73
+ url: `ws://127.0.0.1:${String(port)}/devtools/browser`,
74
+ userAgent: null,
75
+ browserVersion: null,
76
+ };
77
+ }
78
+ async function fetchVersionJson(url) {
79
+ const controller = new AbortController();
80
+ const timer = setTimeout(() => {
81
+ controller.abort();
82
+ }, VERSION_FETCH_TIMEOUT_MS);
83
+ try {
84
+ const res = await fetch(url, { redirect: "manual", signal: controller.signal });
85
+ if (res.url !== url || (res.status >= 300 && res.status < 400)) {
86
+ throw new BrowserToolError("CDP_TRANSPORT_REFUSED", "CDP version endpoint redirected.");
87
+ }
88
+ if (!res.ok) {
89
+ throw new BrowserToolError("CHROME_UNREACHABLE", "CDP version endpoint returned a non-200.");
90
+ }
91
+ const text = await readCappedText(res);
92
+ try {
93
+ return JSON.parse(text);
94
+ }
95
+ catch {
96
+ throw new BrowserToolError("CHROME_UNREACHABLE", "CDP version endpoint returned invalid JSON.");
97
+ }
98
+ }
99
+ catch (error) {
100
+ if (error instanceof BrowserToolError)
101
+ throw error;
102
+ throw new BrowserToolError("CHROME_UNREACHABLE", "CDP version endpoint is unreachable.");
103
+ }
104
+ finally {
105
+ clearTimeout(timer);
106
+ }
107
+ }
108
+ function isChromiumUserAgent(userAgent) {
109
+ if (userAgent === null)
110
+ return true;
111
+ // Single-pass linear scan — substring matches only, no regex (ADR-0002 ReDoS gate).
112
+ return (userAgent.includes("Chrome") ||
113
+ userAgent.includes("Chromium") ||
114
+ userAgent.includes("HeadlessChrome"));
115
+ }
116
+ function safeOrigin(url) {
117
+ try {
118
+ return new URL(url).origin;
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ function evidenceStringField(key, value) {
125
+ return typeof value === "string" ? { [key]: value } : {};
126
+ }
127
+ function evidenceNumberField(key, value) {
128
+ return typeof value === "number" ? { [key]: value } : {};
129
+ }
130
+ function evidenceBooleanField(key, value) {
131
+ return typeof value === "boolean" ? { [key]: value } : {};
132
+ }
133
+ function evidenceNullableNumberField(key, value) {
134
+ return typeof value === "number" || value === null ? { [key]: value } : {};
135
+ }
136
+ function browserViewportPayload(value) {
137
+ if (typeof value !== "object" || value === null)
138
+ return undefined;
139
+ const rec = value;
140
+ return typeof rec.width === "number" && typeof rec.height === "number"
141
+ ? { width: rec.width, height: rec.height }
142
+ : undefined;
143
+ }
144
+ function evidenceViewportField(value) {
145
+ const viewportPx = browserViewportPayload(value);
146
+ return viewportPx === undefined ? {} : { viewportPx };
147
+ }
148
+ function isSubframeNavigation(params) {
149
+ return typeof params.frame?.parentId === "string" && params.frame.parentId.length > 0;
150
+ }
151
+ function mainFrameUrl(params) {
152
+ const url = params.frame?.url;
153
+ if (typeof url !== "string" || url.length === 0 || url === "about:blank")
154
+ return null;
155
+ return url;
156
+ }
157
+ async function readCappedText(res) {
158
+ if (res.body === null)
159
+ return "";
160
+ const reader = res.body.getReader();
161
+ const chunks = [];
162
+ let total = 0;
163
+ for (;;) {
164
+ const { done, value } = await reader.read();
165
+ if (done)
166
+ break;
167
+ total += value.byteLength;
168
+ if (total > MAX_VERSION_BODY_BYTES) {
169
+ await reader.cancel().catch(() => undefined);
170
+ throw new BrowserToolError("PAYLOAD_TOO_LARGE", "CDP version response exceeds the size limit.");
171
+ }
172
+ chunks.push(value);
173
+ }
174
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
175
+ }
176
+ // The class form keeps each method small (max-lines-per-function gate) while sharing one private
177
+ // state region. Method names match the BrowserSessionManager port shape.
178
+ class BrowserSessionManagerImpl {
179
+ opts;
180
+ redactor;
181
+ clientFactory;
182
+ fetchVersion;
183
+ idleTtlMs;
184
+ nowMs;
185
+ sessions = new Map();
186
+ subscribers = new Map();
187
+ counters = { navigations: 0 };
188
+ constructor(opts) {
189
+ this.opts = opts;
190
+ this.redactor = opts.redactor ?? defaultRedactor;
191
+ this.clientFactory =
192
+ opts.cdpClientFactory ?? ((url, options) => new CdpClient(url, options));
193
+ this.fetchVersion = opts.fetchVersion ?? fetchVersionJson;
194
+ this.idleTtlMs = opts.idleTtlMs ?? SESSION_IDLE_TTL_MS;
195
+ this.nowMs = opts.nowMs ?? (() => Date.now());
196
+ }
197
+ toEvidenceEvent(event) {
198
+ const p = event.payload;
199
+ return {
200
+ schemaVersion: "1",
201
+ type: event.type,
202
+ sessionId: event.sessionId,
203
+ seq: event.seq,
204
+ ts: event.ts,
205
+ ...evidenceStringField("originOnly", p.originOnly),
206
+ ...evidenceNullableNumberField("httpStatus", p.httpStatus),
207
+ ...evidenceNumberField("captureSeq", p.captureSeq),
208
+ ...evidenceBooleanField("persisted", p.persisted),
209
+ ...evidenceViewportField(p.viewportPx),
210
+ ...evidenceStringField("path", p.path),
211
+ ...evidenceStringField("sha256", p.sha256),
212
+ ...evidenceNumberField("bytes", p.bytes),
213
+ ...evidenceNumberField("byteLength", p.byteLength),
214
+ ...evidenceStringField("reason", p.reason),
215
+ ...evidenceStringField("warning", p.warning),
216
+ ...evidenceStringField("code", p.code),
217
+ ...evidenceStringField("message", p.message),
218
+ };
219
+ }
220
+ buildManifest(record) {
221
+ const finishedAt = record.closedAt ?? this.nowMs();
222
+ const manifest = {
223
+ evidenceSchemaVersion: EVIDENCE_SCHEMA_VERSION,
224
+ run: {
225
+ runId: record.runId,
226
+ fingerprint: record.fingerprint,
227
+ harnessVersion: HARNESS_VERSION,
228
+ taskType: "browser-capture",
229
+ outcome: "completed",
230
+ startedAt: record.startedAt,
231
+ finishedAt,
232
+ durationMs: Math.max(0, finishedAt - record.startedAt),
233
+ },
234
+ model: { modelId: "browser-tool", costClass: resolveCostClass("browser-tool") },
235
+ usageTotals: { promptTokens: 0, completionTokens: 0, requestCount: 0, totalLatencyMs: 0 },
236
+ stateTransitions: [],
237
+ toolCalls: [],
238
+ commandExecutions: [],
239
+ browser: {
240
+ sessionId: record.id,
241
+ cdpPort: record.cdpPort,
242
+ targetId: record.targetId,
243
+ status: record.closed ? "closed" : "open",
244
+ startedAt: record.startedAt,
245
+ ...(record.closedAt === undefined ? {} : { closedAt: record.closedAt }),
246
+ ...(record.closeReason === undefined ? {} : { closeReason: record.closeReason }),
247
+ ...(record.lastOriginOnly === null ? {} : { lastOriginOnly: record.lastOriginOnly }),
248
+ events: record.auditEvents,
249
+ ...(record.screenshots.length === 0 ? {} : { screenshots: record.screenshots }),
250
+ ...(record.contentCaptures.length === 0 ? {} : { contentCaptures: record.contentCaptures }),
251
+ },
252
+ };
253
+ const redacted = this.redactor(manifest);
254
+ return typeof redacted === "object" && redacted !== null
255
+ ? redacted
256
+ : manifest;
257
+ }
258
+ persistRecord(record) {
259
+ const store = this.opts.evidenceStore;
260
+ if (store === undefined)
261
+ return;
262
+ const manifest = this.buildManifest(record);
263
+ store.put(record.runId, JSON.stringify(manifest, null, 2));
264
+ }
265
+ emitRecord(record, kind, payload, ts = this.nowMs()) {
266
+ record.auditSeq += 1;
267
+ const event = {
268
+ schemaVersion: "1",
269
+ type: `browser:${kind}`,
270
+ runId: record.runId,
271
+ fingerprint: record.fingerprint,
272
+ seq: record.auditSeq,
273
+ ts,
274
+ kind,
275
+ sessionId: record.id,
276
+ payload,
277
+ };
278
+ record.auditEvents.push(this.toEvidenceEvent(event));
279
+ this.persistRecord(record);
280
+ this.fanout(event);
281
+ return event;
282
+ }
283
+ emitErrorRecord(record, code, message) {
284
+ this.emitRecord(record, "error", { code, message });
285
+ }
286
+ async runSessionAction(sessionId, action) {
287
+ const record = this.requireRecord(sessionId);
288
+ try {
289
+ return await action(record);
290
+ }
291
+ catch (error) {
292
+ if (error instanceof BrowserToolError) {
293
+ this.emitErrorRecord(record, error.code, error.message);
294
+ }
295
+ throw error;
296
+ }
297
+ }
298
+ checkStatus = async (cdpPort) => {
299
+ normalizeCdpPort(cdpPort);
300
+ try {
301
+ const url = `http://127.0.0.1:${String(cdpPort)}${VERSION_PATH}`;
302
+ const version = await this.fetchVersion(url);
303
+ const meta = browserWsUrlFromVersion(version, cdpPort);
304
+ if (!isChromiumUserAgent(meta.userAgent)) {
305
+ throw new BrowserToolError("CHROME_VERSION_MISMATCH", "CDP endpoint is not a Chromium-based browser.");
306
+ }
307
+ return {
308
+ reachable: true,
309
+ userAgent: meta.userAgent,
310
+ browserVersion: meta.browserVersion,
311
+ webSocketDebuggerUrl: meta.url,
312
+ };
313
+ }
314
+ catch (error) {
315
+ if (error instanceof BrowserToolError)
316
+ throw error;
317
+ return {
318
+ reachable: false,
319
+ userAgent: null,
320
+ browserVersion: null,
321
+ webSocketDebuggerUrl: null,
322
+ };
323
+ }
324
+ };
325
+ buildPlaceholder(id, cdpPort) {
326
+ // A closed sentinel record that reserves the sessions slot without an active CdpClient.
327
+ // It is replaced by the live record inside attachAndRegister once the CDP handshake
328
+ // completes. Because closed=true, requireRecord and closeSession treat it as unavailable.
329
+ const noop = () => undefined;
330
+ const fakeClient = {
331
+ connect: () => Promise.resolve(),
332
+ send: () => Promise.resolve({}),
333
+ onEvent: () => noop,
334
+ onClose: () => noop,
335
+ close: noop,
336
+ isClosed: () => true,
337
+ closeCause: () => undefined,
338
+ };
339
+ const now = this.nowMs();
340
+ return {
341
+ id,
342
+ cdpPort,
343
+ targetId: "",
344
+ client: fakeClient,
345
+ cdpSessionId: "",
346
+ runId: id.replace(/-/g, ""),
347
+ fingerprint: `browser-${id.replace(/-/g, "")}`,
348
+ startedAt: now,
349
+ closedAt: undefined,
350
+ closeReason: undefined,
351
+ lastUrl: null,
352
+ lastOriginOnly: null,
353
+ expectedOriginOnly: null,
354
+ originAllowed: false,
355
+ captureSeq: 0,
356
+ auditSeq: 0,
357
+ auditEvents: [],
358
+ screenshots: [],
359
+ contentCaptures: [],
360
+ pendingScreenshots: new Map(),
361
+ idleTimer: undefined,
362
+ lastTouchedMs: now,
363
+ closed: true,
364
+ removeCdpListener: noop,
365
+ removeCdpCloseListener: noop,
366
+ };
367
+ }
368
+ openSession = async (cdpPort) => {
369
+ normalizeCdpPort(cdpPort);
370
+ // M2: reserve the slot synchronously before any await, closing the TOCTOU window where
371
+ // concurrent calls could each pass the size check and then all succeed.
372
+ if (this.sessions.size >= MAX_SESSIONS) {
373
+ throw new BrowserToolError("SESSION_LIMIT_EXCEEDED", "Too many active browser sessions.");
374
+ }
375
+ const reservedId = randomUUID();
376
+ const placeholder = this.buildPlaceholder(reservedId, cdpPort);
377
+ this.sessions.set(reservedId, placeholder);
378
+ try {
379
+ const versionInfo = await this.checkStatus(cdpPort);
380
+ if (!versionInfo.reachable || versionInfo.webSocketDebuggerUrl === null) {
381
+ throw new BrowserToolError("CHROME_UNREACHABLE", "CDP endpoint is not reachable.");
382
+ }
383
+ const client = this.clientFactory(versionInfo.webSocketDebuggerUrl, {});
384
+ try {
385
+ return await this.attachAndRegister(client, cdpPort, reservedId);
386
+ }
387
+ catch (error) {
388
+ client.close();
389
+ if (error instanceof BrowserToolError)
390
+ throw error;
391
+ throw new BrowserToolError("CHROME_UNREACHABLE", "Failed to open browser session.");
392
+ }
393
+ }
394
+ catch (error) {
395
+ this.sessions.delete(reservedId);
396
+ throw error;
397
+ }
398
+ };
399
+ async attachAndRegister(client, cdpPort, reservedId) {
400
+ await client.connect();
401
+ const target = await client.send("Target.createTarget", {
402
+ url: "about:blank",
403
+ });
404
+ const attach = await client.send("Target.attachToTarget", {
405
+ targetId: target.targetId,
406
+ flatten: true,
407
+ });
408
+ const record = this.buildRecord(client, cdpPort, target.targetId, attach.sessionId, reservedId);
409
+ record.removeCdpListener = client.onEvent(this.frameNavigatedListener(record));
410
+ record.removeCdpCloseListener = client.onClose((reason) => {
411
+ if (reason === "explicit")
412
+ return;
413
+ void this.closeRecord(record, "chrome-disconnected", false).catch(() => undefined);
414
+ });
415
+ await client.send("Page.enable", {}, attach.sessionId);
416
+ this.sessions.set(record.id, record);
417
+ this.touch(record);
418
+ await this.emitTrustWarningForProfileMetadata(client, record);
419
+ this.emitRecord(record, "session-opened", { cdpPort, targetId: target.targetId });
420
+ return {
421
+ sessionId: record.id,
422
+ cdpPort,
423
+ targetId: target.targetId,
424
+ status: "open",
425
+ createdAt: this.nowMs(),
426
+ };
427
+ }
428
+ buildRecord(client, cdpPort, targetId, cdpSessionId, id = randomUUID()) {
429
+ const runId = id.replace(/-/g, "");
430
+ const now = this.nowMs();
431
+ return {
432
+ id,
433
+ cdpPort,
434
+ targetId,
435
+ client,
436
+ cdpSessionId,
437
+ runId,
438
+ fingerprint: `browser-${runId}`,
439
+ startedAt: now,
440
+ closedAt: undefined,
441
+ closeReason: undefined,
442
+ lastUrl: null,
443
+ lastOriginOnly: null,
444
+ expectedOriginOnly: null,
445
+ originAllowed: false,
446
+ captureSeq: 0,
447
+ auditSeq: 0,
448
+ auditEvents: [],
449
+ screenshots: [],
450
+ contentCaptures: [],
451
+ pendingScreenshots: new Map(),
452
+ idleTimer: undefined,
453
+ lastTouchedMs: now,
454
+ closed: false,
455
+ removeCdpListener: () => undefined,
456
+ removeCdpCloseListener: () => undefined,
457
+ };
458
+ }
459
+ async emitTrustWarningForProfileMetadata(client, record) {
460
+ const browser = await client
461
+ .send("Browser.getVersion")
462
+ .catch(() => null);
463
+ const commandLine = browser?.commandLine;
464
+ if (typeof commandLine === "string" && commandLine.includes("--user-data-dir=")) {
465
+ return;
466
+ }
467
+ const warning = browser !== null &&
468
+ typeof browser.userAgent === "string" &&
469
+ browser.userAgent.includes("Headless")
470
+ ? "Headless Chromium detected; verify ephemeral --user-data-dir."
471
+ : "Chrome command-line metadata unavailable; verify ephemeral --user-data-dir.";
472
+ if (warning.length > 0) {
473
+ this.emitRecord(record, "trust-warning", {
474
+ warning,
475
+ });
476
+ }
477
+ }
478
+ closeSession = async (sessionId) => {
479
+ const record = this.sessions.get(sessionId);
480
+ if (record === undefined || record.closed)
481
+ return;
482
+ await this.closeRecord(record, "explicit", true);
483
+ };
484
+ async closeRecord(record, reason, closeTarget) {
485
+ if (record.closed)
486
+ return;
487
+ record.closed = true;
488
+ record.closedAt = this.nowMs();
489
+ record.closeReason = reason;
490
+ if (record.idleTimer !== undefined)
491
+ clearTimeout(record.idleTimer);
492
+ record.removeCdpListener();
493
+ record.removeCdpCloseListener();
494
+ if (closeTarget) {
495
+ try {
496
+ await record.client.send("Target.closeTarget", { targetId: record.targetId });
497
+ }
498
+ catch {
499
+ // Best-effort cleanup; the BFF is shutting the session down regardless.
500
+ }
501
+ record.client.close();
502
+ }
503
+ this.sessions.delete(record.id);
504
+ try {
505
+ this.emitRecord(record, "session-closed", { reason });
506
+ }
507
+ finally {
508
+ this.subscribers.delete(record.id);
509
+ }
510
+ }
511
+ navigate = async (sessionId, url) => {
512
+ return this.runSessionAction(sessionId, async (record) => {
513
+ const normalized = normalizeNavigateUrl(url);
514
+ this.touch(record);
515
+ record.originAllowed = false;
516
+ record.lastUrl = null;
517
+ record.lastOriginOnly = null;
518
+ record.expectedOriginOnly = normalized.originOnly;
519
+ const cdpStatus = await this.invokeNavigate(record, normalized.url);
520
+ await this.waitForOriginRecheck(record, normalized.originOnly);
521
+ // record.originAllowed is mutated by the async frameNavigated listener; the TS narrower can't
522
+ // see across the awaited recheck so a runtime guard remains required here.
523
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
524
+ if (!record.originAllowed) {
525
+ throw new BrowserToolError("ORIGIN_NOT_ALLOWED", "Post-navigate origin is not on the requested loopback origin.");
526
+ }
527
+ this.counters.navigations += 1;
528
+ this.emitRecord(record, "navigated", {
529
+ originOnly: record.lastOriginOnly,
530
+ httpStatus: cdpStatus,
531
+ });
532
+ return { originOnly: normalized.originOnly, httpStatus: cdpStatus };
533
+ });
534
+ };
535
+ async invokeNavigate(record, url) {
536
+ const result = await record.client.send("Page.navigate", { url }, record.cdpSessionId);
537
+ return typeof result.httpStatus === "number" ? result.httpStatus : null;
538
+ }
539
+ async waitForOriginRecheck(record, expectedOrigin) {
540
+ const deadline = this.nowMs() + FRAGMENT_RECHECK_TIMEOUT_MS;
541
+ while (record.lastUrl === null && this.nowMs() < deadline) {
542
+ await new Promise((resolve) => {
543
+ setTimeout(resolve, 20);
544
+ });
545
+ }
546
+ if (record.lastUrl === null) {
547
+ // No frameNavigated arrived (e.g. a fast 204 or already-stopped load). Trust the pre-navigate
548
+ // validator: the requested URL was loopback, so allow capture against that origin.
549
+ record.originAllowed = true;
550
+ record.lastOriginOnly = expectedOrigin;
551
+ }
552
+ }
553
+ screenshot = async (sessionId) => {
554
+ return this.runSessionAction(sessionId, async (record) => {
555
+ this.requireOriginAllowed(record, "Screenshot");
556
+ this.touch(record);
557
+ const result = await record.client.send("Page.captureScreenshot", { format: "png" }, record.cdpSessionId);
558
+ // Defence in depth against the frameNavigated race: if a redirect arrived while
559
+ // captureScreenshot was in flight, the listener has now run and flipped originAllowed.
560
+ // Discard the bytes rather than hand evidence from a non-loopback origin to the caller.
561
+ if (!record.originAllowed) {
562
+ throw new BrowserToolError("ORIGIN_NOT_ALLOWED", "Screenshot blocked: origin drifted off-loopback during capture.");
563
+ }
564
+ const bytes = Buffer.from(result.data, "base64");
565
+ if (bytes.length > MAX_SCREENSHOT_BYTES) {
566
+ throw new BrowserToolError("SCREENSHOT_TOO_LARGE", "Screenshot exceeds the size limit.");
567
+ }
568
+ record.captureSeq += 1;
569
+ const seq = record.captureSeq;
570
+ const capturedAt = this.nowMs();
571
+ // M1: evict the oldest entry if the cap is reached (Map is insertion-ordered).
572
+ if (record.pendingScreenshots.size >= MAX_PENDING_SCREENSHOTS) {
573
+ const oldest = record.pendingScreenshots.keys().next().value;
574
+ if (oldest !== undefined)
575
+ record.pendingScreenshots.delete(oldest);
576
+ }
577
+ record.pendingScreenshots.set(seq, { seq, viewportPx: DEFAULT_VIEWPORT, bytes, capturedAt });
578
+ this.emitRecord(record, "screenshot-captured", {
579
+ captureSeq: seq,
580
+ persisted: false,
581
+ viewportPx: DEFAULT_VIEWPORT,
582
+ }, capturedAt);
583
+ return { seq, viewportPx: DEFAULT_VIEWPORT, dataBase64: result.data, persisted: false };
584
+ });
585
+ };
586
+ applyScreenshot = async (sessionId, captureSeq) => this.runSessionAction(sessionId, (record) => {
587
+ const pending = record.pendingScreenshots.get(captureSeq);
588
+ if (pending === undefined) {
589
+ throw new BrowserToolError("NO_PENDING_SCREENSHOT", "No pending screenshot for this sequence.");
590
+ }
591
+ this.touch(record);
592
+ const name = `browser-${String(pending.seq)}.png`;
593
+ const written = writeSideFile(this.opts.evidenceDir, record.runId, name, pending.bytes, this.opts.sideFileOptions);
594
+ const screenshot = {
595
+ seq: pending.seq,
596
+ viewportPx: pending.viewportPx,
597
+ path: written.relativePath,
598
+ sha256: written.sha256,
599
+ bytes: written.bytes,
600
+ capturedAt: pending.capturedAt,
601
+ };
602
+ record.screenshots.push(screenshot);
603
+ record.pendingScreenshots.delete(captureSeq);
604
+ this.emitRecord(record, "screenshot-captured", {
605
+ captureSeq,
606
+ persisted: true,
607
+ viewportPx: pending.viewportPx,
608
+ path: written.relativePath,
609
+ sha256: written.sha256,
610
+ bytes: written.bytes,
611
+ });
612
+ return {
613
+ seq: pending.seq,
614
+ viewportPx: pending.viewportPx,
615
+ persisted: true,
616
+ path: written.relativePath,
617
+ sha256: written.sha256,
618
+ bytes: written.bytes,
619
+ };
620
+ });
621
+ content = async (sessionId) => {
622
+ return this.runSessionAction(sessionId, async (record) => {
623
+ this.requireOriginAllowed(record, "Content capture");
624
+ this.touch(record);
625
+ const doc = await record.client.send("DOM.getDocument", {}, record.cdpSessionId);
626
+ const html = await record.client.send("DOM.getOuterHTML", { nodeId: doc.root.nodeId }, record.cdpSessionId);
627
+ // Defence in depth against the frameNavigated race during the DOM.getOuterHTML RPC.
628
+ if (!record.originAllowed) {
629
+ throw new BrowserToolError("ORIGIN_NOT_ALLOWED", "Content capture blocked: origin drifted off-loopback during capture.");
630
+ }
631
+ const raw = html.outerHTML;
632
+ if (Buffer.byteLength(raw, "utf8") > MAX_CONTENT_BYTES) {
633
+ throw new BrowserToolError("CONTENT_TOO_LARGE", "Page content exceeds the size limit.");
634
+ }
635
+ const redacted = this.redactor(raw);
636
+ const redactedHtml = typeof redacted === "string" ? redacted : raw;
637
+ const byteLength = Buffer.byteLength(redactedHtml, "utf8");
638
+ if (byteLength > MAX_CONTENT_BYTES) {
639
+ throw new BrowserToolError("CONTENT_TOO_LARGE", "Page content exceeds the size limit.");
640
+ }
641
+ record.captureSeq += 1;
642
+ const seq = record.captureSeq;
643
+ record.contentCaptures.push({ seq, byteLength, capturedAt: this.nowMs(), redactedHtml });
644
+ this.emitRecord(record, "page-content-captured", { captureSeq: seq, byteLength });
645
+ return { seq, byteLength, redactedHtml };
646
+ });
647
+ };
648
+ listSessionIds = () => [...this.sessions.keys()];
649
+ hasSession = (sessionId) => {
650
+ const record = this.sessions.get(sessionId);
651
+ return record !== undefined && !record.closed;
652
+ };
653
+ dispose = async () => {
654
+ for (const id of [...this.sessions.keys()]) {
655
+ const record = this.sessions.get(id);
656
+ if (record !== undefined) {
657
+ await this.closeRecord(record, "process-exit", true);
658
+ }
659
+ }
660
+ };
661
+ subscribe = (sessionId, listener) => {
662
+ if (!this.hasSession(sessionId)) {
663
+ return () => undefined;
664
+ }
665
+ let set = this.subscribers.get(sessionId);
666
+ if (set === undefined) {
667
+ set = new Set();
668
+ this.subscribers.set(sessionId, set);
669
+ }
670
+ const targetSet = set;
671
+ targetSet.add(listener);
672
+ return () => {
673
+ targetSet.delete(listener);
674
+ if (targetSet.size === 0)
675
+ this.subscribers.delete(sessionId);
676
+ };
677
+ };
678
+ counterAccessor = () => this.counters;
679
+ requireRecord(sessionId) {
680
+ const record = this.sessions.get(sessionId);
681
+ if (record === undefined || record.closed) {
682
+ throw new BrowserToolError("SESSION_NOT_FOUND", "Browser session not found.");
683
+ }
684
+ return record;
685
+ }
686
+ requireOriginAllowed(record, subject) {
687
+ if (record.originAllowed)
688
+ return;
689
+ throw new BrowserToolError("ORIGIN_NOT_ALLOWED", `${subject} blocked: current origin is not on the loopback interface.`);
690
+ }
691
+ touch(record) {
692
+ record.lastTouchedMs = this.nowMs();
693
+ if (record.idleTimer !== undefined)
694
+ clearTimeout(record.idleTimer);
695
+ record.idleTimer = setTimeout(() => {
696
+ void this.closeRecord(record, "idle-timeout", true).catch(() => undefined);
697
+ }, this.idleTtlMs).unref();
698
+ }
699
+ fanout(event) {
700
+ const set = this.subscribers.get(event.sessionId);
701
+ if (set === undefined)
702
+ return;
703
+ for (const listener of [...set]) {
704
+ try {
705
+ listener(event);
706
+ }
707
+ catch {
708
+ // A subscriber throwing must not stop fan-out.
709
+ }
710
+ }
711
+ }
712
+ frameNavigatedListener(record) {
713
+ return (event) => {
714
+ if (event.method !== "Page.frameNavigated")
715
+ return;
716
+ this.handleFrameNavigated(record, event.params);
717
+ };
718
+ }
719
+ handleFrameNavigated(record, params) {
720
+ if (isSubframeNavigation(params))
721
+ return;
722
+ const url = mainFrameUrl(params);
723
+ if (url === null)
724
+ return;
725
+ record.lastUrl = url;
726
+ const originOnly = safeOrigin(url);
727
+ if (this.isExpectedOrigin(record, url, originOnly)) {
728
+ record.originAllowed = true;
729
+ record.lastOriginOnly = originOnly;
730
+ return;
731
+ }
732
+ this.rejectFrameNavigation(record, originOnly);
733
+ }
734
+ isExpectedOrigin(record, url, originOnly) {
735
+ return isLoopbackUrl(url) && originOnly !== null && originOnly === record.expectedOriginOnly;
736
+ }
737
+ rejectFrameNavigation(record, originOnly) {
738
+ record.originAllowed = false;
739
+ record.lastOriginOnly = originOnly;
740
+ // Best-effort stop loading; ignore the rejection — the next caller will see
741
+ // ORIGIN_NOT_ALLOWED on screenshot/content anyway.
742
+ void record.client.send("Page.stopLoading", {}, record.cdpSessionId).catch(() => undefined);
743
+ this.emitErrorRecord(record, "ORIGIN_NOT_ALLOWED", "Navigation drifted away from the requested loopback origin.");
744
+ }
745
+ }
746
+ export function createBrowserSessionManager(options) {
747
+ return new BrowserSessionManagerImpl(options);
748
+ }