Files

51 lines
2.8 KiB
Markdown
Raw Permalink Normal View History

# Browser Control Broker
## Purpose
This module carries agent browser actions from the server to the client that
owns the in-app browser view, and the result back. The browser lives in a
renderer, not in the server process, so the server can never act on a page
itself; it can only ask and wait.
## Boundaries
- `broker.js` owns request lifetime: it publishes one action through the
injected `emitRequest`, holds the pending request, and settles it on a client
result, a timeout, or an abort signal. It knows nothing about transports.
- `routes.js` is the result callback (`POST /api/browser-control/result`). It
validates the envelope and hands the outcome to the broker.
- `../../index.js` supplies `emitRequest`, which writes the request to the
OpenChamber SSE clients and returns how many were reached.
- `../openchamber-control/service.js` is the only caller. It maps the
`browser.*` actions of the `openchamber_web` tool onto `broker.request()` and
owns their parameter validation.
- The client half is `packages/ui/src/lib/browser/controlClient.ts`, which
registers the mounted browser pane as the one responder.
## Invariants
- Capability belongs to the connection, not to configuration. A client declares
it can drive a page by opening its event stream with `browser=1`, which only
a Chromium host does; the flag lives and dies with that connection, so there
is no setting to enable and no restart to remember.
- `emitRequest` counts only clients that can serve the action. `browser.open`
needs any client, because opening a tab is what creates a view; every other
action needs a declared-capable one.
- Exactly one client performs a request. The broadcast reaches everyone who
could serve it, so a client claims the request over
`POST /api/browser-control/claim` and acts only if granted; the first claim
wins and every other client does nothing. Deciding by whose result arrives
first would be too late, because by then each of them has already clicked.
A claim for a settled request is refused for the same reason.
- Nobody listening is answered immediately with a 503 describing the
environment, never by blocking for the full timeout. A blocked wait followed
by a timeout cannot be told apart from a page that hung.
- A client that accepted a request and then disappeared still times out.
Assuming success would report a page interaction that never happened.
- A result for an unknown request id is accepted with `matched: false`, not an
error: a client answering after the timeout has behaved correctly.
- The result route parses its own body. This server has no global body parser,
and a missing one silently turns every answer into an agent-visible timeout.
- Request payload limits are sized for a page snapshot (visible text plus every
interactive element), not for a control message.