Skip to content

feat(core): bind browser operations to exact protocol capabilities - #138

Closed
seonghobae wants to merge 22 commits into
test/browser-context-origin-epoch-protocol-dispatchfrom
test/browser-typed-operation-capability-binding
Closed

feat(core): bind browser operations to exact protocol capabilities#138
seonghobae wants to merge 22 commits into
test/browser-context-origin-epoch-protocol-dispatchfrom
test/browser-typed-operation-capability-binding

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Partial implementation of #28, stacked on exact current PR #127 head 65f51937ff2af64d9c157666e7c3b7df89cbb89a.

Buyer/security gap

The strongest current context/origin/document-epoch dispatch boundary previously accepted a caller-selected raw BrowserProtocolCapability; this branch makes the buyer-visible operation explicit and derives the exact adapter capability internally. It also adds the first bounded WebDriver BiDi accessibility-query value needed to move QueryNodes from a coarse operation label toward a real semantic observation adapter without exposing CSS/XPath/script/raw-DOM authority.

Current dependency / stack

Current exact #138 head is 7fc113d26f864ac14e6793409d79328bbe954a2c. Exact live prerequisite #127 is 65f51937ff2af64d9c157666e7c3b7df89cbb89a.

Fresh ancestry is ahead 14 / behind 0, with merge base exactly the live prerequisite. The live-base delta is limited to five paths: CHANGELOG.md, crates/originweave-core/src/browser_protocol_operation.rs, crates/originweave-core/src/lib.rs, crates/originweave-core/tests/browser_typed_operation_protocol_dispatch.rs, and crates/originweave-core/tests/webdriver_bidi_accessibility_query.rs. Keep Draft while #127 and its prerequisite stack remain active. No prerequisite check, review, status, coverage, mergeability, synthetic-merge, or predecessor evidence transfers.

TDD / RCA sequence

Earlier test-first lineage established typed operation-to-capability binding and same-call dispatch before production support, then made that boundary exact-green.

For the buyer-visible operation vocabulary, test-only head 993529296d9e95e360571ed6cd7c11dea9b8a70f reached the intended missing-production compile boundary in CI 31931265479 after repository contracts and canonical formatting passed. Production then added Navigate, QueryNodes, ClickNode, TypeText, WaitForState, and ObserveNetwork with exact capability derivation.

For the bounded BiDi query contract:

  1. test-only head d1d5c736a1e50154f59d1ba718108d8610be8165 exposed canonical rustfmt differences first, so that setup failure is not promoted as semantic RED;
  2. formatting-only test head 341ccdcb0980240fcdb0e8630fd5b4475abd4960 passed repository contracts and formatting, then CI 31941083881 reached the intended workspace compile failure because WebDriverBiDiAccessibilityQuery and its public constants/errors did not yet exist; this is the valid production-boundary RED;
  3. production added the bounded query value and public export; exact head 750348a499f87c4a057acf0f3cd69ad6537a0148 exposed only one canonical export-order rustfmt delta; and
  4. current head 7fc113d26f864ac14e6793409d79328bbe954a2c applies that exact canonical formatting and is exact-green.

No lint allow, coverage exclusion, workflow change, gate weakening, or browser-I/O shortcut was introduced.

Implemented contract

BrowserProtocolOperation provides six bounded semantics:

  • NavigateNavigation;
  • QueryNodesSemanticObservation;
  • ClickNodeTypedInput;
  • TypeTextTypedInput;
  • WaitForStateSemanticObservation; and
  • ObserveNetworkNetworkObservation.

BrowserProtocolAdapterDescriptor::dispatch_operation_if_context_origin_epoch_current revalidates exact session/context/origin/document epoch, validates exact runtime protocol metadata, derives the operation's required capability internally, prevents callback execution on denial, and transfers the non-cloneable validated protocol-use proof plus the same typed operation into the immediate callback.

