Skip to content

Validate observed Action target identity before act - #2439

Open
antonvishal wants to merge 5 commits into
browserbase:restore-act-actionfrom
antonvishal:action-target
Open

Validate observed Action target identity before act#2439
antonvishal wants to merge 5 commits into
browserbase:restore-act-actionfrom
antonvishal:action-target

Conversation

@antonvishal

@antonvishal antonvishal commented Jul 26, 2026

Copy link
Copy Markdown

Summary

  • Stacked on Restore Action inputs to act #2427. Observe→act can still report success when a selector resolves after a rerender but now targets a different DOM node.
  • Observed Actions gain optional target / argumentTargets (frameOrdinal + backendNodeId). Deterministic act validates identity before mutation and falls back to existing self-healing on mismatch.
  • Actions without target keep prior behavior. Cache replay preserves target metadata when present.

Test plan

  • observeact(Action) after a list reorder / remount: mismatch fails or self-heals instead of clicking the wrong row
  • Actions without target still execute as before
  • Drag-and-drop validates destination via argumentTargets
  • Cached act/observe actions retain/rebind targets; malformed target metadata is dropped without breaking replay
  • Server unit tests: act, observe, action-target-validation, locator-target-guard, cache-actions

Summary by cubic

Validate observed action target identity before executing actions and rebind targets on cache hits to avoid acting on the wrong element after rerenders. Adds guard rails across clicks, drag-and-drop, key press, and mouse wheel with hit-tests; targetless press/wheel keep legacy behavior and guarded mismatches fail closed with clear errors.

  • New Features

    • Protocol: added ActionTarget; Action supports optional target and argumentTargets.
    • Server/runtime: locator target guard validates backend node and hit-tests before input; press/mouse.wheel resolve/focus only when guarded; dragAndDrop rechecks before coordinate drag; mismatches throw ActionTargetMismatchError with optional self-heal.
    • Observe/cache/replay: emit target metadata, normalize root / to /html[1] and text-node XPaths to actionable elements, and rebind targets on observe cache hits and act replays; malformed target metadata is ignored; rebind failures throw CachedActionRebindError.
    • SDKs: TS and Python export ActionTarget; tests updated across protocol, server, and SDKs.
  • Migration

    • No breaking changes; no action required. Actions without target keep prior behavior.

Written for commit 892a139. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 892a139

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Jul 26, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 20 files

