Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions docs/BRIDGE_CONTRACT_V1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# OpenSIN-Bridge Contract v1

> Status: stable
> Schema: `opensin.bridge.contract/v1`
> Source of truth: `extension/src/contract/v1/index.js`
> Worker counterpart: `OpenSIN-AI/A2A-SIN-Worker-heypiggy/bridge_contract.py`

## Why

Worker issue [#69](https://github.com/OpenSIN-AI/A2A-SIN-Worker-heypiggy/issues/69)
demands a versioned contract for browser commands, events, retries, and failure
classes. Previously the bridge surface was the union of `extension/src/tools/*`
plus an organic alias layer in `aliases.js`. That worked for ad-hoc agents but
gave downstream workers no machine-readable promise about:

- which methods are safe to retry,
- which methods mutate browser state,
- what failure shape the worker can branch on.

This document is the human-readable view. The machine-readable view ships in
`extension/src/contract/v1/index.js` and is exposed at runtime via
`bridge.contract`.

## Read it

```js
const contract = await rpc("bridge.contract")
contract.version // "opensin.bridge.contract/v1"
contract.revision // 1
contract.methods // [{ name, idempotent, mutates, raises, retryHint, ... }]
contract.errorCodes // ["transport_error", "target_gone", ...]
contract.retryHints // { transport_error: "safe_retry", ... }
contract.internalToContract // { TIMEOUT: "timeout", ... }
```

The contract is also pinned in CI via
`scripts/validate-bridge-contract.mjs` (`pnpm run validate:bridge-contract`).

## Idempotency model

Every method declares an `idempotent` boolean. The rule is:

- `idempotent: true` → calling it twice with the same params produces the
same observable browser state and is therefore safe to retry on transient
failure.
- `idempotent: false` → the worker MUST NOT blindly retry. Either it has
postconditions to verify the action took effect, or it must escalate to
recovery.

Examples:

| Method | Idempotent | Mutates | Reasoning |
| ----------------- | ---------- | ------- | -------------------------------------------------------- |
| `tabs.list` | yes | no | Read-only. |
| `tabs.activate` | yes | yes | Same tabId twice converges to the same active tab. |
| `nav.goto` | no | yes | SPAs may build different state per navigation. |
| `dom.click` | no | yes | Two submits = double order. Worker must verify. |
| `dom.snapshot` | yes | no | Read-only. |
| `session.invalidate` | yes | yes | Already-invalid session stays invalid; converges. |
| `session.restore` | no | yes | Restoring twice can clobber fresh state. Worker decides. |

## Error taxonomy

Failures map onto a small, stable code set (full list:
`extension/src/contract/v1/index.js`, `ERROR_CODES`). The bridge never invents
new error codes at runtime — every internal `BridgeError` is translated via
`INTERNAL_TO_CONTRACT`.

### Transport / RPC envelope

| Code | Meaning |
| ----------------- | ------------------------------------------------------ |
| `transport_error` | WS / native host disconnected, request never reached. |
| `rpc_invalid` | Malformed RPC envelope or invalid params. |
| `unknown_method` | Method not registered in this contract revision. |
| `rate_limited` | Bridge-internal rate limit hit. Retry with backoff. |
| `timeout` | Tool exceeded its timeout budget. |

### Browser surface

| Code | Meaning |
| --------------------- | -------------------------------------------------------- |
| `target_gone` | Tab/frame closed during call. Recover then retry. |
| `navigation_aborted` | Navigation interrupted (programmatic or user). |
| `navigation_timeout` | Navigation didn't complete within timeoutMs. |
| `cdp_failed` | Chrome DevTools Protocol failure (debugger detached). |
| `frame_detached` | The selected frame is gone. |

### DOM / interaction

| Code | Meaning |
| -------------------------- | ---------------------------------------------------- |
| `element_not_found` | Selector / ref didn't resolve. |
| `element_not_actionable` | Resolved but covered, disabled, or zero-size. |
| `postcondition_failed` | Mutating action did not produce expected change. |
| `duplicate_action` | Submit-style guard prevented a second click. |

### Session

| Code | Meaning |
| ----------------------- | ------------------------------------------------------ |
| `session_invalid` | Manifest invalidated (logged out, cookie purged, ...). |
| `session_stale` | TTL expired. Worker can extend or reacquire. |
| `session_locked` | Another worker holds the session lock. |
| `origin_not_permitted` | Origin not in the bridge's allowed list. |

### Adversarial environment

| Code | Meaning |
| --------------------- | ------------------------------------------------------ |
| `anti_bot_challenge` | Cloudflare / DataDome / PerimeterX detected. |
| `captcha_required` | reCAPTCHA / hCaptcha / Turnstile interactive challenge.|
| `rate_limit_remote` | Remote site (not bridge) is throttling. |

## Retry hints

Every error code carries a hint (`safe_retry`, `recover_then_retry`, `abort`).
The worker's retry loop SHOULD use this to decide between:

- `safe_retry` → exponential backoff in place
- `recover_then_retry` → re-acquire context (session, frame, tab) first
- `abort` → escalate to higher-level policy

Hints are advisory. The worker is the policy authority — the bridge is the
classifier.

## Stability guarantees

Within v1:

- No method is removed.
- No error code is repurposed.
- Adding a method or relaxing idempotency bumps `revision`.

A breaking change forces a new file under `extension/src/contract/v2/index.js`
and a parallel exposure (`bridge.contract` always returns the active major).

## Related tools

- `bridge.contract` — get the full contract object
- `bridge.contract.method` — get one method's metadata
- `bridge.contract.translate` — translate an internal code to a contract code
- `bridge.contract.idempotent` — yes/no shortcut
- `bridge.contract.version` — version + revision only
- `bridge.evidenceBundle` — pair with the contract for failure triage (#70)
- `bridge.traces` — recent dispatches, optionally by `traceId`
91 changes: 91 additions & 0 deletions docs/GAPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Gaps and honest limitations

Status as of the rewrite alignment PR. This document is the counterpart to
`A2A-SIN-Worker-heypiggy/docs/GAPS.md`. Anything listed here is **not**
solved by the contract-v1 layer and must not be assumed production-grade.

## 1. Target resolution in the interaction path

- `dom.click` / `dom.type` accept a selector or a target object. Selectors
are matched with a plain `querySelector` fallback. Shadow DOM, iframes
and cross-origin frames are **not** transparently resolved.
- No built-in retry for "element exists but is covered by an overlay".
Callers must compose `stealth.detectChallenge` + dismiss logic themselves.
- Accessibility snapshots returned by `dom.snapshot` do not yet include
ancestor chain or stable path hashes. Resolvers relying only on
`name` or `ariaLabel` will collide on pages with repeated text.

**Planned fix:** extend `dom.snapshot` to return `{ role, name, path, bbox,
visible, interactable, ancestors: [...] }` per node, and add
`dom.resolve` that scores candidates.

## 2. Stealth surface is a heuristic, not a defence

- `stealth.assess` checks locale / timezone / viewport / `navigator.webdriver`.
Modern anti-bot stacks (Cloudflare Turnstile, DataDome, PerimeterX,
Kasada, Akamai Bot Manager) fingerprint dozens of additional signals:
canvas, WebGL, AudioContext, font metrics, TLS JA3/JA4, HTTP/2 SETTINGS.
None of those are covered here.
- `stealth.detectChallenge` is a DOM signature matcher. It **cannot**
distinguish a silently-triggered silent challenge from a clean page.
- There is no mouse or keyboard humanisation primitive in the bridge.
`dom.click` fires a single synthetic event at the target centre.

**Planned fix:** add `input.humanMove`, `input.humanClick`, `input.humanType`
primitives in a new `tools/input.js` driven by CDP
`Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`, with jitter and
motion traces.

## 3. Session lifecycle gaps

- `session.manifest` tracks TTL in wall-clock seconds. There is no
refresh-token aware flow; reauth is handled outside the bridge.
- `session.lastKnownGood` snapshot contains cookies only. IndexedDB
and ServiceWorker registrations are not captured.
- Cross-profile isolation depends on Chrome's cookie stores and is not
enforced inside the bridge. Two origins sharing a cookie jar leak
into each other's manifests.

## 4. Observability gaps

- Evidence bundles embed screenshots as base64 inline. For long
sessions this blows up disk usage. No rotation or compression.
- Trace IDs are generated inside the bridge and are not propagated
back into the page via `X-Trace-Id` or similar, so network-level
evidence cannot be joined with DOM evidence.
- `bridge.traces` stores the ring buffer in memory; extension reloads
wipe history.

## 5. Contract completeness

- Contract v1 covers **40** methods. Real worker call sites still use
some legacy names that are only implemented via `tools/aliases.js`
and **not** exposed through `bridge.contract`. A worker calling
`ensure_contract()` will accept v1 but can still invoke a
non-catalogued legacy method at runtime.
- `dom.evaluate` accepts arbitrary JS. It is listed as non-mutating
but cannot be enforced. Callers must treat it as effectively mutating.

## 6. Test coverage

- 60 Node tests cover contract metadata, session lifecycle, evidence
bundle shape and stealth assessment math. There is **zero** end-to-end
coverage against a real Chrome instance, a real website, or a
real Cloudflare-protected target.
- No fuzzer against malformed RPC envelopes, yet the bridge is the
trust boundary between worker and browser.

## 7. What this PR does **not** fix

- Anti-bot evasion quality.
- Worker orchestration logic (lives in the worker repo).
- Real target-resolver robustness.
- Production readiness of the opt-in new stack on the worker side.

## Rollout contract

Until the gaps above are closed, consumers must assume the new surface
is **best-effort additive tooling**, not a replacement for
site-specific hardening. The flat legacy names (`click`, `type`,
`nav_to`, ...) keep working and remain the supported path for
heypiggy's in-production flows.
67 changes: 67 additions & 0 deletions docs/REWRITE_TRACKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Rewrite Tracker — HeyPiggy worker + OpenSIN-Bridge

> Last updated: 2026-04-20
> Master: [worker #67](https://github.com/OpenSIN-AI/A2A-SIN-Worker-heypiggy/issues/67)
> Base SHA: `bf1cd8c` on `main` (worker repo)

## Why this file exists

Worker repo `OpenSIN-AI/A2A-SIN-Worker-heypiggy` issue #67 froze the previous
implementation as broken and split the rewrite into nine sub-issues. Several of
them have a **bridge-side counterpart** that has to land in this repo so the
worker can be rewritten against a stable contract instead of the old organic
surface.

This file is the index. Each row links the worker issue to the concrete
bridge artifacts that satisfy its bridge-side requirements.

## Coverage matrix

| Worker issue | Theme | Bridge artifacts | Status |
| ------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| #68 | Architecture RFC | (worker-only) — the bridge contract v1 (`docs/BRIDGE_CONTRACT_V1.md`) is the bridge-side input to the RFC. | done |
| #69 | Bridge contract | `extension/src/contract/v1/index.js`, `extension/src/tools/contract.js`, `docs/BRIDGE_CONTRACT_V1.md`, `scripts/validate-bridge-contract.mjs`, `bridge.contract` RPC | done |
| #70 | Observability / evidence | `extension/src/drivers/evidence.js`, `extension/src/drivers/trace.js`, `extension/src/tools/evidence.js`, `bridge.evidenceBundle` and `bridge.traces` RPCs | done |
| #71 | Session lifecycle | `extension/src/drivers/session-lifecycle.js`, `session.manifest`, `session.invalidate`, `session.lastKnownGood`, `session.health` RPCs | done |
| #72 | Runtime state machine | (worker-only) — bridge exposes the primitives the FSM consumes (snapshot, dom.click, session.health, evidenceBundle, contract). | n/a |
| #73 | Interaction engine | (worker-only) — bridge already exposes actionability via `dom.fullSnapshot` + `dom.click` postcondition fields. Contract documents the failure codes. | n/a |
| #74 | Stealth strategy | `extension/src/tools/stealth.js`, `stealth.assess`, `stealth.detectChallenge`. The existing `stealth-main.js` is unchanged; this adds the assessment surface. | done |
| #75 | Panel plugins | (worker-only) — bridge stays panel-agnostic by design (`BOUNDARIES.md`). | n/a |
| #76 | Validation harness | `scripts/validate-bridge-contract.mjs`, `test/contract/*.test.mjs`, `test/session/*.test.mjs`, `test/evidence/*.test.mjs`, package script `validate:bridge-contract` | done |

## How to consume from the worker

```python
# heypiggy/bridge_contract.py
import json
import urllib.request

def fetch_contract(bridge_http_url: str) -> dict:
body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "bridge.contract", "arguments": {}}}
req = urllib.request.Request(
f"{bridge_http_url}/mcp",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
envelope = json.loads(resp.read())
text = envelope["result"]["content"][0]["text"]
return json.loads(text)
```

The worker pins the contract revision at boot, validates that every
method/error code it expects is present, and aborts startup if anything is
missing. That is the gate worker issue #69 demands.

## Stop conditions

Per worker issue #76, the rewrite can not enable wider rollout until:

- `pnpm run validate:bridge-contract` passes in CI on this repo
- the worker-side replay harness clears the synthetic flow set
- a live canary run produces a complete `bridge.evidenceBundle` for every
failure (no unclassified errors)

Until those gates pass the worker stays in the locked-down rewrite branch
established in worker issue #67.
Loading
Loading