Skip to content

Attach screenshots (CDP) + ESM build fix + idempotent claim#37

Open
jddunn wants to merge 1 commit into
masterfrom
feat/attach-screenshot
Open

Attach screenshots (CDP) + ESM build fix + idempotent claim#37
jddunn wants to merge 1 commit into
masterfrom
feat/attach-screenshot

Conversation

@jddunn

@jddunn jddunn commented Jul 25, 2026

Copy link
Copy Markdown
Member

Adds browser_attach_screenshot over CDP Page.captureScreenshot (threaded through the daemon protocol/client/surface). The JXA display-capture fallback is deliberately disabled by default — its crop drifted onto neighbouring windows in live testing, which would leak private windows into evidence files; CDP is the correct page-pixel path.

Two real defects found while proving it live:

  • claimAgentTab was not idempotent → every attach failed NO_BLANK_TAB (probeIdentity already claimed the tab).
  • package emitted CommonJS while the source is ESM → all dist imports failed at runtime. Now type: module + Node16, matching sibling curated exts.

Summary by Sourcery

Introduce a CDP-backed screenshot capability for the browser attach surface and wire it through tools and daemon, while fixing attach tab claiming and aligning the package with ESM/Node16 builds.

New Features:

  • Add optional screenshot capture support to the attach controller and daemon, exposing it via a new browser_attach_screenshot tool and daemon client method.
  • Implement CDP-based Page.captureScreenshot in the RawCdpBackend, including optional full-page capture.
  • Extend the JXA backend with a raise command and an opt-in window-bounded display screenshot path guarded behind an environment flag.

Bug Fixes:

  • Make JxaBackend.claimAgentTab idempotent so repeated claims reuse the existing tab instead of failing with NO_BLANK_TAB.

Enhancements:

  • Tighten daemon protocol and surface to handle screenshot operations with validation and structured errors.
  • Refine attach tools tests and daemon surface typing to cover the new screenshot tool and ensure stable tool registration.

Build:

  • Switch TypeScript compilation to ES2022/Node16 module and resolution targets for browser-automation.
  • Mark the browser-automation package as type: module to emit ESM-compatible dist output.

Tests:

  • Update attach tools and pack registration tests to account for the new screenshot tool id and behavior, including daemon tool execution and status handling.

Summary by CodeRabbit

  • New Features
    • Added a new browser attachment tool for capturing PNG screenshots of the active tab.
    • Supports standard and full-page screenshots, with the saved file path and size returned.
    • Added screenshot support across daemon-backed browser automation workflows.
  • Bug Fixes
    • Improved repeated agent-tab claiming to avoid unnecessary failures.
    • Added clearer handling when screenshot support or required permissions are unavailable.
  • Compatibility
    • Updated the package to use modern ECMAScript module settings.

…ild fix

browser_attach_screenshot writes a PNG of the agent tab for research evidence,
implemented over CDP Page.captureScreenshot (page pixels — no coordinate math,
no focus steal, no other windows). Threaded through the daemon protocol/client/
surface so it works on the IPC lane too.

The JXA lane's screencapture fallback is present but DISABLED by default: its
-R crop works in display points while Chrome bounds + Retina scale disagree, so
the region drifted onto neighbouring windows in live testing — unacceptable for
an evidence file. Opt in with WUNDERLAND_ATTACH_JXA_SHOTS=1 only.

Also fixes two real defects found while proving this live:
- JxaBackend.claimAgentTab is now idempotent: probeIdentity() claims a tab
  first, so the controller's own claim consumed a SECOND about:blank and failed
  NO_BLANK_TAB on every attach.
- package/tsconfig emit ESM (type: module, Node16) matching sibling curated
  extensions; the CommonJS emit made every dist import fail at runtime.
@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a CDP-based screenshot capture pipeline for the browser-automation attach system, fixes idempotency of tab claiming in the JXA backend, and converts the package to a proper ESM/Node16 build configuration with protocol and test updates.

Sequence diagram for CDP-based browser_attach_screenshot pipeline