Architecture diagram
sequenceDiagram
    participant Client as Client SDK
    participant Act as actService
    participant Observe as observeService
    participant Cache as cacheService
    participant ActHandler as actHandlerUtils
    participant Locator as Locator
    participant CDP as CDP (Browser)
    participant Snapshot as Snapshot Engine

    Note over Client,Snapshot: NEW: Target Identity Validation Flow

    Client->>Observe: observe(pageId, options)
    Observe->>Snapshot: captureSnapshot()
    Snapshot-->>Observe: combinedXpathMap
    Observe->>Observe: build actions with target from xpathMap
    Observe-->>Client: actions[] with optional target

    Client->>Act: act(params with action including target)
    Act->>Cache: check cache
    alt Cache HIT
        Cache-->>Act: cached actions
        Act->>Act: rebind targets to current snapshot
        Act->>Snapshot: captureSnapshot()
        Snapshot-->>Act: current xpathMap
        Act->>Act: update: action.target = new backendNodeId
    else Cache MISS
        Act->>Act: proceed with original action
    end

    Act->>ActHandler: performUnderstudyMethod(page, frame, method, selector, args, logger, domSettleTimeoutMs, targetConstraints)

    ActHandler->>ActHandler: resolveLocatorWithHops()
    ActHandler->>Locator: new Locator(selector)
    alt Action has target
        ActHandler->>Locator: locator.withTargetGuard(expected, frameOrdinal)
        Note over Locator: binds expected frameOrdinal + backendNodeId
    end

    alt Action type: click, fill, etc.
        ActHandler->>Locator: click(), fill(), etc.
        Locator->>Locator: resolveElement()
        Locator->>CDP: DOM.querySelector / DOM.querySelectorAll
        CDP-->>Locator: objectId
        alt Target guard exists
            Locator->>CDP: DOM.describeNode(objectId)
            CDP-->>Locator: node.backendNodeId
            Locator->>Locator: compare actual vs expected
            alt Expected matches actual
                Locator->>CDP: Input.dispatchMouseEvent / DOM.setInputValue
            else Mismatch
                Locator-->>ActHandler: throw ActionTargetMismatchError
                ActHandler-->>Act: error propagates
                alt Self-heal enabled
                    Act->>Act: selfHealAction()
                    Act->>Locator: resolve with new selector
                    Act->>ActHandler: performUnderstudyMethod(updated selector)
                else Self-heal disabled
                    Act-->>Client: { success: false, error: "target changed" }
                end
            end
        else No target guard
            Locator->>CDP: Input.dispatchMouseEvent (legacy)
        end
    else Action type: dragAndDrop
        ActHandler->>ActHandler: resolveLocatorWithHops(targetXpath)
        alt Destination has target
            ActHandler->>Locator: locator.withTargetGuard(destinationTarget)
        end
        ActHandler->>Locator: dragAndDrop(locator, targetLocator)
        Locator->>Locator: validate both source and destination targets
        Locator->>CDP: Input.dispatchDragEvent
    end

    Note over Locator,CDP: Validation happens before DOM mutation

    alt Action succeeds
        Locator-->>ActHandler: success
        ActHandler-->>Act: { success: true, actions[] with target metadata }
        Act-->>Client: { result: { success: true, actions } }
    else Action fails validation
        ActHandler-->>Act: error
        alt Self-heal
            Act->>Act: selfHealAction()
            Act->>CDP: capture new snapshot + LLM inference
            CDP-->>Act: new selector + target
            Act->>ActHandler: performUnderstudyMethod(new selector, new targetConstraints)
            ActHandler-->>Act: success
            Act-->>Client: { result: { success: true, actions } }
        end
    end

    Note over Cache,Act: Cache Replay with Target Rebind

    Client->>Observe: observe(cached=true)
    Observe->>Cache: get cached actions
    Cache-->>Observe: actions with old target metadata
    Observe->>Snapshot: captureSnapshot()
    Snapshot-->>Observe: current xpathMap
    Observe->>Observe: for each action, find matching xpath in current map
    Observe->>Observe: update action.target = new backendNodeId
    alt Drag-and-drop
        Observe->>Observe: also rebind argumentTargets["0"]
    end
    Observe-->>Client: rebound actions[] with current targets

    Note over Cache: Malformed Target Handling

    Cache->>Cache: normalizeCachedActions()
    alt target has valid schema
        Cache->>Cache: preserve target
    else target invalid (negative frameOrdinal, zero backendNodeId, etc.)
        Cache->>Cache: drop target metadata
    end
    alt argumentTargets has non-numeric keys
        Cache->>Cache: drop invalid entries
    end
    Cache-->>Client: actions with valid targets only

    Note over Client,SDK: Client-Side Type Validation

    Client->>Client: parse action with ActionTargetSchema
    alt target passes validation
        Client-->>Act: send full action with target
    else target fails (negative frameOrdinal, zero backendNodeId, non-numeric keys)
        Client-->>Client: throw validation error
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/server/services/actService.ts
Comment thread packages/server/understudy/locator.ts
Comment thread packages/server/handlers/handlerUtils/actHandlerUtils.ts
Comment thread packages/server/services/observeService.ts
Comment thread packages/server/actionTarget.ts Outdated
Comment thread packages/server/services/cacheService.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 9 files (changes from recent commits).

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread packages/server/services/observeService.ts
Comment thread packages/server/understudy/locator.ts
Comment thread packages/server/handlers/handlerUtils/actHandlerUtils.ts
Comment thread packages/server/actionTarget.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/server/tests/action-target.test.ts Outdated
Comment thread packages/server/handlers/handlerUtils/actHandlerUtils.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants