Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13590-a16f-7f60-be60-fd71627bc99d
11 KiB
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:
kubectl -n playwright port-forward deployment/playwright 18081:8080 --address 127.0.0.1
Leave the tunnel running. In another terminal, from this checkout:
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
python3 client/rapp_playwright.py run --spec examples/trial.json --out .local/run-01
Example request:
{
"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:
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):
{"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, browser contexts, browser lifecycle. 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.