sequenceDiagram
  actor Agent
  participant AttachScreenshotTool
  participant AttachSurface as DaemonAttachSurface
  participant DaemonClient as AttachDaemonClient
  participant Daemon as AttachDaemon
  participant Controller as AttachController
  participant Backend as RawCdpBackend

  Agent->>AttachScreenshotTool: execute({ path, fullPage })
  AttachScreenshotTool->>AttachSurface: screenshot(path, fullPage)
  AttachSurface->>DaemonClient: screenshot(path, fullPage)
  DaemonClient->>Daemon: request('screenshot', { path, fullPage })
  Daemon->>Controller: screenshot(a.path, a.fullPage)
  Controller->>Backend: screenshotTab(tab, fullPage)
  Backend->>Backend: send('Page.captureScreenshot', { format: 'png', captureBeyondViewport })
  Backend-->>Controller: base64 PNG data
  Controller->>Controller: writeFileSync(filePath, buf)
  Controller-->>Daemon: { path, bytes }
  Daemon-->>DaemonClient: { path, bytes }
  DaemonClient-->>AttachSurface: { path, bytes }
  AttachSurface-->>AttachScreenshotTool: { path, bytes }
  AttachScreenshotTool-->>Agent: tool envelope with result
Loading

File-Level Changes

Change Details Files
Add CDP-backed screenshot capture capability throughout the attach stack and expose it as a tool and daemon operation.
  • Extend AttachBackend and AttachSurface interfaces with optional screenshot methods returning base64 PNGs or writing PNGs to disk.
  • Implement RawCdpBackend.screenshotTab using Page.captureScreenshot with optional full-page capture via captureBeyondViewport.
  • Add AttachController.screenshot to enforce lease, deadlines, dry-run behavior, and filesystem writes for screenshots.
  • Introduce AttachScreenshotTool and register it in createAttachTools and ATTACH_TOOL_IDS, wiring it through the daemon client, daemon command handling, and daemon surface.
  • Update tests to cover the new screenshot tool id, tool count, daemon tool typing, pack registration, and daemon protocol AttachOp union.
registry/curated/system/browser-automation/src/attach/AttachController.ts
registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts
registry/curated/system/browser-automation/src/attach/tools.ts
registry/curated/system/browser-automation/src/attach/__tests__/tools.test.ts
registry/curated/system/browser-automation/src/attach/__tests__/pack-registration.test.ts
registry/curated/system/browser-automation/src/attach/daemon/client.ts
registry/curated/system/browser-automation/src/attach/daemon/daemon.ts
registry/curated/system/browser-automation/src/attach/daemon/surface.ts
registry/curated/system/browser-automation/src/attach/daemon/protocol.ts
Fix JXA backend tab claiming to be idempotent and add a guarded, opt-in display capture path that remains disabled by default for privacy reasons.
  • Make JxaBackend.claimAgentTab reuse an already-claimed tab when available to avoid NO_BLANK_TAB failures caused by double-claiming.
  • Extend the JXA driver script with a raise command that brings the agent window to the front, optionally focuses the agent tab, and returns window bounds for cropping.
  • Implement JxaBackend.screenshotTab using macOS screencapture with region cropping based on window bounds, temporary file handling, and structured AttachError failures, gated behind WUNDERLAND_ATTACH_JXA_SHOTS env flag and refusing when bounds are unavailable.
registry/curated/system/browser-automation/src/attach/backends/jxa.ts
Switch the browser-automation package to ESM/Node16 so built artifacts match ESM source and sibling curated extensions.
  • Update tsconfig compilerOptions to target ES2022, use Node16 module and moduleResolution, and ES2022 lib.
  • Mark the package as type: module in package.json to ensure Node treats dist output as ESM.
registry/curated/system/browser-automation/tsconfig.json
registry/curated/system/browser-automation/package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The attach package adds screenshot capture through controller, CDP and JXA backends, daemon transport, and a new browser_attach_screenshot tool. It also updates attach tests and switches the package to ES module and Node16 TypeScript settings.

