@arnilo/prism-tools 0.0.0-stage → 0.13.1

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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Browser egress policy and Playwright context routing (defense in depth).
3
+ * Production DNS/private egress containment remains a host firewall/proxy.
4
+ */
5
+ import { BrowserError } from "./errors.js";
6
+ import type { ResolvedBrowserLimits } from "./limits.js";
7
+ import type { PlaywrightBrowserContext, PlaywrightRoute } from "./types.js";
8
+
9
+ export type BrowserUrlDecision =
10
+ | { readonly allowed: true; readonly url: URL; readonly kind: "http" | "https" | "ws" | "wss" }
11
+ | { readonly allowed: false; readonly reason: string };
12
+
13
+ export interface BrowserNetworkPolicy {
14
+ /**
15
+ * When true, external browsing requires `containedProxyAttestation`.
16
+ * Without attestation every non-blank request is aborted (fail closed).
17
+ */
18
+ readonly requireContainedProxy?: boolean;
19
+ /**
20
+ * Host attestation that real egress is forced through an isolated proxy/firewall.
21
+ * Playwright routing alone is never treated as DNS containment.
22
+ */
23
+ readonly containedProxyAttestation?: {
24
+ readonly proxyEndpoint: string;
25
+ readonly denyDirectEgress: true;
26
+ };
27
+ /** Allow loopback destinations (default false). */
28
+ readonly allowLoopback?: boolean;
29
+ /** Allow private/link-local/ULA destinations (default false). */
30
+ readonly allowPrivateHosts?: boolean;
31
+ /** Optional host callback consulted after scheme/host checks. */
32
+ readonly validateUrl?: (url: URL) => boolean | Promise<boolean>;
33
+ }
34
+
35
+ export interface NetworkBudget {
36
+ requestCount: number;
37
+ webSocketCount: number;
38
+ readonly redirects: Map<string, number>;
39
+ }
40
+
41
+ export function createNetworkBudget(): NetworkBudget {
42
+ return { requestCount: 0, webSocketCount: 0, redirects: new Map() };
43
+ }
44
+
45
+ const BLOCKED_SCHEMES = new Set([
46
+ "file:",
47
+ "data:",
48
+ "blob:",
49
+ "javascript:",
50
+ "chrome:",
51
+ "chrome-devtools:",
52
+ "chrome-extension:",
53
+ "devtools:",
54
+ "view-source:",
55
+ ]);
56
+
57
+ export function classifyBrowserUrl(raw: string, policy: BrowserNetworkPolicy = {}): BrowserUrlDecision {
58
+ let url: URL;
59
+ try {
60
+ url = new URL(raw);
61
+ } catch {
62
+ return { allowed: false, reason: "url is not absolute" };
63
+ }
64
+
65
+ if (url.protocol === "about:") {
66
+ if (url.pathname === "blank" || url.href === "about:blank") {
67
+ return { allowed: false, reason: "about:blank is not a navigable egress target" };
68
+ }
69
+ return { allowed: false, reason: `scheme ${url.protocol} is blocked` };
70
+ }
71
+
72
+ if (BLOCKED_SCHEMES.has(url.protocol)) {
73
+ return { allowed: false, reason: `scheme ${url.protocol} is blocked` };
74
+ }
75
+
76
+ const kind =
77
+ url.protocol === "http:"
78
+ ? "http"
79
+ : url.protocol === "https:"
80
+ ? "https"
81
+ : url.protocol === "ws:"
82
+ ? "ws"
83
+ : url.protocol === "wss:"
84
+ ? "wss"
85
+ : undefined;
86
+ if (!kind) {
87
+ return { allowed: false, reason: `unsupported scheme ${url.protocol}` };
88
+ }
89
+
90
+ if (url.username || url.password) {
91
+ return { allowed: false, reason: "url userinfo is not allowed" };
92
+ }
93
+
94
+ const hostClass = classifyHost(url.hostname);
95
+ if (hostClass === "loopback" && !policy.allowLoopback) {
96
+ return { allowed: false, reason: "loopback destinations are denied by default" };
97
+ }
98
+ if ((hostClass === "private" || hostClass === "link-local" || hostClass === "ula") && !policy.allowPrivateHosts) {
99
+ return { allowed: false, reason: "private/link-local destinations are denied by default" };
100
+ }
101
+
102
+ if (policy.requireContainedProxy !== false) {
103
+ const attestation = policy.containedProxyAttestation;
104
+ if (attestation?.denyDirectEgress !== true || !attestation.proxyEndpoint) {
105
+ return {
106
+ allowed: false,
107
+ reason: "contained proxy attestation required for external browser egress",
108
+ };
109
+ }
110
+ try {
111
+ const proxy = new URL(attestation.proxyEndpoint);
112
+ if (proxy.protocol !== "http:" && proxy.protocol !== "https:" && proxy.protocol !== "socks5:") {
113
+ return { allowed: false, reason: "proxyEndpoint must be http(s) or socks5" };
114
+ }
115
+ } catch {
116
+ return { allowed: false, reason: "proxyEndpoint is not a valid URL" };
117
+ }
118
+ }
119
+
120
+ return { allowed: true, url, kind };
121
+ }
122
+
123
+ export function classifyHost(hostname: string): "public" | "loopback" | "private" | "link-local" | "ula" | "invalid" {
124
+ const host = hostname
125
+ .trim()
126
+ .toLowerCase()
127
+ .replace(/^\[|\]$/g, "");
128
+ if (!host) return "invalid";
129
+ if (host === "localhost" || host.endsWith(".localhost")) return "loopback";
130
+ if (host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0") return "loopback";
131
+
132
+ if (isIpv4(host)) {
133
+ const parts = host.split(".").map((p) => Number(p));
134
+ const [a, b] = parts;
135
+ if (a === 127) return "loopback";
136
+ if (a === 10) return "private";
137
+ if (a === 172 && b !== undefined && b >= 16 && b <= 31) return "private";
138
+ if (a === 192 && b === 168) return "private";
139
+ if (a === 169 && b === 254) return "link-local";
140
+ if (a === 100 && b !== undefined && b >= 64 && b <= 127) return "private"; // CGNAT
141
+ if (a !== undefined && a >= 224) return "private"; // multicast/reserved treated as non-public
142
+ return "public";
143
+ }
144
+
145
+ if (host.includes(":")) {
146
+ if (host === "::1" || host.startsWith("::1/") || host === "0:0:0:0:0:0:0:1") return "loopback";
147
+ if (host.startsWith("fe80:") || host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb")) {
148
+ return "link-local";
149
+ }
150
+ if (host.startsWith("fc") || host.startsWith("fd")) return "ula";
151
+ if (host.startsWith("ff")) return "private";
152
+ return "public";
153
+ }
154
+
155
+ // Hostnames without dots that are not localhost are still resolved by the browser;
156
+ // treat as public for URL classification — DNS containment remains host-owned.
157
+ return "public";
158
+ }
159
+
160
+ function isIpv4(host: string): boolean {
161
+ const parts = host.split(".");
162
+ if (parts.length !== 4) return false;
163
+ return parts.every((part) => {
164
+ if (!/^\d{1,3}$/.test(part)) return false;
165
+ const n = Number(part);
166
+ return n >= 0 && n <= 255;
167
+ });
168
+ }
169
+
170
+ export async function assertBrowserUrlAllowed(raw: string, policy: BrowserNetworkPolicy = {}): Promise<URL> {
171
+ const decision = classifyBrowserUrl(raw, policy);
172
+ if (!decision.allowed) {
173
+ throw new BrowserError("ERR_PRISM_BROWSER_NETWORK", decision.reason);
174
+ }
175
+ if (policy.validateUrl) {
176
+ const ok = await policy.validateUrl(decision.url);
177
+ if (!ok) {
178
+ throw new BrowserError("ERR_PRISM_BROWSER_NETWORK", "host validateUrl denied the URL");
179
+ }
180
+ }
181
+ return decision.url;
182
+ }
183
+
184
+ export interface InstallNetworkRoutingOptions {
185
+ readonly context: PlaywrightBrowserContext;
186
+ readonly policy: BrowserNetworkPolicy;
187
+ readonly limits: ResolvedBrowserLimits;
188
+ readonly budget: NetworkBudget;
189
+ }
190
+
191
+ /**
192
+ * Install context-wide request routing. Defense in depth only — not a DNS firewall.
193
+ * Requires `serviceWorkers: "block"` on the context so routes observe requests.
194
+ */
195
+ export async function installNetworkRouting(options: InstallNetworkRoutingOptions): Promise<() => void> {
196
+ const { context, policy, limits, budget } = options;
197
+ if (typeof context.route !== "function") {
198
+ throw new BrowserError("ERR_PRISM_BROWSER", "BrowserContext.route is required for network policy enforcement");
199
+ }
200
+
201
+ const handler = async (route: PlaywrightRoute): Promise<void> => {
202
+ try {
203
+ const request = route.request();
204
+ const url = request.url();
205
+ // about:blank is the initial empty document — allow without charging egress budget.
206
+ if (url === "about:blank") {
207
+ await route.continue();
208
+ return;
209
+ }
210
+ const resourceType = typeof request.resourceType === "function" ? request.resourceType() : undefined;
211
+ const isWebSocket = resourceType === "websocket" || url.startsWith("ws:") || url.startsWith("wss:");
212
+
213
+ if (isWebSocket) {
214
+ if (budget.webSocketCount >= limits.maxWebSockets) {
215
+ await route.abort("blockedbyclient");
216
+ return;
217
+ }
218
+ } else if (budget.requestCount >= limits.maxNetworkRequests) {
219
+ await route.abort("blockedbyclient");
220
+ return;
221
+ }
222
+
223
+ const redirectedFrom = typeof request.redirectedFrom === "function" ? request.redirectedFrom() : null;
224
+ if (redirectedFrom) {
225
+ const key = redirectedFrom.url();
226
+ const count = (budget.redirects.get(key) ?? 0) + 1;
227
+ budget.redirects.set(key, count);
228
+ if (count > limits.maxRedirectsPerRequest) {
229
+ await route.abort("blockedbyclient");
230
+ return;
231
+ }
232
+ }
233
+
234
+ const decision = classifyBrowserUrl(url, policy);
235
+ if (!decision.allowed) {
236
+ await route.abort("blockedbyclient");
237
+ return;
238
+ }
239
+ if (policy.validateUrl) {
240
+ const ok = await policy.validateUrl(decision.url);
241
+ if (!ok) {
242
+ await route.abort("blockedbyclient");
243
+ return;
244
+ }
245
+ }
246
+
247
+ if (isWebSocket) budget.webSocketCount += 1;
248
+ else budget.requestCount += 1;
249
+ await route.continue();
250
+ } catch {
251
+ try {
252
+ await route.abort("failed");
253
+ } catch {
254
+ /* ignore */
255
+ }
256
+ }
257
+ };
258
+
259
+ await context.route("**/*", handler);
260
+ return () => {
261
+ void context.unroute?.("**/*", handler).catch?.(() => undefined);
262
+ };
263
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Bounded console + network observation ring (0.1.4, plan 016 Task 4).
3
+ * CDP Runtime/Network events are recorded per page into bounded rings with
4
+ * drain-on-read semantics. Bodies, cookies, and auth headers are NEVER captured.
5
+ */
6
+ import type { CdpConsoleEntry, CdpNetworkEntry, PlaywrightCdpSession } from "./types.js";
7
+
8
+ export interface ObservationRing {
9
+ recordConsole(entry: CdpConsoleEntry): void;
10
+ recordNetwork(entry: CdpNetworkEntry): void;
11
+ /** Return entries since the previous drain and clear; truncated reflects eviction. */
12
+ drain(): { console: readonly CdpConsoleEntry[]; network: readonly CdpNetworkEntry[]; truncated: boolean };
13
+ }
14
+
15
+ export function createObservationRing(options: { maxConsoleEntries: number; maxNetworkRequests: number }): ObservationRing {
16
+ const consoleEntries: CdpConsoleEntry[] = [];
17
+ const networkEntries: CdpNetworkEntry[] = [];
18
+ let consoleSeq = 0;
19
+ let networkSeq = 0;
20
+ let truncated = false;
21
+
22
+ function push<T>(list: T[], entry: T, cap: number): void {
23
+ list.push(entry);
24
+ if (list.length > cap) {
25
+ list.shift();
26
+ truncated = true;
27
+ }
28
+ }
29
+
30
+ return {
31
+ recordConsole(entry) {
32
+ consoleSeq += 1;
33
+ push(consoleEntries, { ...entry, seq: consoleSeq }, options.maxConsoleEntries);
34
+ },
35
+ recordNetwork(entry) {
36
+ networkSeq += 1;
37
+ push(networkEntries, { ...entry, seq: networkSeq }, options.maxNetworkRequests);
38
+ },
39
+ drain() {
40
+ const out = {
41
+ console: consoleEntries.splice(0),
42
+ network: networkEntries.splice(0),
43
+ truncated,
44
+ };
45
+ truncated = false;
46
+ return out;
47
+ },
48
+ };
49
+ }
50
+
51
+ const MAX_URL_BYTES = 2_048;
52
+ const MAX_ERROR_TEXT_BYTES = 512;
53
+ const MAX_METHOD_BYTES = 32;
54
+ const MAX_ARG_PREVIEW_BYTES = 512;
55
+ const MAX_ARGS = 4;
56
+
57
+ export type CdpConsoleType = CdpConsoleEntry["type"];
58
+
59
+ /** CDP console API types that map 1:1; the rest collapse to "other". */
60
+ const CONSOLE_TYPES = new Set<string>(["log", "error", "warning", "info", "debug", "assert", "exception"]);
61
+
62
+ function consoleTypeOf(raw: unknown): CdpConsoleType {
63
+ if (typeof raw !== "string") return "other";
64
+ if (raw === "warning") return "warning";
65
+ if (CONSOLE_TYPES.has(raw)) return raw as CdpConsoleType;
66
+ return "other";
67
+ }
68
+
69
+ function previewOf(object: {
70
+ readonly type?: string;
71
+ readonly subtype?: string;
72
+ readonly value?: unknown;
73
+ readonly description?: string;
74
+ }): string {
75
+ let text: string;
76
+ if (object.value !== undefined) {
77
+ try {
78
+ text = JSON.stringify(object.value);
79
+ } catch {
80
+ text = String(object.value);
81
+ }
82
+ } else if (typeof object.description === "string" && object.description.length > 0) {
83
+ text = object.description;
84
+ } else {
85
+ text = object.type ?? "object";
86
+ }
87
+ return truncateUtf8(text, MAX_ARG_PREVIEW_BYTES);
88
+ }
89
+
90
+ function truncateUtf8(text: string, maxBytes: number): string {
91
+ const buf = Buffer.from(text, "utf8");
92
+ if (buf.byteLength <= maxBytes) return text;
93
+ return `${buf.subarray(0, maxBytes).toString("utf8")}…`;
94
+ }
95
+
96
+ function truncateText(text: string | undefined, maxBytes: number): string | undefined {
97
+ if (text === undefined) return undefined;
98
+ return truncateUtf8(text, maxBytes);
99
+ }
100
+
101
+ /**
102
+ * Install CDP Runtime/Network observation handlers on a page session.
103
+ * Returns the unsubscribe function. Event params are treated as untrusted.
104
+ */
105
+ export function installCdpObservation(session: PlaywrightCdpSession, ring: ObservationRing): () => void {
106
+ const onConsole = (params: Record<string, unknown>) => {
107
+ const type = consoleTypeOf(params.type);
108
+ const args = Array.isArray(params.args) ? (params.args as Array<Record<string, unknown>>).slice(0, MAX_ARGS).map(previewOf) : [];
109
+ ring.recordConsole({ seq: 0, type, args });
110
+ };
111
+ const onException = (params: Record<string, unknown>) => {
112
+ const details = params.exceptionDetails as Record<string, unknown> | undefined;
113
+ const exception = details?.exception as Record<string, unknown> | undefined;
114
+ const description = truncateText(
115
+ typeof exception?.description === "string" && exception.description.length > 0
116
+ ? exception.description
117
+ : typeof details?.text === "string"
118
+ ? details.text
119
+ : undefined,
120
+ MAX_ARG_PREVIEW_BYTES,
121
+ );
122
+ const url = truncateText(typeof details?.url === "string" ? details.url : undefined, MAX_URL_BYTES);
123
+ ring.recordConsole({
124
+ seq: 0,
125
+ type: "exception",
126
+ args: description !== undefined ? [description] : [],
127
+ ...(url !== undefined ? { text: url } : {}),
128
+ });
129
+ };
130
+ const onRequest = (params: Record<string, unknown>) => {
131
+ const request = params.request as Record<string, unknown> | undefined;
132
+ const url = truncateText(typeof request?.url === "string" ? request.url : undefined, MAX_URL_BYTES) ?? "";
133
+ const method = truncateText(typeof request?.method === "string" ? request.method : undefined, MAX_METHOD_BYTES);
134
+ ring.recordNetwork({
135
+ seq: 0,
136
+ phase: "request",
137
+ requestId: typeof params.requestId === "string" ? params.requestId.slice(0, 128) : "",
138
+ url,
139
+ ...(method !== undefined ? { method } : {}),
140
+ });
141
+ };
142
+ const onResponse = (params: Record<string, unknown>) => {
143
+ const response = params.response as Record<string, unknown> | undefined;
144
+ const url = truncateText(typeof response?.url === "string" ? response.url : undefined, MAX_URL_BYTES) ?? "";
145
+ ring.recordNetwork({
146
+ seq: 0,
147
+ phase: "response",
148
+ requestId: typeof params.requestId === "string" ? params.requestId.slice(0, 128) : "",
149
+ url,
150
+ ...(typeof response?.status === "number" ? { status: response.status } : {}),
151
+ });
152
+ };
153
+ const onFailed = (params: Record<string, unknown>) => {
154
+ ring.recordNetwork({
155
+ seq: 0,
156
+ phase: "failed",
157
+ requestId: typeof params.requestId === "string" ? params.requestId.slice(0, 128) : "",
158
+ url: "",
159
+ ...(truncateText(typeof params.errorText === "string" ? params.errorText : undefined, MAX_ERROR_TEXT_BYTES)
160
+ ? { errorText: truncateText(typeof params.errorText === "string" ? params.errorText : undefined, MAX_ERROR_TEXT_BYTES) }
161
+ : {}),
162
+ });
163
+ };
164
+
165
+ session.on("Runtime.consoleAPICalled", onConsole);
166
+ session.on("Runtime.exceptionThrown", onException);
167
+ session.on("Network.requestWillBeSent", onRequest);
168
+ session.on("Network.responseReceived", onResponse);
169
+ session.on("Network.loadingFailed", onFailed);
170
+
171
+ return () => {
172
+ session.off?.("Runtime.consoleAPICalled", onConsole);
173
+ session.off?.("Runtime.exceptionThrown", onException);
174
+ session.off?.("Network.requestWillBeSent", onRequest);
175
+ session.off?.("Network.responseReceived", onResponse);
176
+ session.off?.("Network.loadingFailed", onFailed);
177
+ };
178
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Observation vs side-effect classification for browser tools: feeds each tool's `effect`
3
+ * declaration and the manager's side-effect hook.
4
+ */
5
+ import type { ToolEffectDeclaration } from "@arnilo/prism";
6
+ import type { BrowserActionName } from "./types.js";
7
+
8
+ export type BrowserRisk = "low" | "medium" | "high";
9
+ export type BrowserEffectClass = "observation" | "mutation" | "high_impact";
10
+
11
+ export interface BrowserOperationClass {
12
+ readonly operation: string;
13
+ readonly effect: BrowserEffectClass;
14
+ readonly risk: BrowserRisk;
15
+ readonly requiresSideEffectHook: boolean;
16
+ }
17
+
18
+ const OBSERVATION = new Set<string>(["open", "snapshot", "wait", "close", "observe"]);
19
+
20
+ export function classifyBrowserOperation(
21
+ operation: string,
22
+ details: {
23
+ readonly dialogResponse?: "accept" | "dismiss";
24
+ readonly hasUrl?: boolean;
25
+ readonly pageKind?: "main" | "popup";
26
+ } = {},
27
+ ): BrowserOperationClass {
28
+ if (operation === "evaluate") {
29
+ // Arbitrary page-context code execution: highest-risk browser operation.
30
+ return {
31
+ operation,
32
+ effect: "high_impact",
33
+ risk: "high",
34
+ requiresSideEffectHook: true,
35
+ };
36
+ }
37
+ if (operation === "block_urls" || operation === "unblock_urls" || operation === "throttle" || operation === "emulate") {
38
+ // CDP network/emulation control changes what the page loads and how it renders.
39
+ return {
40
+ operation,
41
+ effect: "mutation",
42
+ risk: "medium",
43
+ requiresSideEffectHook: true,
44
+ };
45
+ }
46
+ if (operation === "open" && details.hasUrl) {
47
+ return {
48
+ operation: "open_navigate",
49
+ effect: "mutation",
50
+ risk: "medium",
51
+ requiresSideEffectHook: true,
52
+ };
53
+ }
54
+ if (OBSERVATION.has(operation)) {
55
+ return {
56
+ operation,
57
+ effect: "observation",
58
+ risk: "low",
59
+ requiresSideEffectHook: false,
60
+ };
61
+ }
62
+ if (operation === "select_page") {
63
+ const popup = details.pageKind === "popup";
64
+ return {
65
+ operation: popup ? "select_popup" : "select_page",
66
+ effect: popup ? "mutation" : "observation",
67
+ risk: popup ? "medium" : "low",
68
+ requiresSideEffectHook: popup,
69
+ };
70
+ }
71
+ if (operation === "dialog") {
72
+ const accept = details.dialogResponse === "accept";
73
+ return {
74
+ operation: accept ? "dialog_accept" : "dialog_dismiss",
75
+ effect: accept ? "high_impact" : "mutation",
76
+ risk: accept ? "high" : "medium",
77
+ requiresSideEffectHook: true,
78
+ };
79
+ }
80
+ if (operation === "download_release") {
81
+ return {
82
+ operation,
83
+ effect: "high_impact",
84
+ risk: "high",
85
+ requiresSideEffectHook: true,
86
+ };
87
+ }
88
+ if (operation === "upload" || operation === "screenshot") {
89
+ return {
90
+ operation,
91
+ effect: operation === "upload" ? "high_impact" : "mutation",
92
+ risk: operation === "upload" ? "high" : "medium",
93
+ requiresSideEffectHook: true,
94
+ };
95
+ }
96
+ if (
97
+ operation === "navigate" ||
98
+ operation === "click" ||
99
+ operation === "type" ||
100
+ operation === "fill" ||
101
+ operation === "select" ||
102
+ operation === "check" ||
103
+ operation === "uncheck" ||
104
+ operation === "scroll"
105
+ ) {
106
+ return {
107
+ operation,
108
+ effect: "mutation",
109
+ risk: "medium",
110
+ requiresSideEffectHook: true,
111
+ };
112
+ }
113
+ return {
114
+ operation,
115
+ effect: "mutation",
116
+ risk: "medium",
117
+ requiresSideEffectHook: true,
118
+ };
119
+ }
120
+
121
+ /** Browser mutations are external effects: browser state must reload+verify, never replay. */
122
+ export function classifyBrowserToolEffect(
123
+ operation: string,
124
+ details: Parameters<typeof classifyBrowserOperation>[1] = {},
125
+ ): ToolEffectDeclaration {
126
+ return classifyBrowserOperation(operation, details).effect === "observation"
127
+ ? { kind: "none", idempotency: "none" }
128
+ : { kind: "external_mutation", idempotency: "unsupported" };
129
+ }
130
+
131
+ export function isSideEffectAction(action: BrowserActionName | "evaluate" | "observe"): boolean {
132
+ return classifyBrowserOperation(action).requiresSideEffectHook;
133
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Bounded screenshot capture returning Prism ImageContent.
3
+ */
4
+ import type { ImageContent } from "@arnilo/prism";
5
+ import { BrowserError } from "./errors.js";
6
+ import type { ResolvedBrowserLimits } from "./limits.js";
7
+ import type { PlaywrightPage } from "./types.js";
8
+
9
+ export interface ScreenshotBudget {
10
+ count: number;
11
+ }
12
+
13
+ export function createScreenshotBudget(): ScreenshotBudget {
14
+ return { count: 0 };
15
+ }
16
+
17
+ export interface CaptureScreenshotOptions {
18
+ readonly page: PlaywrightPage;
19
+ readonly limits: ResolvedBrowserLimits;
20
+ readonly budget: ScreenshotBudget;
21
+ readonly fullPage?: boolean;
22
+ readonly clip?: { x: number; y: number; width: number; height: number };
23
+ readonly signal?: AbortSignal;
24
+ }
25
+
26
+ export interface ScreenshotResult {
27
+ readonly image: ImageContent;
28
+ readonly bytes: number;
29
+ readonly width?: number;
30
+ readonly height?: number;
31
+ readonly megapixels?: number;
32
+ }
33
+
34
+ function assertFinitePositive(name: string, value: number): void {
35
+ if (!Number.isFinite(value) || value <= 0) {
36
+ throw new BrowserError("ERR_PRISM_BROWSER_INPUT", `${name} must be a positive finite number`);
37
+ }
38
+ }
39
+
40
+ export async function captureBoundedScreenshot(options: CaptureScreenshotOptions): Promise<ScreenshotResult> {
41
+ const { page, limits, budget } = options;
42
+ if (budget.count >= limits.maxScreenshots) {
43
+ throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `maxScreenshots ${limits.maxScreenshots} exceeded`);
44
+ }
45
+ if (typeof page.screenshot !== "function") {
46
+ throw new BrowserError("ERR_PRISM_BROWSER", "Page.screenshot is unavailable");
47
+ }
48
+ if (options.signal?.aborted) {
49
+ throw new BrowserError("ERR_PRISM_BROWSER", "screenshot aborted");
50
+ }
51
+
52
+ if (options.clip) {
53
+ assertFinitePositive("clip.width", options.clip.width);
54
+ assertFinitePositive("clip.height", options.clip.height);
55
+ const megapixels = (options.clip.width * options.clip.height) / 1_000_000;
56
+ if (megapixels > limits.maxScreenshotMegapixels) {
57
+ throw new BrowserError(
58
+ "ERR_PRISM_BROWSER_LIMIT",
59
+ `screenshot clip exceeds maxScreenshotMegapixels ${limits.maxScreenshotMegapixels}`,
60
+ );
61
+ }
62
+ }
63
+
64
+ let buffer: Buffer;
65
+ try {
66
+ const raw = await page.screenshot({
67
+ type: "png",
68
+ fullPage: options.fullPage === true,
69
+ ...(options.clip ? { clip: options.clip } : {}),
70
+ timeout: limits.actionTimeoutMs,
71
+ });
72
+ buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
73
+ } catch (error) {
74
+ if (error instanceof BrowserError) throw error;
75
+ throw new BrowserError("ERR_PRISM_BROWSER_ARTIFACT", `screenshot failed: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+
78
+ if (buffer.byteLength > limits.maxScreenshotBytes) {
79
+ throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `screenshot exceeds maxScreenshotBytes ${limits.maxScreenshotBytes}`);
80
+ }
81
+ // PNG IHDR width/height at bytes 16..24
82
+ let width: number | undefined;
83
+ let height: number | undefined;
84
+ let megapixels: number | undefined;
85
+ if (buffer.byteLength >= 24 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
86
+ width = buffer.readUInt32BE(16);
87
+ height = buffer.readUInt32BE(20);
88
+ megapixels = (width * height) / 1_000_000;
89
+ if (megapixels > limits.maxScreenshotMegapixels) {
90
+ throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `screenshot exceeds maxScreenshotMegapixels ${limits.maxScreenshotMegapixels}`);
91
+ }
92
+ }
93
+
94
+ budget.count += 1;
95
+ const image: ImageContent = {
96
+ type: "image",
97
+ mimeType: "image/png",
98
+ data: buffer.toString("base64"),
99
+ metadata: {
100
+ bytes: buffer.byteLength,
101
+ ...(width !== undefined ? { width } : {}),
102
+ ...(height !== undefined ? { height } : {}),
103
+ source: "browser_screenshot",
104
+ },
105
+ };
106
+ return { image, bytes: buffer.byteLength, width, height, megapixels };
107
+ }