206 lines
11 KiB
Markdown
206 lines
11 KiB
Markdown
|
|
# Browser job contract v1
|
|||
|
|
|
|||
|
|
An agent needs Python 3.10+ and the single file `client/rapp_playwright.py`.
|
|||
|
|
Playwright and Chromium run at the provider. The service's JSON protocol is
|
|||
|
|
versioned independently of Playwright; clients send `X-Rapp-Protocol: 1` and
|
|||
|
|
requests contain `api_version: "1"`. A mismatch returns HTTP 409 before a run.
|
|||
|
|
|
|||
|
|
## Access and first command
|
|||
|
|
|
|||
|
|
The managed endpoint is private. With a Kubernetes identity allowed to get the
|
|||
|
|
playwright pod and create its port-forward:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
kubectl -n playwright port-forward deployment/playwright 18081:8080 --address 127.0.0.1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Leave the tunnel running. In another terminal, from this checkout:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
python3 client/rapp_playwright.py preflight
|
|||
|
|
python3 client/rapp_playwright.py trial --out .local/trial-01
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The trial obtains its specification from the service, opens the bundled fixture,
|
|||
|
|
checks the rendered counter, presses Tab, asserts button focus, presses Enter,
|
|||
|
|
asserts the increment, and saves screenshots at 1280×800 and 390×844. It downloads
|
|||
|
|
artifacts and a receipt. Use a **new output directory** for each invocation.
|
|||
|
|
This command becomes a live service path only after the deployment exists;
|
|||
|
|
see SCOPE.md and retained runtime evidence for actual availability.
|
|||
|
|
|
|||
|
|
Preflight reports actual browser launch evidence, its timestamp, browser and
|
|||
|
|
Playwright versions, release, execution location, allowed target origins,
|
|||
|
|
concurrency and retention. The launch probe runs at startup and at most once
|
|||
|
|
per minute while idle. It is not an assertion about the consumer's target.
|
|||
|
|
HTTP 503 indicates an unavailable browser; connection failure means the service
|
|||
|
|
or tunnel is unavailable. It never implies a locally installed browser.
|
|||
|
|
|
|||
|
|
The service listens on **pod loopback**, with a deny-all ingress policy and no
|
|||
|
|
Ingress/NodePort. Kubernetes RBAC/SSH authenticates the tunnel; the service does
|
|||
|
|
not provide an unauthenticated public listener or issue platform identities.
|
|||
|
|
No shared application password is required. Do not widen the listener to expose
|
|||
|
|
it remotely without adding an authenticated transport and reviewing the boundary.
|
|||
|
|
|
|||
|
|
## Run a consumer scenario
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
python3 client/rapp_playwright.py run --spec examples/trial.json --out .local/run-01
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Example request:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"api_version": "1",
|
|||
|
|
"target": "fixture",
|
|||
|
|
"label": "keyboard-smoke",
|
|||
|
|
"timeout_ms": 60000,
|
|||
|
|
"viewports": [{"name":"desktop","width":1280,"height":800}, {"name":"narrow","width":390,"height":844}],
|
|||
|
|
"steps": [
|
|||
|
|
{"action":"visible","selector":"h1"},
|
|||
|
|
{"action":"press","key":"Tab"},
|
|||
|
|
{"action":"focused","selector":"#increment"},
|
|||
|
|
{"action":"press","key":"Enter"},
|
|||
|
|
{"action":"text","selector":"#count","value":"1"},
|
|||
|
|
{"action":"screenshot","name":"keyboard-result"}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`target` is `fixture` or an HTTP(S) URL whose exact origin the provider has
|
|||
|
|
allowed. Userinfo, query strings and fragments in the initial target are rejected
|
|||
|
|
to avoid putting credentials into ordinary receipts. V1 tests public or synthetic
|
|||
|
|
pages; it has no credential injection, storage-state import or arbitrary code.
|
|||
|
|
Consumers own permission to use targets and handle captured content.
|
|||
|
|
|
|||
|
|
Steps execute in order in a new browser context for each viewport. Supported
|
|||
|
|
actions are `click`, `focus`, `fill`, `press`, `visible`, `text`, `focused`, `url`
|
|||
|
|
and `screenshot`. Selectors for `text` are CSS; use CSS consistently across the
|
|||
|
|
scenario. `text` compares trimmed text exactly with a five-second retry window.
|
|||
|
|
`focused` checks the active element; `url` checks the complete current URL.
|
|||
|
|
At least one assertion is required. A failed step stops the run, preserves prior
|
|||
|
|
artifacts, and records the viewport, index, action and failure. Each completed
|
|||
|
|
viewport also produces a final screenshot. Screenshots are viewport-sized,
|
|||
|
|
not full-page captures. No visual-baseline pass is inferred from a saved PNG.
|
|||
|
|
|
|||
|
|
## HTTP lifecycle
|
|||
|
|
|
|||
|
|
| Method/path | Result |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `GET /healthz` | Process liveness only; no protocol header needed |
|
|||
|
|
| `GET /v1/preflight` | Launch availability, supported versions, location and limits |
|
|||
|
|
| `GET /v1/trial` | Consumer-independent fixture scenario |
|
|||
|
|
| `POST /v1/runs` | HTTP 202: run ID, secret capability token, expiry |
|
|||
|
|
| `GET /v1/runs/{id}` | Current status or terminal receipt; requires run token |
|
|||
|
|
| `DELETE /v1/runs/{id}` | Idempotent cancellation request; requires run token |
|
|||
|
|
| `GET /v1/runs/{id}/artifacts/{name}` | Terminal run's named PNG; requires run token |
|
|||
|
|
|
|||
|
|
Run operations use `Authorization: Bearer <run-token>`. The service stores only
|
|||
|
|
the token's SHA-256 hash and has no run-listing endpoint. Wrong/missing tokens,
|
|||
|
|
unknown IDs and expired IDs all return 404. Do not publish `run.json`, which is
|
|||
|
|
the client's private run handle. Directory permissions are 0700 and files 0600.
|
|||
|
|
The client prints no token. Receipts omit fill values, expected page text,
|
|||
|
|
cookies, browser state, console output and request headers. Screenshots can still
|
|||
|
|
contain sensitive page content; only test appropriate targets and control copies.
|
|||
|
|
|
|||
|
|
States: `queued → running → passed|failed|cancelled`. A previously nonterminal
|
|||
|
|
run found after a service restart becomes `interrupted/service_restarted`, and
|
|||
|
|
its uncommitted artifacts are removed. Terminal receipts/artifacts survive a
|
|||
|
|
restart when the provider's data volume is retained. V1 provides no automatic
|
|||
|
|
retry: a lost submission response may require waiting for the server time budget
|
|||
|
|
and starting a new run. A run ID is not an idempotency key.
|
|||
|
|
|
|||
|
|
The caller can recover with:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
python3 client/rapp_playwright.py status --handle .local/run-01/run.json
|
|||
|
|
python3 client/rapp_playwright.py cancel --handle .local/run-01/run.json
|
|||
|
|
python3 client/rapp_playwright.py download --handle .local/run-01/run.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
For a nondefault endpoint, repeat `--url http://127.0.0.1:PORT` before each
|
|||
|
|
subcommand (or set `RAPP_PLAYWRIGHT_URL`). A run handle does not select the
|
|||
|
|
originating service. `invocation.json` retains the original service URL.
|
|||
|
|
|
|||
|
|
Disconnecting does not implicitly cancel an accepted job. Ctrl-C in the client
|
|||
|
|
requests cancellation; server deadlines still apply when the client disappears.
|
|||
|
|
|
|||
|
|
## Limits and evidence
|
|||
|
|
|
|||
|
|
Defaults: one active browser, four queued jobs, 64 retained runs, 60-second run
|
|||
|
|
budget (maximum 120 seconds), 40 steps, four viewports, width ≤1920 and height
|
|||
|
|
≤1600, 32 KiB submission, 20 MiB artifacts per run and 200 MiB retained artifact
|
|||
|
|
admission budget. Runs expire one hour after acceptance, including capabilities,
|
|||
|
|
receipts and server artifacts. The janitor runs every second. The managed pod
|
|||
|
|
adds CPU, memory and ephemeral-storage limits; PVC exhaustion remains an operator
|
|||
|
|
failure, not UI failure. Retained client copies have a consumer-owned lifecycle.
|
|||
|
|
|
|||
|
|
Receipt fields include ID, status/error, label, target, viewports, acceptance,
|
|||
|
|
start/finish/expiry timestamps, service/Playwright/browser versions, release and
|
|||
|
|
execution location, completed steps, per-step outcomes and artifact names/sizes.
|
|||
|
|
The Python client adds download SHA-256 hashes and saves the exact command in
|
|||
|
|
`invocation.json`. The service deliberately does not retain the full request.
|
|||
|
|
|
|||
|
|
Terminal result examples (selected fields; complete executed receipts are in
|
|||
|
|
[independent consumer evidence](evidence/2026-10-13-independent-local-consumer.md)):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{"status":"passed","error":null,"completed_steps":14}
|
|||
|
|
{"status":"failed","error":"ui_assertion_failed","steps":[{"viewport":"desktop","index":1,"action":"text","status":"failed"}]}
|
|||
|
|
{"status":"failed","error":"target_unreachable","steps":[],"artifacts":[]}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
An unavailable browser is rejected before admission with HTTP 503 and
|
|||
|
|
`{"error":"browser_unavailable"}`; a request with the wrong API version receives
|
|||
|
|
HTTP 409. A missing service/tunnel fails at the client as `service_unreachable`.
|
|||
|
|
|
|||
|
|
| Error | Meaning / next action |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `browser_unavailable` / `browser_launch_failed` | Provider dependencies or browser failed; check preflight/operator logs |
|
|||
|
|
| `target_not_allowed` | Provider origin policy denied navigation or a redirect; request a scoped target change |
|
|||
|
|
| `target_unreachable` | DNS, connection, TLS or target HTTP error; verify address from the browser network |
|
|||
|
|
| `ui_assertion_failed` | Rendered UI/action did not satisfy the scenario; inspect step and available artifacts |
|
|||
|
|
| `run_timeout` | Whole run exceeded its budget; simplify or increase up to the documented maximum |
|
|||
|
|
| `browser_crashed` | Worker exited without a terminal result; provider runtime diagnosis |
|
|||
|
|
| `queue_full` / `artifact_capacity` | HTTP 429; retry after capacity frees |
|
|||
|
|
| `service_restarted` / `service_stopped` | Provider interruption; submit a fresh run |
|
|||
|
|
|
|||
|
|
For `trial` and `run`, exit codes are 0 passed, 2 failed UI assertion, 3 setup/
|
|||
|
|
infrastructure or other unsuccessful run, and 4 keyboard interruption. `status`,
|
|||
|
|
`cancel` and `download` return 0 when that API operation succeeds, regardless of
|
|||
|
|
the run outcome; inspect the receipt's `status` and `error`. A receipt cannot substitute
|
|||
|
|
for content-explorer accepting its own screenshot/keyboard task.
|
|||
|
|
|
|||
|
|
## Target reachability
|
|||
|
|
|
|||
|
|
A remote browser's `localhost` is the provider, not the caller's workstation.
|
|||
|
|
The origin gate covers redirects and subresources via a per-run HTTP/CONNECT
|
|||
|
|
proxy. DNS resolves once per connection; non-public addresses are denied unless
|
|||
|
|
the exact origin is explicitly listed in `PRIVATE_ORIGINS`. WebSockets and
|
|||
|
|
service workers are blocked. A consumer cannot request broader origin grants.
|
|||
|
|
|
|||
|
|
For a development app, first publish a reviewed private preview reachable from
|
|||
|
|
the service, or arrange an owner-approved reverse tunnel to a **dedicated target
|
|||
|
|
port** and register that exact origin. The browser API port-forward does not
|
|||
|
|
forward the consumer app. For local Docker development, explicitly allow the
|
|||
|
|
host gateway origin and bind the dev server to an address the container can
|
|||
|
|
reach. Never map all private networks just to make localhost work.
|
|||
|
|
|
|||
|
|
## Decision and upstream reuse
|
|||
|
|
|
|||
|
|
Chosen: bounded HTTP jobs, Python standard-library client, native Playwright
|
|||
|
|
inside a per-run worker, fresh Chromium process and per-viewport context. Native
|
|||
|
|
remote WebSocket sessions would require matching clients and expose broader
|
|||
|
|
automation/network access; defer them until a consumer needs arbitrary suites.
|
|||
|
|
This release adds the missing orchestration/evidence path, not a new browser engine.
|
|||
|
|
|
|||
|
|
The design reuses Playwright headless/context/screenshot patterns inspected in
|
|||
|
|
whynot-design, coulomb-social, core-hub and frontend-patterns. Official references:
|
|||
|
|
[Docker and version matching](https://playwright.dev/docs/docker),
|
|||
|
|
[browser contexts](https://playwright.dev/docs/api/class-browsercontext),
|
|||
|
|
[browser lifecycle](https://playwright.dev/docs/api/class-browser).
|
|||
|
|
The pinned package and browser image are both Playwright 1.64.0. The container
|
|||
|
|
uses a non-root user. Chromium's OS sandbox is configurable and defaults off for
|
|||
|
|
the trusted-target managed pilot; this is **not a hostile-page or mutually
|
|||
|
|
untrusted-tenant execution service**. No arbitrary user scripts run in Node.
|