@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.
- package/CHANGELOG.md +418 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +29 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +584 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-owned Playwright BrowserContext manager.
|
|
3
|
+
* One non-persistent context per run; actions serialize through a per-run queue.
|
|
4
|
+
* Task 6: egress routing, side-effect hooks, upload/download/screenshot policy.
|
|
5
|
+
*/
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
import { ACTION_HANDLERS, isActionName } from "./actions/index.js";
|
|
8
|
+
import type { ActionContext, ManagedPage, PageCdpState, RunSession } from "./actions/types.js";
|
|
9
|
+
import { type CdpMode, cdpNetworkEnable, cdpRuntimeEnable, createPageCdpSession, resolveCdpMode } from "./cdp.js";
|
|
10
|
+
import { type BrowserDownloadOptions, cleanupDownloads, createDownloadBudget, quarantineDownload } from "./downloads.js";
|
|
11
|
+
import { BrowserError } from "./errors.js";
|
|
12
|
+
import { evaluateInPage } from "./evaluate.js";
|
|
13
|
+
import { type BrowserLimitOptions, type ResolvedBrowserLimits, resolveBrowserLimits } from "./limits.js";
|
|
14
|
+
import { assertBrowserUrlAllowed, type BrowserNetworkPolicy, createNetworkBudget, installNetworkRouting } from "./network.js";
|
|
15
|
+
import { createObservationRing, installCdpObservation } from "./observe.js";
|
|
16
|
+
import { classifyBrowserOperation, isSideEffectAction } from "./policy.js";
|
|
17
|
+
import { createScreenshotBudget } from "./screenshot.js";
|
|
18
|
+
import { captureAriaSnapshot, toSnapshotResult } from "./snapshot.js";
|
|
19
|
+
import { resolveTargetLocator } from "./targets.js";
|
|
20
|
+
import type {
|
|
21
|
+
BrowserActionName,
|
|
22
|
+
BrowserActRequest,
|
|
23
|
+
BrowserActResult,
|
|
24
|
+
BrowserCdpOptions,
|
|
25
|
+
BrowserDownloadInfo,
|
|
26
|
+
BrowserEvaluateRequest,
|
|
27
|
+
BrowserEvaluateResult,
|
|
28
|
+
BrowserObserveResult,
|
|
29
|
+
BrowserOpenResult,
|
|
30
|
+
BrowserPageInfo,
|
|
31
|
+
BrowserSnapshotResult,
|
|
32
|
+
BrowserTarget,
|
|
33
|
+
PlaywrightBrowser,
|
|
34
|
+
PlaywrightBrowserContext,
|
|
35
|
+
PlaywrightDialog,
|
|
36
|
+
PlaywrightDownload,
|
|
37
|
+
PlaywrightPage,
|
|
38
|
+
} from "./types.js";
|
|
39
|
+
import { type BrowserUploadOptions, createUploadBudget } from "./uploads.js";
|
|
40
|
+
|
|
41
|
+
export interface CreateBrowserManagerOptions {
|
|
42
|
+
readonly browser: PlaywrightBrowser;
|
|
43
|
+
readonly limits?: BrowserLimitOptions;
|
|
44
|
+
/** CDP capability gating (Tasks 4-5, 0.1.4): default "auto" — enabled on Chromium hosts. */
|
|
45
|
+
readonly cdp?: BrowserCdpOptions;
|
|
46
|
+
/** Optional host hook invoked just before a mutating/high-impact action. */
|
|
47
|
+
readonly beforeSideEffect?: (info: {
|
|
48
|
+
runId: string;
|
|
49
|
+
action: BrowserActionName | "evaluate" | "observe";
|
|
50
|
+
url?: string;
|
|
51
|
+
pageId?: string;
|
|
52
|
+
origin?: string;
|
|
53
|
+
paths?: readonly string[];
|
|
54
|
+
resource?: string;
|
|
55
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
56
|
+
}) => void | Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Egress policy for context routing. Defaults to deny private/loopback/file/data
|
|
59
|
+
* and requireContainedProxy:true (fail closed without host attestation).
|
|
60
|
+
* Playwright routing is defense in depth — not a DNS firewall.
|
|
61
|
+
*/
|
|
62
|
+
readonly networkPolicy?: BrowserNetworkPolicy;
|
|
63
|
+
readonly uploads?: BrowserUploadOptions;
|
|
64
|
+
readonly downloads?: BrowserDownloadOptions;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BrowserManager {
|
|
68
|
+
readonly limits: ResolvedBrowserLimits;
|
|
69
|
+
open(runId: string, options?: { url?: string; signal?: AbortSignal }): Promise<BrowserOpenResult>;
|
|
70
|
+
snapshot(runId: string, options?: { pageId?: string; signal?: AbortSignal }): Promise<BrowserSnapshotResult>;
|
|
71
|
+
act(runId: string, request: BrowserActRequest, options?: { signal?: AbortSignal }): Promise<BrowserActResult>;
|
|
72
|
+
evaluate(runId: string, request: BrowserEvaluateRequest, options?: { signal?: AbortSignal }): Promise<BrowserEvaluateResult>;
|
|
73
|
+
observe(runId: string, options?: { pageId?: string; signal?: AbortSignal }): Promise<BrowserObserveResult>;
|
|
74
|
+
closeRun(runId: string): Promise<void>;
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
hasRun(runId: string): boolean;
|
|
77
|
+
listDownloads(runId: string): readonly BrowserDownloadInfo[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let pageSeq = 0;
|
|
81
|
+
function nextPageId(): string {
|
|
82
|
+
pageSeq += 1;
|
|
83
|
+
return `page_${pageSeq}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function defaultNetworkPolicy(input?: BrowserNetworkPolicy): BrowserNetworkPolicy {
|
|
87
|
+
return {
|
|
88
|
+
requireContainedProxy: input?.requireContainedProxy ?? true,
|
|
89
|
+
allowLoopback: input?.allowLoopback ?? false,
|
|
90
|
+
allowPrivateHosts: input?.allowPrivateHosts ?? false,
|
|
91
|
+
...(input?.containedProxyAttestation ? { containedProxyAttestation: input.containedProxyAttestation } : {}),
|
|
92
|
+
...(input?.validateUrl ? { validateUrl: input.validateUrl } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createBrowserManager(options: CreateBrowserManagerOptions): BrowserManager {
|
|
97
|
+
if (!options?.browser || typeof options.browser.newContext !== "function") {
|
|
98
|
+
throw new BrowserError("ERR_PRISM_BROWSER_INPUT", "createBrowserManager requires a host-supplied Playwright Browser with newContext()");
|
|
99
|
+
}
|
|
100
|
+
const limits = resolveBrowserLimits(options.limits);
|
|
101
|
+
const networkPolicy = defaultNetworkPolicy(options.networkPolicy);
|
|
102
|
+
const cdpMode: CdpMode = resolveCdpMode(options.cdp);
|
|
103
|
+
const runs = new Map<string, RunSession>();
|
|
104
|
+
const creating = new Map<string, Promise<RunSession>>();
|
|
105
|
+
let closed = false;
|
|
106
|
+
// Opt-in idle-run reaper (idleRunTtlMs > 0): one unref'd interval for the manager's
|
|
107
|
+
// lifetime. A run with nothing queued and no activity for the TTL is disposed exactly
|
|
108
|
+
// like closeRun(), so later calls fail closed with ERR_PRISM_BROWSER_STATE. The
|
|
109
|
+
// sweep runs at half the TTL, so a stale run is released within 1.0-1.5 x TTL.
|
|
110
|
+
let sweeping = false;
|
|
111
|
+
const reaper =
|
|
112
|
+
limits.idleRunTtlMs > 0 ? setInterval(() => void sweepIdleRuns(), Math.max(1, Math.floor(limits.idleRunTtlMs / 2))) : undefined;
|
|
113
|
+
reaper?.unref();
|
|
114
|
+
|
|
115
|
+
const manager: BrowserManager = {
|
|
116
|
+
limits,
|
|
117
|
+
async open(runId, openOptions) {
|
|
118
|
+
assertManagerOpen();
|
|
119
|
+
const id = assertRunId(runId);
|
|
120
|
+
if (openOptions?.url) {
|
|
121
|
+
assertInputBytes(openOptions.url);
|
|
122
|
+
await assertBrowserUrlAllowed(openOptions.url, networkPolicy);
|
|
123
|
+
}
|
|
124
|
+
const session = await ensureSession(id);
|
|
125
|
+
return enqueue(session, openOptions?.signal, async () => {
|
|
126
|
+
assertSessionUsable(session);
|
|
127
|
+
chargeWallTime(session);
|
|
128
|
+
if (openOptions?.url) {
|
|
129
|
+
await maybeSideEffect(session, "navigate", { url: openOptions.url });
|
|
130
|
+
await navigateActive(session, openOptions.url, openOptions.signal);
|
|
131
|
+
}
|
|
132
|
+
return summarizeOpen(session);
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async snapshot(runId, snapOptions) {
|
|
137
|
+
assertManagerOpen();
|
|
138
|
+
const id = assertRunId(runId);
|
|
139
|
+
const session = runs.get(id);
|
|
140
|
+
if (!session) throw new BrowserError("ERR_PRISM_BROWSER_STATE", `No browser context for run ${id}`);
|
|
141
|
+
return enqueue(session, snapOptions?.signal, async () => {
|
|
142
|
+
assertSessionUsable(session);
|
|
143
|
+
chargeWallTime(session);
|
|
144
|
+
const page = resolvePage(session, snapOptions?.pageId);
|
|
145
|
+
const live = await captureAriaSnapshot(page.page, page.pageId, limits);
|
|
146
|
+
session.snapshot = live;
|
|
147
|
+
return toSnapshotResult(live);
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async act(runId, request, actOptions) {
|
|
152
|
+
assertManagerOpen();
|
|
153
|
+
const id = assertRunId(runId);
|
|
154
|
+
const session = runs.get(id);
|
|
155
|
+
if (!session) throw new BrowserError("ERR_PRISM_BROWSER_STATE", `No browser context for run ${id}`);
|
|
156
|
+
return enqueue(session, actOptions?.signal, async () => {
|
|
157
|
+
assertSessionUsable(session);
|
|
158
|
+
chargeWallTime(session);
|
|
159
|
+
return performAction(session, request, actOptions?.signal);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async evaluate(runId, request, evalOptions) {
|
|
164
|
+
assertManagerOpen();
|
|
165
|
+
const id = assertRunId(runId);
|
|
166
|
+
const session = runs.get(id);
|
|
167
|
+
if (!session) throw new BrowserError("ERR_PRISM_BROWSER_STATE", `No browser context for run ${id}`);
|
|
168
|
+
return enqueue(session, evalOptions?.signal, async () => {
|
|
169
|
+
assertSessionUsable(session);
|
|
170
|
+
chargeWallTime(session);
|
|
171
|
+
const page = resolvePage(session, request.pageId);
|
|
172
|
+
chargeAction(session);
|
|
173
|
+
await maybeSideEffect(session, "evaluate", { pageId: page.pageId });
|
|
174
|
+
const state = await ensurePageCdp(session, page);
|
|
175
|
+
const outcome = await evaluateInPage(state.session, request, limits);
|
|
176
|
+
return {
|
|
177
|
+
pageId: page.pageId,
|
|
178
|
+
url: safeUrl(page.page),
|
|
179
|
+
...(outcome.value !== undefined ? { value: outcome.value } : {}),
|
|
180
|
+
...(outcome.exception !== undefined ? { exception: outcome.exception } : {}),
|
|
181
|
+
...(outcome.truncated ? { truncated: true } : {}),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async observe(runId, observeOptions) {
|
|
187
|
+
assertManagerOpen();
|
|
188
|
+
const id = assertRunId(runId);
|
|
189
|
+
const session = runs.get(id);
|
|
190
|
+
if (!session) throw new BrowserError("ERR_PRISM_BROWSER_STATE", `No browser context for run ${id}`);
|
|
191
|
+
return enqueue(session, observeOptions?.signal, async () => {
|
|
192
|
+
assertSessionUsable(session);
|
|
193
|
+
chargeWallTime(session);
|
|
194
|
+
const page = resolvePage(session, observeOptions?.pageId);
|
|
195
|
+
const state = await ensurePageCdp(session, page);
|
|
196
|
+
if (!state.domainsEnabled) {
|
|
197
|
+
await cdpRuntimeEnable(state.session);
|
|
198
|
+
await cdpNetworkEnable(state.session);
|
|
199
|
+
state.domainsEnabled = true;
|
|
200
|
+
}
|
|
201
|
+
const { console: consoleEntries, network, truncated } = state.ring.drain();
|
|
202
|
+
return {
|
|
203
|
+
pageId: page.pageId,
|
|
204
|
+
url: safeUrl(page.page),
|
|
205
|
+
console: consoleEntries,
|
|
206
|
+
network,
|
|
207
|
+
truncated,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
async closeRun(runId) {
|
|
213
|
+
const id = assertRunId(runId);
|
|
214
|
+
const session = runs.get(id);
|
|
215
|
+
if (!session) return;
|
|
216
|
+
await disposeSession(session);
|
|
217
|
+
runs.delete(id);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
async close() {
|
|
221
|
+
closed = true;
|
|
222
|
+
if (reaper) clearInterval(reaper);
|
|
223
|
+
const sessions = [...runs.values()];
|
|
224
|
+
runs.clear();
|
|
225
|
+
await Promise.all(sessions.map((session) => disposeSession(session)));
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
hasRun(runId) {
|
|
229
|
+
return runs.has(runId);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
listDownloads(runId) {
|
|
233
|
+
const session = runs.get(assertRunId(runId));
|
|
234
|
+
if (!session) return [];
|
|
235
|
+
return [...session.downloadBudget.items.values()].map(toDownloadInfo);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return manager;
|
|
240
|
+
|
|
241
|
+
/** Closes runs idle for `idleRunTtlMs` with nothing queued. Never rejects. */
|
|
242
|
+
async function sweepIdleRuns(): Promise<void> {
|
|
243
|
+
if (sweeping || closed) return;
|
|
244
|
+
sweeping = true;
|
|
245
|
+
try {
|
|
246
|
+
const cutoff = Date.now() - limits.idleRunTtlMs;
|
|
247
|
+
for (const session of [...runs.values()]) {
|
|
248
|
+
if (session.closed || session.queued > 0 || session.lastUsedAt > cutoff) continue;
|
|
249
|
+
runs.delete(session.runId);
|
|
250
|
+
await disposeSession(session).catch(() => undefined);
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
sweeping = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function assertManagerOpen(): void {
|
|
258
|
+
if (closed) throw new BrowserError("ERR_PRISM_BROWSER_CLOSED", "Browser manager is closed");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function assertSessionUsable(session: RunSession): void {
|
|
262
|
+
if (session.closed) {
|
|
263
|
+
throw new BrowserError("ERR_PRISM_BROWSER_CLOSED", `Browser context for run ${session.runId} is closed`);
|
|
264
|
+
}
|
|
265
|
+
if (session.crashed) {
|
|
266
|
+
throw new BrowserError("ERR_PRISM_BROWSER_CLOSED", `Browser context for run ${session.runId} crashed`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function ensureSession(runId: string): Promise<RunSession> {
|
|
271
|
+
const existing = runs.get(runId);
|
|
272
|
+
if (existing && !existing.closed) return existing;
|
|
273
|
+
const pending = creating.get(runId);
|
|
274
|
+
if (pending) return pending;
|
|
275
|
+
const created = createSession(runId).finally(() => creating.delete(runId));
|
|
276
|
+
creating.set(runId, created);
|
|
277
|
+
return created;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function createSession(runId: string): Promise<RunSession> {
|
|
281
|
+
if (runs.has(runId)) return runs.get(runId)!;
|
|
282
|
+
const acceptDownloads = Boolean(options.downloads);
|
|
283
|
+
let context: PlaywrightBrowserContext;
|
|
284
|
+
try {
|
|
285
|
+
context = await options.browser.newContext({
|
|
286
|
+
serviceWorkers: "block",
|
|
287
|
+
acceptDownloads,
|
|
288
|
+
...(networkPolicy.containedProxyAttestation ? { proxy: { server: networkPolicy.containedProxyAttestation.proxyEndpoint } } : {}),
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new BrowserError(
|
|
292
|
+
"ERR_PRISM_BROWSER",
|
|
293
|
+
`Failed to create BrowserContext: ${error instanceof Error ? error.message : String(error)}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
context.setDefaultTimeout?.(limits.actionTimeoutMs);
|
|
297
|
+
context.setDefaultNavigationTimeout?.(limits.navigationTimeoutMs);
|
|
298
|
+
|
|
299
|
+
const session: RunSession = {
|
|
300
|
+
runId,
|
|
301
|
+
context,
|
|
302
|
+
createdAt: Date.now(),
|
|
303
|
+
pages: new Map(),
|
|
304
|
+
activePageId: undefined,
|
|
305
|
+
actionCount: 0,
|
|
306
|
+
popupCount: 0,
|
|
307
|
+
dialogCount: 0,
|
|
308
|
+
listenerCount: 0,
|
|
309
|
+
snapshot: undefined,
|
|
310
|
+
pendingDialog: undefined,
|
|
311
|
+
closed: false,
|
|
312
|
+
crashed: false,
|
|
313
|
+
queue: Promise.resolve(),
|
|
314
|
+
queued: 0,
|
|
315
|
+
lastUsedAt: Date.now(),
|
|
316
|
+
cleanup: [],
|
|
317
|
+
networkBudget: createNetworkBudget(),
|
|
318
|
+
uploadBudget: createUploadBudget(),
|
|
319
|
+
downloadBudget: createDownloadBudget(),
|
|
320
|
+
screenshotBudget: createScreenshotBudget(),
|
|
321
|
+
lastDownloadId: undefined,
|
|
322
|
+
cdpPages: new Map(),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Always install routing for defense-in-depth scheme/private denial.
|
|
326
|
+
try {
|
|
327
|
+
const uninstall = await installNetworkRouting({
|
|
328
|
+
context,
|
|
329
|
+
policy: networkPolicy,
|
|
330
|
+
limits,
|
|
331
|
+
budget: session.networkBudget,
|
|
332
|
+
});
|
|
333
|
+
session.cleanup.push(uninstall);
|
|
334
|
+
session.listenerCount += 1;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
await context.close().catch(() => undefined);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const onPage = (pageOrDownload: PlaywrightPage | PlaywrightDownload) => {
|
|
341
|
+
if (!pageOrDownload || typeof (pageOrDownload as PlaywrightPage).goto !== "function") return;
|
|
342
|
+
void acceptPage(session, pageOrDownload as PlaywrightPage, "popup");
|
|
343
|
+
};
|
|
344
|
+
context.on("page", onPage);
|
|
345
|
+
session.cleanup.push(() => context.off?.("page", onPage as never));
|
|
346
|
+
session.listenerCount += 1;
|
|
347
|
+
|
|
348
|
+
if (acceptDownloads && options.downloads) {
|
|
349
|
+
const onDownload = (download: PlaywrightDownload) => {
|
|
350
|
+
void (async () => {
|
|
351
|
+
try {
|
|
352
|
+
const meta = await quarantineDownload(download, options.downloads!, limits, session.downloadBudget);
|
|
353
|
+
session.lastDownloadId = meta.downloadId;
|
|
354
|
+
} catch {
|
|
355
|
+
/* quarantine errors surface on next download_release / list; do not freeze queue */
|
|
356
|
+
}
|
|
357
|
+
})();
|
|
358
|
+
};
|
|
359
|
+
context.on("download", onDownload as (pageOrDownload: PlaywrightPage | PlaywrightDownload) => void);
|
|
360
|
+
session.cleanup.push(() => context.off?.("download", onDownload as never));
|
|
361
|
+
session.listenerCount += 1;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const first = await context.newPage();
|
|
365
|
+
await acceptPage(session, first, "main");
|
|
366
|
+
runs.set(runId, session);
|
|
367
|
+
return session;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function acceptPage(session: RunSession, page: PlaywrightPage, kind: "main" | "popup"): Promise<ManagedPage | undefined> {
|
|
371
|
+
if (session.closed) {
|
|
372
|
+
try {
|
|
373
|
+
await page.close();
|
|
374
|
+
} catch {
|
|
375
|
+
/* ignore */
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
if (kind === "popup") {
|
|
380
|
+
if (session.popupCount >= limits.maxPopups || session.pages.size >= limits.maxPages) {
|
|
381
|
+
try {
|
|
382
|
+
await page.close();
|
|
383
|
+
} catch {
|
|
384
|
+
/* ignore */
|
|
385
|
+
}
|
|
386
|
+
invalidateSnapshot(session);
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
session.popupCount += 1;
|
|
390
|
+
} else if (session.pages.size >= limits.maxPages) {
|
|
391
|
+
try {
|
|
392
|
+
await page.close();
|
|
393
|
+
} catch {
|
|
394
|
+
/* ignore */
|
|
395
|
+
}
|
|
396
|
+
throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `maxPages ${limits.maxPages} exceeded`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const managed: ManagedPage = {
|
|
400
|
+
pageId: nextPageId(),
|
|
401
|
+
page,
|
|
402
|
+
kind,
|
|
403
|
+
closed: false,
|
|
404
|
+
};
|
|
405
|
+
session.pages.set(managed.pageId, managed);
|
|
406
|
+
if (!session.activePageId) session.activePageId = managed.pageId;
|
|
407
|
+
|
|
408
|
+
const onDialog = (dialog: PlaywrightDialog) => {
|
|
409
|
+
session.dialogCount += 1;
|
|
410
|
+
if (session.dialogCount > limits.maxDialogs) {
|
|
411
|
+
void dialog.dismiss().catch(() => undefined);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
session.pendingDialog = dialog;
|
|
415
|
+
};
|
|
416
|
+
const onClose = () => {
|
|
417
|
+
managed.closed = true;
|
|
418
|
+
if (session.activePageId === managed.pageId) {
|
|
419
|
+
session.activePageId = [...session.pages.values()].find((p) => !p.closed)?.pageId;
|
|
420
|
+
}
|
|
421
|
+
invalidateSnapshot(session);
|
|
422
|
+
};
|
|
423
|
+
const onCrash = () => {
|
|
424
|
+
managed.closed = true;
|
|
425
|
+
session.crashed = true;
|
|
426
|
+
invalidateSnapshot(session);
|
|
427
|
+
};
|
|
428
|
+
page.on("dialog", onDialog);
|
|
429
|
+
page.on("close", onClose);
|
|
430
|
+
page.on("crash", onCrash);
|
|
431
|
+
session.listenerCount += 3;
|
|
432
|
+
session.cleanup.push(() => {
|
|
433
|
+
page.off?.("dialog", onDialog as never);
|
|
434
|
+
page.off?.("close", onClose as never);
|
|
435
|
+
page.off?.("crash", onCrash as never);
|
|
436
|
+
});
|
|
437
|
+
invalidateSnapshot(session);
|
|
438
|
+
return managed;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function enqueue<T>(session: RunSession, signal: AbortSignal | undefined, work: () => Promise<T>): Promise<T> {
|
|
442
|
+
if (session.queued >= limits.maxQueuedActions) {
|
|
443
|
+
throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `maxQueuedActions ${limits.maxQueuedActions} exceeded`);
|
|
444
|
+
}
|
|
445
|
+
session.queued += 1;
|
|
446
|
+
session.lastUsedAt = Date.now();
|
|
447
|
+
const run = session.queue.then(async () => {
|
|
448
|
+
throwIfAborted(signal);
|
|
449
|
+
return work();
|
|
450
|
+
});
|
|
451
|
+
session.queue = run.then(
|
|
452
|
+
() => undefined,
|
|
453
|
+
() => undefined,
|
|
454
|
+
);
|
|
455
|
+
try {
|
|
456
|
+
return await run;
|
|
457
|
+
} finally {
|
|
458
|
+
session.queued = Math.max(0, session.queued - 1);
|
|
459
|
+
session.lastUsedAt = Date.now();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function performAction(
|
|
464
|
+
session: RunSession,
|
|
465
|
+
request: BrowserActRequest,
|
|
466
|
+
signal: AbortSignal | undefined,
|
|
467
|
+
): Promise<BrowserActResult> {
|
|
468
|
+
throwIfAborted(signal);
|
|
469
|
+
const action = request.action;
|
|
470
|
+
if (!isActionName(action)) {
|
|
471
|
+
throw new BrowserError("ERR_PRISM_BROWSER_INPUT", `Unsupported action: ${String(action)}`);
|
|
472
|
+
}
|
|
473
|
+
const handler = ACTION_HANDLERS[action];
|
|
474
|
+
if (!handler) {
|
|
475
|
+
throw new BrowserError("ERR_PRISM_BROWSER_INPUT", `Unsupported action: ${action}`);
|
|
476
|
+
}
|
|
477
|
+
const ctx: ActionContext = {
|
|
478
|
+
limits,
|
|
479
|
+
networkPolicy,
|
|
480
|
+
downloads: options.downloads,
|
|
481
|
+
uploads: options.uploads,
|
|
482
|
+
chargeAction,
|
|
483
|
+
maybeSideEffect,
|
|
484
|
+
resultFor,
|
|
485
|
+
resolvePage,
|
|
486
|
+
assertInputBytes,
|
|
487
|
+
invalidateSnapshot,
|
|
488
|
+
safeUrl,
|
|
489
|
+
resolveTargetForAction,
|
|
490
|
+
navigateActive,
|
|
491
|
+
ensurePageCdp,
|
|
492
|
+
toDownloadInfo,
|
|
493
|
+
throwIfAborted,
|
|
494
|
+
};
|
|
495
|
+
return handler(ctx, session, request, signal);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function ensurePageCdp(session: RunSession, page: ManagedPage): Promise<PageCdpState> {
|
|
499
|
+
if (cdpMode === "off") {
|
|
500
|
+
throw new BrowserError("ERR_PRISM_BROWSER_CDP_UNAVAILABLE", "CDP tools are disabled (cdp mode 'off')");
|
|
501
|
+
}
|
|
502
|
+
const existing = session.cdpPages.get(page.pageId);
|
|
503
|
+
if (existing) return existing;
|
|
504
|
+
const cdpSession = await createPageCdpSession(options.browser, session.context, page.page);
|
|
505
|
+
const ring = createObservationRing({
|
|
506
|
+
maxConsoleEntries: limits.maxConsoleEntries,
|
|
507
|
+
maxNetworkRequests: limits.maxNetworkRequests,
|
|
508
|
+
});
|
|
509
|
+
const unsubscribe = installCdpObservation(cdpSession, ring);
|
|
510
|
+
const state: PageCdpState = {
|
|
511
|
+
session: cdpSession,
|
|
512
|
+
ring,
|
|
513
|
+
unsubscribe,
|
|
514
|
+
domainsEnabled: false,
|
|
515
|
+
blockedPatterns: undefined,
|
|
516
|
+
throttled: false,
|
|
517
|
+
emulated: false,
|
|
518
|
+
};
|
|
519
|
+
session.cdpPages.set(page.pageId, state);
|
|
520
|
+
session.cleanup.push(() => {
|
|
521
|
+
try {
|
|
522
|
+
state.unsubscribe();
|
|
523
|
+
} catch {
|
|
524
|
+
/* ignore */
|
|
525
|
+
}
|
|
526
|
+
void cdpSession.detach?.().catch(() => undefined);
|
|
527
|
+
});
|
|
528
|
+
return state;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function resolveTargetForAction(session: RunSession, page: PlaywrightPage, target: BrowserTarget, snapshotId: string | undefined) {
|
|
532
|
+
if ("ref" in target) {
|
|
533
|
+
const live = session.snapshot;
|
|
534
|
+
if (!live || live.snapshotId !== snapshotId) {
|
|
535
|
+
throw new BrowserError("ERR_PRISM_BROWSER_TARGET", "Stale snapshotId; call browser_snapshot before using refs");
|
|
536
|
+
}
|
|
537
|
+
return resolveTargetLocator(page, target, live.refs, live.snapshotId);
|
|
538
|
+
}
|
|
539
|
+
return resolveTargetLocator(page, target, undefined, undefined);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function navigateActive(session: RunSession, url: string, signal: AbortSignal | undefined, pageId?: string): Promise<void> {
|
|
543
|
+
throwIfAborted(signal);
|
|
544
|
+
await assertBrowserUrlAllowed(url, networkPolicy);
|
|
545
|
+
const page = resolvePage(session, pageId);
|
|
546
|
+
await page.page.goto(url, {
|
|
547
|
+
timeout: limits.navigationTimeoutMs,
|
|
548
|
+
waitUntil: "domcontentloaded",
|
|
549
|
+
});
|
|
550
|
+
invalidateSnapshot(session);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function maybeSideEffect(
|
|
554
|
+
session: RunSession,
|
|
555
|
+
action: BrowserActionName | "evaluate" | "observe",
|
|
556
|
+
meta: {
|
|
557
|
+
url?: string;
|
|
558
|
+
pageId?: string;
|
|
559
|
+
paths?: readonly string[];
|
|
560
|
+
resource?: string;
|
|
561
|
+
dialogResponse?: "accept" | "dismiss";
|
|
562
|
+
pageKind?: "main" | "popup";
|
|
563
|
+
},
|
|
564
|
+
): Promise<void> {
|
|
565
|
+
if (!options.beforeSideEffect) return;
|
|
566
|
+
const classified = classifyBrowserOperation(action, {
|
|
567
|
+
dialogResponse: meta.dialogResponse,
|
|
568
|
+
hasUrl: Boolean(meta.url),
|
|
569
|
+
pageKind: meta.pageKind,
|
|
570
|
+
});
|
|
571
|
+
if (!classified.requiresSideEffectHook && !isSideEffectAction(action)) return;
|
|
572
|
+
if (!classified.requiresSideEffectHook) return;
|
|
573
|
+
let origin: string | undefined;
|
|
574
|
+
if (meta.url) {
|
|
575
|
+
try {
|
|
576
|
+
origin = new URL(meta.url).origin;
|
|
577
|
+
} catch {
|
|
578
|
+
origin = undefined;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
await options.beforeSideEffect({
|
|
582
|
+
runId: session.runId,
|
|
583
|
+
action,
|
|
584
|
+
url: meta.url,
|
|
585
|
+
pageId: meta.pageId,
|
|
586
|
+
origin,
|
|
587
|
+
paths: meta.paths,
|
|
588
|
+
resource: meta.resource,
|
|
589
|
+
metadata: {
|
|
590
|
+
action,
|
|
591
|
+
effect: classified.effect,
|
|
592
|
+
risk: classified.risk,
|
|
593
|
+
operation: classified.operation,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function resolvePage(session: RunSession, pageId?: string): ManagedPage {
|
|
599
|
+
const id = pageId ?? session.activePageId;
|
|
600
|
+
if (!id) throw new BrowserError("ERR_PRISM_BROWSER_STATE", "No active page");
|
|
601
|
+
const page = session.pages.get(id);
|
|
602
|
+
if (!page || page.closed || page.page.isClosed()) {
|
|
603
|
+
throw new BrowserError("ERR_PRISM_BROWSER_STATE", `Page ${id} is closed`);
|
|
604
|
+
}
|
|
605
|
+
return page;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function chargeWallTime(session: RunSession): void {
|
|
609
|
+
if (Date.now() - session.createdAt > limits.runWallTimeMs) {
|
|
610
|
+
throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `runWallTimeMs ${limits.runWallTimeMs} exceeded`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function chargeAction(session: RunSession): void {
|
|
615
|
+
if (session.actionCount >= limits.maxActions) {
|
|
616
|
+
throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `maxActions ${limits.maxActions} exceeded`);
|
|
617
|
+
}
|
|
618
|
+
session.actionCount += 1;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function assertInputBytes(value: string): void {
|
|
622
|
+
if (Buffer.byteLength(value, "utf8") > limits.maxActionInputBytes) {
|
|
623
|
+
throw new BrowserError("ERR_PRISM_BROWSER_LIMIT", `action input exceeds maxActionInputBytes ${limits.maxActionInputBytes}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function resultFor(session: RunSession, action: BrowserActionName, page: ManagedPage): BrowserActResult {
|
|
628
|
+
return {
|
|
629
|
+
action,
|
|
630
|
+
pageId: page.pageId,
|
|
631
|
+
url: safeUrl(page.page),
|
|
632
|
+
title: "",
|
|
633
|
+
pages: listPages(session),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function disposeSession(session: RunSession): Promise<void> {
|
|
638
|
+
if (session.closed) return;
|
|
639
|
+
session.closed = true;
|
|
640
|
+
invalidateSnapshot(session);
|
|
641
|
+
await cleanupDownloads(session.downloadBudget).catch(() => undefined);
|
|
642
|
+
for (const dispose of session.cleanup.splice(0)) {
|
|
643
|
+
try {
|
|
644
|
+
dispose();
|
|
645
|
+
} catch {
|
|
646
|
+
/* ignore */
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
session.cdpPages.clear();
|
|
650
|
+
const closePromise = session.context.close();
|
|
651
|
+
const grace = sleep(limits.closeGraceMs);
|
|
652
|
+
await Promise.race([closePromise.catch(() => undefined), grace]);
|
|
653
|
+
try {
|
|
654
|
+
await closePromise;
|
|
655
|
+
} catch {
|
|
656
|
+
/* ignore */
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function summarizeOpen(session: RunSession): BrowserOpenResult {
|
|
661
|
+
const page = resolvePage(session);
|
|
662
|
+
return {
|
|
663
|
+
runId: session.runId,
|
|
664
|
+
pageId: page.pageId,
|
|
665
|
+
url: safeUrl(page.page),
|
|
666
|
+
title: "",
|
|
667
|
+
pages: listPages(session),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function listPages(session: RunSession): BrowserPageInfo[] {
|
|
673
|
+
const out: BrowserPageInfo[] = [];
|
|
674
|
+
for (const page of session.pages.values()) {
|
|
675
|
+
if (page.closed) continue;
|
|
676
|
+
out.push({
|
|
677
|
+
pageId: page.pageId,
|
|
678
|
+
url: safeUrl(page.page),
|
|
679
|
+
title: "",
|
|
680
|
+
active: page.pageId === session.activePageId,
|
|
681
|
+
kind: page.kind,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function toDownloadInfo(meta: {
|
|
688
|
+
downloadId: string;
|
|
689
|
+
suggestedName: string;
|
|
690
|
+
bytes: number;
|
|
691
|
+
sha256: string;
|
|
692
|
+
mimeType?: string;
|
|
693
|
+
released: boolean;
|
|
694
|
+
url: string;
|
|
695
|
+
}): BrowserDownloadInfo {
|
|
696
|
+
return {
|
|
697
|
+
downloadId: meta.downloadId,
|
|
698
|
+
suggestedName: meta.suggestedName,
|
|
699
|
+
bytes: meta.bytes,
|
|
700
|
+
sha256: meta.sha256,
|
|
701
|
+
mimeType: meta.mimeType,
|
|
702
|
+
released: meta.released,
|
|
703
|
+
url: meta.url,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function invalidateSnapshot(session: RunSession): void {
|
|
708
|
+
session.snapshot = undefined;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function safeUrl(page: PlaywrightPage): string {
|
|
712
|
+
try {
|
|
713
|
+
return page.url().slice(0, 2_048);
|
|
714
|
+
} catch {
|
|
715
|
+
return "";
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function assertRunId(runId: string): string {
|
|
720
|
+
if (typeof runId !== "string" || !runId || Buffer.byteLength(runId, "utf8") > 256) {
|
|
721
|
+
throw new BrowserError("ERR_PRISM_BROWSER_INPUT", "runId must be a non-empty string ≤256 bytes");
|
|
722
|
+
}
|
|
723
|
+
return runId;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
727
|
+
if (signal?.aborted) {
|
|
728
|
+
throw new BrowserError("ERR_PRISM_BROWSER", "Operation aborted");
|
|
729
|
+
}
|
|
730
|
+
}
|