WebDriverBiDiAccessibilityQuery is a transport-parameter value for the reviewed browsingContext.locateNodes accessibility locator. It:

  • accepts an exact accessibility role, accessible name, or both;
  • rejects explicit empty values and rejects a query with neither selector value;
  • applies OriginWeave-local UTF-8 byte budgets of 64 bytes for role and 512 bytes for name before storing owned strings;
  • requires a finite positive result count capped at 128 nodes;
  • exposes the exact method and accessibility locator type as constants/accessors; and
  • returns deterministic source-free typed errors for every fail-closed boundary.

These bounds are local resource-governance limits, not claims about upstream browser maxima. The query value grants no session, context, origin, node, policy, capability, network, or secret authority and performs no browser I/O.

Exact-current proof

On unchanged exact head 7fc113d26f864ac14e6793409d79328bbe954a2c against exact prerequisite 65f51937ff2af64d9c157666e7c3b7df89cbb89a:

  • fresh ancestry: ahead 14 / behind 0, merge base exactly the prerequisite;
  • CI run 31941250130: success;
  • Rust contracts job 95150693728: repository contracts, canonical formatting, locked workspace/all-target check, full tests, strict Clippy, and rustdoc success;
  • Production coverage job 95150693729: exact owned-production function/line/region/branch measurement and enforcement success;
  • Manifest V3 Compatibility run 31941250167: success;
  • GitHub reports the Draft mergeable against its exact prerequisite base;
  • formal reviews currently returned: none; and
  • inline review threads currently returned: none.

Security/SAST/central review workflows not generated for this stacked Draft are absent evidence, not passing evidence. Skipped formatting-diagnostic helpers are not represented as passing gates. No predecessor-head, prerequisite-head, queued, pending, skipped, cancelled, absent, stale, synthetic, status-only, or model-only evidence is promoted as current proof.

Truth boundary

This branch still does not serialize a BiDi command, attach the query to an authenticated live browsing context, derive runtime metadata from Chromium, parse/normalize returned remote values into SemanticNodeObservation, authorize policy/approval/destination/network use, validate semantic-node freshness for input, execute browser I/O, expose unrestricted JavaScript, or prove an action post-condition. The next real adapter boundary must compose the bounded query with exact current context/origin/document authority and treat returned browser data as untrusted observation before any typed action can use it.

Passing automation is not independent approval. This scheduled actor does not merge, self-approve, alter workflows, add secrets, weaken checks, tag, or publish.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82ad849c-d31c-4891-89bb-92e8a37cf81f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@seonghobae seonghobae changed the title test(core): bind browser operations to exact protocol capabilities feat(core): bind browser operations to exact protocol capabilities Aug 13, 2026

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review at exact head 64d763004dc096f3bf8a3aea4cff47e07fbe60f8

Draft stacked on #127 (65f51937ff2af64d9c157666e7c3b7df89cbb89a). Typed operation-to-capability derivation is sound. The new query and remote-node types are honest that they grant no session/context/epoch authority. That is not enough to merge, and it is not enough to treat BiDi identifiers as admitted handles.

Would be REQUEST_CHANGES if GitHub allowed a non-author event on this PR. This actor is the PR author, so the submitted event is COMMENT. The two inline findings remain must-fix before this slice is complete.

CodeRabbit CLI 0.7.3 installed; coderabbit auth login --agent failed with automatic_login_failed / timed-out browser callback. GitHub CodeRabbit skipped this Draft. Findings below are from the workspace files at this exact head.

Current-head CI (Rust contracts, production coverage, MV3 fixture) is success. That is not merge evidence for a Draft stack (mergeable_state: unstable).