Changes

Screenshot capture

Layer / File(s) Summary
Controller screenshot contract
registry/curated/system/browser-automation/src/attach/AttachController.ts
Adds screenshot capabilities to attach interfaces and implements readiness, lease, dry-run, file-writing, state, and error handling.
Backend screenshot implementations
registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts, registry/curated/system/browser-automation/src/attach/backends/jxa.ts
Adds CDP PNG capture and opt-in JXA window raising, cropping, encoding, and cleanup.
Daemon screenshot routing
registry/curated/system/browser-automation/src/attach/daemon/*
Adds the screenshot protocol operation and routes validated requests through the daemon client, surface, and controller.
Tool registration and package configuration
registry/curated/system/browser-automation/src/attach/tools.ts, registry/curated/system/browser-automation/src/attach/__tests__/*, registry/curated/system/browser-automation/package.json, registry/curated/system/browser-automation/tsconfig.json
Registers browser_attach_screenshot, updates tool tests, and configures ES modules with ES2022 and Node16 settings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AttachScreenshotTool
  participant DaemonAttachSurface
  participant AttachDaemonClient
  participant AttachDaemon
  participant AttachController
  participant RawCdpBackend
  AttachScreenshotTool->>DaemonAttachSurface: screenshot(path, fullPage)
  DaemonAttachSurface->>AttachDaemonClient: screenshot(path, fullPage)
  AttachDaemonClient->>AttachDaemon: queue screenshot operation
  AttachDaemon->>AttachController: screenshot(path, fullPage)
  AttachController->>RawCdpBackend: screenshotTab(tab, fullPage)
  RawCdpBackend-->>AttachController: base64 PNG
  AttachController-->>AttachDaemon: path and bytes
  AttachDaemon-->>AttachScreenshotTool: screenshot result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: CDP screenshots, ESM build fixes, and idempotent claim behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/attach-screenshot

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The AttachBackend.screenshotTab and related surface/tool docs describe the capability as CDP-only and non-focusing, but the JXA backend now implements screenshotTab and explicitly focuses the window; consider reconciling these comments with the actual behavior or scoping the JXA implementation differently.
  • In the daemon screenshot handler, invalid or missing path currently returns an UNKNOWN error code; using a more specific validation-style code (e.g., something akin to BAD_REQUEST/INVALID_ARG) would make error handling and diagnostics clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `AttachBackend.screenshotTab` and related surface/tool docs describe the capability as CDP-only and non-focusing, but the JXA backend now implements `screenshotTab` and explicitly focuses the window; consider reconciling these comments with the actual behavior or scoping the JXA implementation differently.
- In the daemon `screenshot` handler, invalid or missing `path` currently returns an `UNKNOWN` error code; using a more specific validation-style code (e.g., something akin to `BAD_REQUEST`/`INVALID_ARG`) would make error handling and diagnostics clearer.

## Individual Comments

### Comment 1
<location path="registry/curated/system/browser-automation/src/attach/backends/jxa.ts" line_range="221-230" />
<code_context>
+    const file = join(mkdtempSync(join(tmpdir(), 'attach-shot-')), 'shot.png');
</code_context>
<issue_to_address>
**suggestion (performance):** Temporary directories created for screenshots are never removed, which can accumulate over time.

Each screenshot call creates a new temp directory via `mkdtempSync`, but only `shot.png` is deleted, leaving the directory in `/tmp`. Over many runs this can lead to unnecessary disk usage, especially for long-lived processes. Please also remove the temp directory returned by `mkdtempSync` (e.g. `rmSync(dir, { recursive: true, force: true })`) in the `finally` block.

Suggested implementation:

```typescript
    const { mkdtempSync, readFileSync, existsSync, rmSync } = await import('node:fs');
    const { tmpdir } = await import('node:os');
    const dir = mkdtempSync(join(tmpdir(), 'attach-shot-'));
    const file = join(dir, 'shot.png');

```

You’ll also need to update the surrounding error-handling/cleanup logic:

1. In the `finally` block that currently deletes `shot.png` (if present), add `rmSync(dir, { recursive: true, force: true });` to remove the temporary directory.
2. If there is no `finally` block yet, wrap the screenshot capture and file reading logic in a `try`/`finally` and in `finally`:
   - Check `existsSync(file)` and delete it as currently done.
   - Call `rmSync(dir, { recursive: true, force: true });` to ensure the temp directory is removed even if the file was never written.
3. Ensure `dir` is in scope for that `finally` block (i.e. declared in the same function scope rather than inside a narrower block).
</issue_to_address>

### Comment 2
<location path="registry/curated/system/browser-automation/src/attach/tools.ts" line_range="118-122" />
<code_context>
+    'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
+  readonly category = 'browser';
+  readonly version = '0.1.0';
+  readonly hasSideEffects = false;
+  readonly inputSchema = {
+    type: 'object' as const,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The screenshot tool writes a file to disk, which is a side effect despite being read-only from the page’s perspective.

Although the operation is read-only for page state, `browser_attach_screenshot` still writes a PNG to disk. If `hasSideEffects` is meant to capture any external state changes (not just page mutations), this should be `true` or the semantics of `hasSideEffects` should be clarified to avoid upstream tools misclassifying this call as purely observational.

```suggestion
  readonly description =
    'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only with respect to page state (never navigates or mutates the page), but writes a PNG file to disk.';
  readonly category = 'browser';
  readonly version = '0.1.0';
  readonly hasSideEffects = true;
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +221 to +230
const file = join(mkdtempSync(join(tmpdir(), 'attach-shot-')), 'shot.png');
// Crop to the agent WINDOW's bounds. A full-display capture would include
// every other window the user has open — private content must never land
// in a research evidence shot.
let region: string | undefined;
try {
const b = JSON.parse(raised) as { x: number; y: number; width: number; height: number };
if ([b.x, b.y, b.width, b.height].every((n) => typeof n === 'number' && Number.isFinite(n)) && b.width > 0 && b.height > 0) {
region = `${Math.max(0, Math.round(b.x))},${Math.max(0, Math.round(b.y))},${Math.round(b.width)},${Math.round(b.height)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Temporary directories created for screenshots are never removed, which can accumulate over time.

Each screenshot call creates a new temp directory via mkdtempSync, but only shot.png is deleted, leaving the directory in /tmp. Over many runs this can lead to unnecessary disk usage, especially for long-lived processes. Please also remove the temp directory returned by mkdtempSync (e.g. rmSync(dir, { recursive: true, force: true })) in the finally block.

Suggested implementation:

    const { mkdtempSync, readFileSync, existsSync, rmSync } = await import('node:fs');
    const { tmpdir } = await import('node:os');
    const dir = mkdtempSync(join(tmpdir(), 'attach-shot-'));
    const file = join(dir, 'shot.png');

You’ll also need to update the surrounding error-handling/cleanup logic:

  1. In the finally block that currently deletes shot.png (if present), add rmSync(dir, { recursive: true, force: true }); to remove the temporary directory.
  2. If there is no finally block yet, wrap the screenshot capture and file reading logic in a try/finally and in finally:
    • Check existsSync(file) and delete it as currently done.
    • Call rmSync(dir, { recursive: true, force: true }); to ensure the temp directory is removed even if the file was never written.
  3. Ensure dir is in scope for that finally block (i.e. declared in the same function scope rather than inside a narrower block).

Comment on lines +118 to +122
readonly description =
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The screenshot tool writes a file to disk, which is a side effect despite being read-only from the page’s perspective.

Although the operation is read-only for page state, browser_attach_screenshot still writes a PNG to disk. If hasSideEffects is meant to capture any external state changes (not just page mutations), this should be true or the semantics of hasSideEffects should be clarified to avoid upstream tools misclassifying this call as purely observational.

Suggested change
readonly description =
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = false;
readonly description =
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only with respect to page state (never navigates or mutates the page), but writes a PNG file to disk.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = true;

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92372f54b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate screenshot file writes as side effects

When a mission uses the documented deny-side-effects policy, this tool remains callable even though AttachController.screenshot() creates directories and overwrites the caller-selected path with writeFileSync. Because the path can name any file writable by the agent process, an unapproved invocation can truncate user files while bypassing the policy intended to gate writes; mark this tool as side-effecting and consider constraining or refusing existing destinations.

Useful? React with 👍 / 👎.

Comment on lines +218 to +220
'Page.captureScreenshot',
{ format: 'png', captureBeyondViewport: !!fullPage },
this.sessionId!,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute a content-sized clip for full-page captures

When fullPage is true on a document taller than the viewport, this still captures only the viewport: captureBeyondViewport permits a requested clip to extend outside the viewport, but it does not expand the default screenshot area by itself. Query the layout/content dimensions and pass an appropriate clip (or temporarily resize the viewport) so the advertised full-page option includes the entire scrollable page.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@registry/curated/system/browser-automation/src/attach/AttachController.ts`:
- Around line 287-302: Constrain the file-writing logic in screenshot to a
configured evidence directory before creating the output: reject traversal,
symlinked paths, special files, and existing non-regular targets, and create the
result exclusively and atomically. Preserve dry-run behavior and return the
resolved safe destination and byte count after writing.

In `@registry/curated/system/browser-automation/src/attach/backends/jxa.ts`:
- Around line 203-245: Remove the WUNDERLAND_ATTACH_JXA_SHOTS opt-in escape
hatch from screenshotTab so the JXA screenshot implementation always rejects the
operation. Keep the existing AttachError response and do not execute raise,
bounds resolution, or screencapture while this unsafe fallback remains
unavailable.

In `@registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts`:
- Around line 215-221: Update screenshotTab to cap full-page capture dimensions
before calling Page.captureScreenshot, using the page layout or pixel bounds to
prevent oversized PNG responses. Preserve normal viewport screenshots, and
ensure the cap is applied before the base64 result is held or converted to a
Buffer.

In `@registry/curated/system/browser-automation/src/attach/tools.ts`:
- Around line 119-122: Update the screenshot tool’s hasSideEffects property to
indicate that capturing a screenshot is side-effecting because it creates
directories and writes a PNG file. Keep the existing category and version
unchanged so confirmation and policy controls apply to this tool’s local
filesystem writes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a65f1ff-4303-428c-9a31-bda684aa0535

📥 Commits

Reviewing files that changed from the base of the PR and between 5b0c67d and 92372f5.

📒 Files selected for processing (12)
  • registry/curated/system/browser-automation/package.json
  • registry/curated/system/browser-automation/src/attach/AttachController.ts
  • registry/curated/system/browser-automation/src/attach/__tests__/pack-registration.test.ts
  • registry/curated/system/browser-automation/src/attach/__tests__/tools.test.ts
  • registry/curated/system/browser-automation/src/attach/backends/jxa.ts
  • registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts
  • registry/curated/system/browser-automation/src/attach/daemon/client.ts
  • registry/curated/system/browser-automation/src/attach/daemon/daemon.ts
  • registry/curated/system/browser-automation/src/attach/daemon/protocol.ts
  • registry/curated/system/browser-automation/src/attach/daemon/surface.ts
  • registry/curated/system/browser-automation/src/attach/tools.ts
  • registry/curated/system/browser-automation/tsconfig.json

Comment on lines +287 to +302
async screenshot(filePath: string, fullPage?: boolean): Promise<{ path: string; bytes: number }> {
this.requireReady();
requireLease(this.opts.leaseFile, this.lease!.nonce);
const fn = this.backend.screenshotTab?.bind(this.backend);
if (!fn) {
throw new AttachError('UNSUPPORTED_OP', `${this.backend.kind} backend has no screenshot capability (CDP transport required)`);
}
if (this.dryRun) return { path: filePath, bytes: 0 };
this.machine.to('reading');
try {
const b64 = await withDeadline(fn(this.tab!, fullPage), this.deadline, 'screenshot');
const { writeFileSync, mkdirSync } = await import('node:fs');
const { dirname } = await import('node:path');
mkdirSync(dirname(filePath), { recursive: true });
const buf = Buffer.from(b64, 'base64');
writeFileSync(filePath, buf);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Constrain screenshot destinations before writing.

Line 300 writes a claimant-supplied path as the daemon user. Since daemon validation only checks that path is non-empty, a caller can overwrite arbitrary files or target special files. Restrict output to a configured evidence directory and reject traversal, symlinks, and non-regular targets; use exclusive/atomic creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@registry/curated/system/browser-automation/src/attach/AttachController.ts`
around lines 287 - 302, Constrain the file-writing logic in screenshot to a
configured evidence directory before creating the output: reject traversal,
symlinked paths, special files, and existing non-regular targets, and create the
result exclusively and atomically. Preserve dry-run behavior and return the
resolved safe destination and byte count after writing.

Comment on lines +203 to +245
async screenshotTab(tab: string, _fullPage?: boolean): Promise<string> {
// OFF BY DEFAULT. `screencapture -R` works in display POINTS while Chrome's
// window bounds and Retina backing scale disagree, so the cropped region
// drifted onto NEIGHBOURING windows in live testing (2026-07-24) — which
// would silently put the user's unrelated, private windows into a research
// evidence file. Correct page-pixel capture is CDP `Page.captureScreenshot`
// (see RawCdpBackend); this lane stays refused unless explicitly opted in
// via WUNDERLAND_ATTACH_JXA_SHOTS=1 for a single-display debug session.
if (process.env.WUNDERLAND_ATTACH_JXA_SHOTS !== '1') {
throw new AttachError(
'UNSUPPORTED_OP',
'screenshots require the CDP transport (WUNDERLAND_ATTACH_TRANSPORT=cdp); the JXA display-capture lane is disabled because its crop can include other windows',
);
}
const [wid, tid] = tab.split(/\s+/);
const raised = await this.exec('raise', [wid, tid]);
const { mkdtempSync, readFileSync, existsSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const file = join(mkdtempSync(join(tmpdir(), 'attach-shot-')), 'shot.png');
// Crop to the agent WINDOW's bounds. A full-display capture would include
// every other window the user has open — private content must never land
// in a research evidence shot.
let region: string | undefined;
try {
const b = JSON.parse(raised) as { x: number; y: number; width: number; height: number };
if ([b.x, b.y, b.width, b.height].every((n) => typeof n === 'number' && Number.isFinite(n)) && b.width > 0 && b.height > 0) {
region = `${Math.max(0, Math.round(b.x))},${Math.max(0, Math.round(b.y))},${Math.round(b.width)},${Math.round(b.height)}`;
}
} catch {
/* older driver without bounds → refuse rather than capture the screen */
}
if (!region) {
throw new AttachError('UNSUPPORTED_OP', 'could not resolve agent-window bounds; refusing a full-display capture');
}
try {
await pExecFile('screencapture', ['-x', '-o', '-t', 'png', '-R', region, file], { timeout: 20_000 });
if (!existsSync(file)) {
throw new AttachError('UNSUPPORTED_OP', 'screencapture produced no file (Screen Recording permission?)');
}
return readFileSync(file).toString('base64');
} catch (err) {
if (err instanceof AttachError) throw err;
throw new AttachError('UNSUPPORTED_OP', `screencapture failed: ${err instanceof Error ? err.message : String(err)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not expose the known-unsafe JXA capture fallback.

The implementation acknowledges that its point/pixel mismatch can capture neighboring private windows, then enables that same path via an environment variable. Keep screenshotTab unavailable for JXA until bounds can be converted and validated reliably; an opt-in flag is not sufficient containment for an exposed evidence-writing tool.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@registry/curated/system/browser-automation/src/attach/backends/jxa.ts` around
lines 203 - 245, Remove the WUNDERLAND_ATTACH_JXA_SHOTS opt-in escape hatch from
screenshotTab so the JXA screenshot implementation always rejects the operation.
Keep the existing AttachError response and do not execute raise, bounds
resolution, or screencapture while this unsafe fallback remains unavailable.

Comment on lines +215 to +221
async screenshotTab(tab: string, fullPage?: boolean): Promise<string> {
this.requireTab(tab);
const res = (await this.send(
'Page.captureScreenshot',
{ format: 'png', captureBeyondViewport: !!fullPage },
this.sessionId!,
45_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== raw-cdp.ts around screenshotTab ==\n'
sed -n '180,280p' registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts

printf '\n== search for screenshot-related guards and metrics ==\n'
rg -n "captureBeyondViewport|Page\.getLayoutMetrics|layoutMetrics|screenshotTab|captureScreenshot|clip|pixel budget|fullPage" registry/curated/system/browser-automation/src -S

Repository: framerslab/agentos-extensions

Length of output: 8311


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,260p' registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts

Repository: framerslab/agentos-extensions

Length of output: 10155


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '\n== raw-cdp.ts around screenshotTab ==\n'
sed -n '180,280p' registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts

printf '\n== search for screenshot-related guards and metrics ==\n'
rg -n "captureBeyondViewport|Page\.getLayoutMetrics|layoutMetrics|screenshotTab|captureScreenshot|clip|pixel budget|fullPage" registry/curated/system/browser-automation/src -S

Repository: framerslab/agentos-extensions

Length of output: 8311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== AttachController screenshot flow ==\n'
sed -n '260,340p' registry/curated/system/browser-automation/src/attach/AttachController.ts

printf '\n== daemon/client screenshot path ==\n'
sed -n '100,150p' registry/curated/system/browser-automation/src/attach/daemon/client.ts
sed -n '240,290p' registry/curated/system/browser-automation/src/attach/daemon/daemon.ts

printf '\n== search for screenshot limits / byte caps ==\n'
rg -n "screenshot.*(limit|max|cap|budget|bytes|size)|base64|Buffer\\.from\\(.*base64|captureBeyondViewport|fullPage" registry/curated/system/browser-automation/src -S

Repository: framerslab/agentos-extensions

Length of output: 13686


Cap full-page screenshot output.

captureBeyondViewport can turn a tall page into a huge PNG, and this path immediately holds the result as base64 before writing it as a Buffer. Add a layout/pixel cap before allowing full-page capture so a single request can’t exhaust Chrome or the daemon.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@registry/curated/system/browser-automation/src/attach/backends/raw-cdp.ts`
around lines 215 - 221, Update screenshotTab to cap full-page capture dimensions
before calling Page.captureScreenshot, using the page layout or pixel bounds to
prevent oversized PNG responses. Preserve normal viewport screenshots, and
ensure the cap is applied before the base64 result is held or converted to a
Buffer.

Comment on lines +119 to +122
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Mark the screenshot tool as side-effecting.

This tool creates directories and writes a PNG, but Line 122 declares hasSideEffects = false. That can bypass tool confirmation or policy controls intended for local writes.

Proposed fix
-  readonly hasSideEffects = false;
+  readonly hasSideEffects = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = false;
'Capture a PNG screenshot of the agent tab to a local file path — visual evidence for a research report. Requires the CDP transport (the AppleScript/JXA transport has no pixel access and refuses with UNSUPPORTED_OP). Read-only: never navigates or mutates the page.';
readonly category = 'browser';
readonly version = '0.1.0';
readonly hasSideEffects = true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@registry/curated/system/browser-automation/src/attach/tools.ts` around lines
119 - 122, Update the screenshot tool’s hasSideEffects property to indicate that
capturing a screenshot is side-effecting because it creates directories and
writes a PNG file. Keep the existing category and version unchanged so
confirmation and policy controls apply to this tool’s local filesystem writes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant