Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13590-a16f-7f60-be60-fd71627bc99d
205 lines
11 KiB
Markdown
205 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.
|