Blocking defects

  1. WebDriverBiDiAccessibilityQuery::new admits control injection and whitespace-only locatorscrates/originweave-core/src/browser_protocol_operation.rs:97-108

    Empty role/name and UTF-8 over-budget cases fail closed. Control and whitespace-only values do not. Some(" "), Some("button\n"), and Some("Submit\u{0000}task") construct successfully. Tests in webdriver_bidi_accessibility_query.rs only cover "" and ASCII byte budgets.

    Why it matters: this is the first untrusted locator-text gate for browsingContext.locateNodes. Those strings are the values a later adapter will serialize. Accessible names may contain spaces; they must not contain controls. Roles should reject whitespace.

  2. WebDriverBiDiRemoteNodeReference.shared_id is consistent with validate_external_identifier — and that consistency is the defectbrowser_protocol_operation.rs:253-255 vs private browser_registry.rs:404-409

    Both gates check only empty + 512-byte UTF-8 budget. Neither rejects control or whitespace. " ", "shared-node-42\n", and "id\0" are admitted. Tests cover missing/empty/over-budget/ multibyte, not controls.

    Why it matters: rustdoc and CHANGELOG present this as admission of a later node handle. A Clone type that stores the raw BiDi sharedId and exposes shared_id() is the ambient-authority footgun if any caller reuses it across session/context/epoch. Tighten both gates together so they stay identical.

Non-blocking notes

  • BrowserProtocolOperation mapping and dispatch_operation_if_context_origin_epoch_current correctly derive capability internally and skip the callback on denial. No authority-boundary defect in that mapping.
  • validate_result_count and per-item WebDriverBiDiRemoteNodeReference::new exist as separate APIs. There is no function that applies result-count admission, then per-item admission, then BrowserAuthorityRegistry::bind_node to an ObservedNodeHandle on the exact current session/context/origin/epoch. Stale-epoch and cross-session reuse are therefore untested on this path because the path does not exist.
  • Realm-local BiDi handle is correctly not admitted. Requiring sharedId as local fail-closed policy is documented honestly in docs/doctoring.md:11.
  • Rustdoc meets #![deny(missing_docs)] on every new public item. The new rustdoc phrase “as a later node handle” overclaims relative to the struct docs (“untrusted transport handle”).
  • Accessibility query tests lack a multibyte UTF-8 budget case (remote-node tests already have one).
  • This Draft is not mergeable onto protected main while #127 and its stack remain open.

Highest-value next bounded product slice

One function: given a reviewed WebDriverBiDiAccessibilityQuery and an untrusted locateNodes item list, validate_result_count, admit each item as WebDriverBiDiRemoteNodeReference (with control/whitespace rejection), then bind_node on the exact current session/context/origin/document epoch, returning ObservedNodeHandle values. Fail closed on stale epoch and cross-session reuse. No browser I/O.

Verdict

REQUEST_CHANGES (submitted as COMMENT because GitHub rejects author request_changes). Do not APPROVE. Do not merge this Draft.

Comment on lines +97 to +108
if role.is_some_and(str::is_empty) {
return Err(WebDriverBiDiAccessibilityQueryError::EmptyRole);
}
if role.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES) {
return Err(WebDriverBiDiAccessibilityQueryError::RoleTooLong);
}
if name.is_some_and(str::is_empty) {
return Err(WebDriverBiDiAccessibilityQueryError::EmptyName);
}
if name.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES) {
return Err(WebDriverBiDiAccessibilityQueryError::NameTooLong);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocking: empty and UTF-8 budget are enforced, but this is the first untrusted locator-text admission for browsingContext.locateNodes and it still accepts control injection and whitespace-only values.

Some(" "), Some("button\n"), and Some("Submit\u{0000}task") all pass. webdriver_bidi_accessibility_query.rs covers "" and over-budget ASCII only — no control, no Unicode budget, no whitespace-only role.

Reject char::is_control() on both role and name (accessible names may contain spaces; roles should not). Do not treat this as a later serialization problem: these strings are the values a future adapter will put on the wire.

Comment on lines +253 to +255
if shared_id.is_empty() || shared_id.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES {
return Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This matches private validate_external_identifier in browser_registry.rs:404-409 (empty + MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES only). It is not consistent with a control/whitespace gate, because that helper does not have one either.

" ", "shared-node-42\n", and "id\0" are admitted as a Clone value whose rustdoc calls it a later node handle. That is the first public BiDi identifier admission surface; copying the weaker registry check lets control-bearing sharedIds become bind_node map keys later.

Fail closed on is_control() / is_whitespace() here and in validate_external_identifier so the two gates stay identical. Add tests for those cases; UTF-8 budget coverage is already good.

@cursor cursor 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.

Do not merge this Draft. Exact head 64d763004dc096f3bf8a3aea4cff47e07fbe60f8 correctly derives typed operation capabilities and keeps BiDi sharedId from becoming session/context/epoch authority. Two admission gates still accept control-bearing and whitespace-only protocol text.

Must fix before this slice can be treated as a fail-closed locator/handle boundary:

  1. WebDriverBiDiAccessibilityQuery::new admits " ", newline, and NUL in role/name. Keep ordinary spaces in accessible names; reject controls in names and reject whitespace/controls in roles.
  2. WebDriverBiDiRemoteNodeReference::new and validate_external_identifier both accept whitespace-only and control-bearing sharedId values. Tighten both gates together and add control-injection tests.

This remains a stacked Draft on #127. Passing or in-progress checks are not merge permission. Next bounded slice after this repair: compose result-count revalidation, per-item remote-node admission, and BrowserAuthorityRegistry::bind_node into ObservedNodeHandle values on the exact current session/context/origin/document epoch, with stale-epoch and cross-session reuse failing closed. No browser I/O.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

Comment on lines +97 to +108
if role.is_some_and(str::is_empty) {
return Err(WebDriverBiDiAccessibilityQueryError::EmptyRole);
}
if role.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES) {
return Err(WebDriverBiDiAccessibilityQueryError::RoleTooLong);
}
if name.is_some_and(str::is_empty) {
return Err(WebDriverBiDiAccessibilityQueryError::EmptyName);
}
if name.is_some_and(|value| value.len() > MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES) {
return Err(WebDriverBiDiAccessibilityQueryError::NameTooLong);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This constructor is the first untrusted locator-text gate for browsingContext.locateNodes. Empty and over-budget role/name fail closed, but control and whitespace-only values still construct: Some(" "), Some("button "), and Some("Submit\u{0000}task") succeed.

Accessible names may contain ordinary spaces ("Task text" is already a valid fixture). They must not contain controls. Roles are WAI-ARIA tokens and must reject whitespace and controls. A later adapter will put these strings on the wire; do not admit them here.

Add fail-closed tests for those cases, then reject them before storing owned strings.

Comment on lines +253 to +255
if shared_id.is_empty() || shared_id.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES {
return Err(WebDriverBiDiRemoteNodeReferenceError::InvalidSharedId);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sharedId admission currently matches validate_external_identifier, and that consistency is the defect: both gates check only empty plus the 512-byte UTF-8 budget. " ", "shared-node-42 ", and "id\0" are admitted as later node-handle text.

Tighten this gate and the registry identifier check together so they stay identical. Reject control and whitespace in the shared identifier, keep the existing empty/over-budget distinction, and add realistic control-injection tests. Do not treat the admitted shared_id() string as OriginWeave session, context, origin, or document-epoch authority.

Copy link
Copy Markdown
Contributor Author

Superseded by #174 after fresh exact-current ancestry verification. The actual live #138 head is 64d763004dc096f3bf8a3aea4cff47e07fbe60f8 (not the older SHA still mentioned in the PR body). Current #174 head d83fec050a2e5dee53ae4137220140885ef443b4 has that exact live head as its merge base and is ahead 14 / behind 0. Thus the typed browser-operation/capability binding and bounded accessibility-query work are preserved, together with the later locateNodes/QueryNodes/SemanticObservation-proof hardening. Closing this predecessor removes a duplicate review vehicle; no check, review, coverage, or approval evidence transfers to #174, which remains Draft behind #127.

@seonghobae seonghobae closed this Aug 16, 2026
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.

2 participants