Implement bounded browser service, consumer evidence and managed package

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13590-a16f-7f60-be60-fd71627bc99d
This commit is contained in:
tegwick 2026-10-13 19:23:29 +02:00
parent 386b0c0f04
commit ccf46d8c59
81 changed files with 3455 additions and 73 deletions

205
docs/consumer-contract.md Normal file
View file

@ -0,0 +1,205 @@
# 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.

View file

@ -0,0 +1,67 @@
# Content-explorer pilot
Consumer requirement: `CONTENT-WP-0000-T04`, responsive screenshots and a keyboard
walkthrough. Baseline SHA: `b594d7433bf4e408b44215e25e5304696d6f840e`, as recorded
in content-explorer's `research/baseline/ONCO_PIN`.
The first gate is the provider fixture trial: two viewports, Tab/Enter, observed
focus and counter update, downloaded PNGs and a versioned receipt. It checks the
service independently of OnCo. It does not complete the consumer task.
The second gate uses a runnable instance of the **pinned** OnCo checkout. Start
its UI without invoking the corpus/network publication pipeline; document the
exact launch path and ensure the provider can reach the target. Use its pinned
page markup to write selectors and keyboard assertions. The consumer retains
the upstream license/data boundary and decides which captures it may retain.
Candidate routes from the consumer baseline are `/`, `/cancers/`,
`/cancers/head-and-neck/`, `/query/` and `/graph/`. Capture desktop and narrow
screens, verify at least one meaningful rendered element, and document a real
keyboard sequence and its observed result. Keep the pinned-source receipt
separate from any exploratory run against `https://onco.cc/`, whose revision can
change. A live website receipt must not be described as proof of the pinned SHA.
Consumer acceptance checklist:
- Run URL and pinned source SHA, target launch command, browser/service versions.
- Commands, timestamps, run ID, downloaded artifacts and hashes.
- Viewport dimensions and named page/route for each capture.
- Keyboard steps with focus and action assertions; any failed step explained.
- Separate infrastructure and semantic/visual mismatches.
- Consumer review and task disposition in content-explorer, without automatic
closure by the provider.
## Executed home-page pilot — 2026-10-13
Passed run `1e89ce26-fc3c-4eb3-8280-b36800b92efc`, using the pinned SHA above,
Playwright 1.64.0 / Chromium 156.0.8078.4 in the local Docker provider. The
original consumer checkout was left untouched. A temporary copy of its existing
checkout and installed dependencies ran with:
```sh
NODE_OPTIONS=--max-old-space-size=6144 /tmp/rapp-playwright-onco/node_modules/.bin/next dev --hostname 0.0.0.0 --port 3300
python3 client/rapp_playwright.py run --spec examples/content-explorer-onco.json --out .local/content-explorer-onco-01
```
The launch command ran from `/tmp/rapp-playwright-onco`; it did not invoke the
corpus generation/publication scripts. The provider explicitly allowed
`http://host.docker.internal:3300` as a private target and used Docker's host
gateway mapping. This is local connectivity evidence, not a Railiance tunnel.
All 16 steps passed across 1280×800 and 390×844: the home heading matched,
baseline screenshots were captured, Tab focused `a.skip-link`, Enter navigated
to `/#main`, and post-keyboard screenshots were captured. Six PNG downloads
were hashed; duplicate final/keyboard images share hashes. The URL assertion
proves navigation to the anchor, not a complete accessibility audit.
Retained [receipt](evidence/2026-10-13-content-explorer-pilot/receipt.json),
[preflight](evidence/2026-10-13-content-explorer-pilot/preflight.json),
[invocation](evidence/2026-10-13-content-explorer-pilot/invocation.json),
[desktop baseline](evidence/2026-10-13-content-explorer-pilot/desktop-2-baseline.png),
[narrow baseline](evidence/2026-10-13-content-explorer-pilot/narrow-2-baseline.png),
[desktop result](evidence/2026-10-13-content-explorer-pilot/desktop-final.png) and
[narrow result](evidence/2026-10-13-content-explorer-pilot/narrow-final.png) exclude
the secret client run handle. These are home-page smoke results. Other routes,
search, graph interaction and consumer visual acceptance remain unverified.
The handoff to content-explorer is message
`621635c9-0a72-4a88-9fd6-2ee1fbddc68f`; its T04 remains consumer-owned.

View file

@ -0,0 +1,13 @@
docker run --rm --init --user 1000:1000 --read-only --tmpfs /tmp:rw,size=512m --shm-size=256m --memory=1g --cpus=1 --cap-drop ALL --security-opt no-new-privileges --mount type=bind,src=/home/worsch/rapp-playwright,dst=/app,readonly -w /app mcr.microsoft.com/playwright:v1.64.0-noble@sha256:06a9939e57531807f8d5fd76ce44b53165ffb7d7501d87ab10e285c20b1e971f node --test tests/integration.mjs
✔ real browser produces responsive keyboard evidence, fresh state and scoped artifacts (34881.527994ms)
✔ UI failures, unreachable targets and forbidden redirects remain distinguishable (33199.838535ms)
✔ queue limits, cancellation, timeout, worker crash and subsequent recovery (24287.921891ms)
✔ restart marks abandoned runs interrupted and expiry removes artifacts (6352.642442ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 99315.842719

View file

@ -0,0 +1,86 @@
npm run check
> rapp-playwright@0.1.0 check
> node --check src/server.mjs && node --check src/worker.mjs && node --check src/proxy.mjs
python3 -m py_compile client/rapp_playwright.py tools/render.py
npm test
> rapp-playwright@0.1.0 test
> node --test tests/*.test.mjs
✔ validated contract requires explicit protocol, allowed origin, bounded input and assertions (3.01824ms)
✔ network gate denies non-public addresses unless explicitly granted (6.721745ms)
✔ proxy pins DNS and denies unlisted origins and private addresses before connection (49.97023ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 196.640985
docker build -t forgejo.coulomb.social/coulomb/rapp-playwright:0.1.0 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 773B done
#1 DONE 0.0s
#2 [internal] load metadata for mcr.microsoft.com/playwright:v1.64.0-noble@sha256:06a9939e57531807f8d5fd76ce44b53165ffb7d7501d87ab10e285c20b1e971f
#2 DONE 0.1s
#3 [internal] load .dockerignore
#3 transferring context: 109B done
#3 DONE 0.0s
#4 [internal] load build context
#4 DONE 0.0s
#5 [ 1/10] FROM mcr.microsoft.com/playwright:v1.64.0-noble@sha256:06a9939e57531807f8d5fd76ce44b53165ffb7d7501d87ab10e285c20b1e971f
#5 resolve mcr.microsoft.com/playwright:v1.64.0-noble@sha256:06a9939e57531807f8d5fd76ce44b53165ffb7d7501d87ab10e285c20b1e971f 0.1s done
#5 DONE 0.1s
#4 [internal] load build context
#4 transferring context: 18.23kB 0.0s done
#4 DONE 0.0s
#6 [ 3/10] RUN apt-get update && apt-get install -y --no-install-recommends tini=0.19.0-1 && rm -rf /var/lib/apt/lists/*
#6 CACHED
#7 [ 4/10] COPY package.json package-lock.json ./
#7 CACHED
#8 [ 2/10] WORKDIR /app
#8 CACHED
#9 [ 5/10] RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
#9 CACHED
#10 [ 6/10] COPY src ./src
#10 DONE 0.1s
#11 [ 7/10] COPY fixtures ./fixtures
#11 DONE 0.1s
#12 [ 8/10] COPY client ./client
#12 DONE 0.1s
#13 [ 9/10] COPY --chmod=0555 tools/chromium-launcher.py ./tools/chromium-launcher.py
#13 DONE 0.1s
#14 [10/10] RUN mkdir /data && chown pwuser:pwuser /data
#14 DONE 0.5s
#15 exporting to image
#15 exporting layers
#15 exporting layers 0.5s done
#15 exporting manifest sha256:20d8310d44cd807579aca06df01090a7bb27892ce387519f9957e4dd67f64cc7 0.0s done
#15 exporting config sha256:5bda1139fda091648b1e984f6be8119291ad5616263b206d62ac7986ba2762b3 0.0s done
#15 exporting attestation manifest sha256:3267896f3b778dedce0b2c92ea1c67b64a6e168417aa422c4750dcbcb0d165f5 0.1s done
#15 exporting manifest list sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441
#15 exporting manifest list sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441 0.0s done
#15 naming to forgejo.coulomb.social/coulomb/rapp-playwright:0.1.0 done
#15 unpacking to forgejo.coulomb.social/coulomb/rapp-playwright:0.1.0
#15 unpacking to forgejo.coulomb.social/coulomb/rapp-playwright:0.1.0 0.2s done
#15 DONE 1.0s

View file

@ -0,0 +1,17 @@
{
"id": "621635c9-0a72-4a88-9fd6-2ee1fbddc68f",
"from_agent": "rapp-playwright",
"to_agent": "content-explorer",
"subject": "Pinned OnCo responsive and keyboard pilot evidence available",
"body": "WP-0004-T02 provider pilot passed on pinned OnCo SHA b594d7433bf4e408b44215e25e5304696d6f840e. Original upstream-onco checkout untouched; temporary copy served with Next dev on port3300. examples/content-explorer-onco.json asserted home heading, captured 1280x800 / 390x844, Tab focus on a.skip-link, Enter and #main URL. Run 1e89ce26-fc3c-4eb3-8280-b36800b92efc passed all16steps and6PNGs. Sanitized receipts and selected PNGs: /home/worsch/rapp-playwright/docs/evidence/2026-10-13-content-explorer-pilot/. Full local outputs .local/content-explorer-onco-01 (private run handle must not be published). Please review for CONTENT-WP-0000-T04; you own acceptance and any further route/keyboard coverage. This is workstation-container evidence, not managed Railiance or current live onco.cc. Provider will retain exact launch command and limits in docs/content-explorer-pilot.md.",
"thread_id": null,
"read_at": null,
"archived_at": null,
"created_at": "2026-10-13T17:12:27.179074Z",
"kind": "message",
"expires_at": null,
"supersedes_id": null,
"reader": null,
"delivered_at": null,
"acknowledged_at": null
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View file

@ -0,0 +1,11 @@
{
"argv": [
"client/rapp_playwright.py",
"run",
"--spec",
"examples/content-explorer-onco.json",
"--out",
".local/content-explorer-onco-01"
],
"service_url": "http://127.0.0.1:18081"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View file

@ -0,0 +1,25 @@
{
"available": true,
"error": null,
"checked_at": "2026-10-13T16:43:51.651Z",
"browser_version": "156.0.8078.4",
"api_version": "1",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"execution_location": "workstation-container",
"release": "development",
"allowed_origins": [
"http://host.docker.internal:3300",
"https://onco.cc"
],
"fixture": true,
"active": 0,
"queued": 0,
"limits": {
"concurrency": 1,
"max_queue": 4,
"max_timeout_ms": 120000,
"retention_ms": 3600000,
"max_runs": 64
}
}

View file

@ -0,0 +1,214 @@
{
"id": "1e89ce26-fc3c-4eb3-8280-b36800b92efc",
"api_version": "1",
"status": "passed",
"error": null,
"label": "content-explorer-pinned-onco-home",
"target": "http://host.docker.internal:3300/",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
},
{
"name": "narrow",
"width": 390,
"height": 844
}
],
"timeout_ms": 120000,
"created_at": "2026-10-13T16:43:53.447Z",
"expires_at": "2026-10-13T17:43:53.447Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "development",
"execution_location": "workstation-container",
"browser_version": "156.0.8078.4",
"completed_steps": 16,
"steps": [
{
"viewport": "desktop",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T16:44:10.155Z",
"selector": "h1"
},
{
"viewport": "desktop",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:44:14.051Z",
"selector": "h1"
},
{
"viewport": "desktop",
"index": 2,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:44:14.353Z"
},
{
"viewport": "desktop",
"index": 3,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:44:17.012Z",
"key": "Tab"
},
{
"viewport": "desktop",
"index": 4,
"action": "focused",
"status": "passed",
"at": "2026-10-13T16:44:17.073Z",
"selector": "a.skip-link"
},
{
"viewport": "desktop",
"index": 5,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:44:17.255Z",
"key": "Enter"
},
{
"viewport": "desktop",
"index": 6,
"action": "url",
"status": "passed",
"at": "2026-10-13T16:44:17.296Z"
},
{
"viewport": "desktop",
"index": 7,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:44:17.296Z"
},
{
"viewport": "narrow",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T16:44:30.719Z",
"selector": "h1"
},
{
"viewport": "narrow",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:44:31.430Z",
"selector": "h1"
},
{
"viewport": "narrow",
"index": 2,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:44:31.588Z"
},
{
"viewport": "narrow",
"index": 3,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:44:34.905Z",
"key": "Tab"
},
{
"viewport": "narrow",
"index": 4,
"action": "focused",
"status": "passed",
"at": "2026-10-13T16:44:34.968Z",
"selector": "a.skip-link"
},
{
"viewport": "narrow",
"index": 5,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:44:35.026Z",
"key": "Enter"
},
{
"viewport": "narrow",
"index": 6,
"action": "url",
"status": "passed",
"at": "2026-10-13T16:44:35.085Z"
},
{
"viewport": "narrow",
"index": 7,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:44:35.085Z"
}
],
"artifacts": [
{
"name": "desktop-2-baseline.png",
"bytes": 342951
},
{
"name": "desktop-7-keyboard-skip.png",
"bytes": 314186
},
{
"name": "desktop-final.png",
"bytes": 314186
},
{
"name": "narrow-2-baseline.png",
"bytes": 140972
},
{
"name": "narrow-7-keyboard-skip.png",
"bytes": 129764
},
{
"name": "narrow-final.png",
"bytes": 129764
}
],
"started_at": "2026-10-13T16:43:53.457Z",
"blocked_requests": 0,
"finished_at": "2026-10-13T16:44:36.275Z",
"downloads": [
{
"name": "desktop-2-baseline.png",
"bytes": 342951,
"sha256": "c36fb610e09bf04ba76e9755103ab2c36d39bfcfc286be8590c517c9fa7705b8"
},
{
"name": "desktop-7-keyboard-skip.png",
"bytes": 314186,
"sha256": "a64096394d8750ef422b1f5b6b0f939607cb13a57703f5b2d39c2be84bf2e3c7"
},
{
"name": "desktop-final.png",
"bytes": 314186,
"sha256": "a64096394d8750ef422b1f5b6b0f939607cb13a57703f5b2d39c2be84bf2e3c7"
},
{
"name": "narrow-2-baseline.png",
"bytes": 140972,
"sha256": "7c3a5278d3dcd35413658fbfceb6d19e176439f2ffc2e628f7bbfea6ee6b641f"
},
{
"name": "narrow-7-keyboard-skip.png",
"bytes": 129764,
"sha256": "865fe283ca6a38742ff176c67f4eeee68f37ac92d8d60a6e67fb8b00778ba58e"
},
{
"name": "narrow-final.png",
"bytes": 129764,
"sha256": "865fe283ca6a38742ff176c67f4eeee68f37ac92d8d60a6e67fb8b00778ba58e"
}
]
}

View file

@ -0,0 +1,18 @@
{
"workplan_id": "98fb515e-9239-57d5-9ffa-8a5c5a86ed90",
"id": "90c6d03a-6308-4741-ab71-ac837e242e85",
"topic_id": "595afc64-bd28-47bf-aafb-ba230b28371b",
"title": "Use bounded browser jobs and assign platform lifecycle ownership",
"description": "Implement protocol-v1 HTTP jobs with Python stdlib client, pinned Playwright 1.64.0 / Chromium image, per-run process/context, private RBAC port-forward and scoped run capabilities. Founder explicitly selected railiance-platform as lifecycle owner on 2026-10-13; service implementation remains in rapp-playwright. Managed activation still requires platform admission.",
"decision_type": "made",
"status": "resolved",
"rationale": "Independent client avoids matching local browser installs; bounded jobs constrain network and resource use. See docs/consumer-contract.md. Ownership follows the founder response, not inferred owner approval.",
"decided_by": "codex (implementation contract); founder (lifecycle owner selection)",
"decided_at": null,
"deadline": null,
"escalation_note": null,
"superseded_by": null,
"created_at": "2026-10-13T17:15:54.752167Z",
"updated_at": "2026-10-13T17:15:54.752167Z",
"workstream_id": "98fb515e-9239-57d5-9ffa-8a5c5a86ed90"
}

View file

@ -0,0 +1,39 @@
{
"url": "https://reuse.coulomb.social/v1/federated",
"matches": [
{
"id": "capability.infotech.headless-browser",
"name": "Headless Browser Automation",
"summary": "Headless browser automation with Playwright and Chromium for UI testing, screenshots and page extraction; recommended default based on existing whynot-design, coulomb-social, core-hub and frontend-patterns implementations. A rapp-playwright-provided service will be available shortly (announced 2026-10-13; planned, not yet verified available).",
"vector": "D3 / A2 / C2 / R2",
"domain": "infotech",
"status": "reviewed",
"owner": "reuse-surface",
"path": "registry/capabilities/capability.infotech.headless-browser.md",
"tags": [
"headless",
"browser",
"automation",
"playwright",
"chromium",
"testing",
"screenshots",
"extraction",
"firefox",
"webkit",
"selenium",
"puppeteer"
],
"consumption_modes": [
"informational",
"source adaptation",
"JavaScript library",
"Python library"
],
"source_repo": "reuse-surface",
"source_url": "https://forgejo.coulomb.social/coulomb/reuse-surface/raw/branch/main/registry/indexes/capabilities.yaml",
"source_index": "/data/cache/reuse-surface.yaml"
}
],
"composed_at": "2026-10-13T14:03:25+00:00"
}

View file

@ -0,0 +1,49 @@
{
"action": "docker restart rapp-playwright-final",
"preflight": {
"available": true,
"error": null,
"checked_at": "2026-10-13T17:17:36.100Z",
"browser_version": "156.0.8078.4",
"api_version": "1",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"execution_location": "workstation-release-check",
"release": "sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441",
"allowed_origins": [],
"fixture": true,
"active": 0,
"queued": 0,
"limits": {
"concurrency": 1,
"max_queue": 4,
"max_timeout_ms": 120000,
"retention_ms": 3600000,
"max_runs": 64
}
},
"retrieved_run_id": "35124424-950a-4339-adea-45525b470cab",
"status": "passed",
"artifacts": [
{
"name": "desktop-6-keyboard-result.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "desktop-final.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "narrow-6-keyboard-result.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
},
{
"name": "narrow-final.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
}
]
}

View file

@ -0,0 +1,40 @@
{
"observed_at": "2026-10-13T17:20:07.614706+00:00",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"browser_version": "156.0.8078.4",
"local_image": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441",
"registry_publication": "blocked: blob PUT HTTP 499 and 500; remote manifest inspect returns manifest unknown",
"unit_tests": {
"passed": 3,
"command": "make test"
},
"browser_integration": {
"passed": 4,
"command": "make test-integration",
"log": "2026-10-13-browser-integration.log"
},
"additional_browser_test": {
"passed": 1,
"name": "missing browser fails preflight and prevents admission",
"log": "2026-10-13-missing-browser.log"
},
"family_declaration_validation": "2 declaration(s) ok with rail-kubernetes dependency",
"render": "passed with exact candidate digest; floating references rejected",
"kubernetes_client_dry_run": "7 objects validated; no objects persisted",
"kubernetes_server_dry_run": "not run: namespace absent and admission pending",
"cluster_mutations": "none",
"capacity": {
"node_ip": "92.205.62.239",
"node_ready": true,
"cpu_request_m": 3460,
"cpu_allocatable_m": 4000,
"memory_requests_mi": 9472
},
"final_container": "rapp-playwright-final",
"local_url": "http://127.0.0.1:18083",
"independent_final_run": "35124424-950a-4339-adea-45525b470cab",
"restart": "completed receipt plus four hashed PNGs retrieved after docker restart",
"platform_request": "e77247e5-ce25-4711-a64f-af81a9f7cbb1",
"managed_status": "not deployed; namespace playwright absent"
}

View file

@ -0,0 +1,67 @@
# Implementation verification — 2026-10-13
The bounded service, Python client and managed package are implemented. Local
runtime and independent consumption passed. Image publication and managed
activation are blocked by external delivery dependencies; neither is claimed.
[Machine-readable checks](2026-10-13-implementation-checks.json) record the
candidate image identity, commands, resource observations and handoff IDs.
- `make test`: syntax/compile checks and 3 unit tests passed.
- `make test-integration`: 4 real-browser cases passed in 99 seconds under
non-root, read-only Docker, dropped capabilities, no-new-privileges, 1 CPU,
1Gi memory and bounded tmpfs. [Log](2026-10-13-browser-integration.log).
- The added missing-browser case separately passed: HTTP 503 launch preflight
and rejected run admission. [Log](2026-10-13-missing-browser.log).
- [Build log](2026-10-13-build.log): final pinned runtime image built successfully.
- [Independent trials](2026-10-13-independent-local-consumer.md): initial and
final-image consumer trials, keyboard/screenshots and failure cases passed.
- [Final-image restart](2026-10-13-final-image-restart.json): availability recovered
and completed run/artifacts remained retrievable from its retained volume.
- [Pinned OnCo pilot](../content-explorer-pilot.md): home-page responsive and
skip-link evidence passed; content-explorer retains its own acceptance.
The integration suite checks fresh cookie/localStorage state, foreign-token
receipt/artifact denial, protocol mismatch, assertion failure, unavailable target,
blocked redirect without request leakage, concurrent admission bounds, queued
and active cancellation, hard timeout, forced worker crash with no remaining
browser process group, subsequent recovery, interrupted persisted work and
expiry deletion. Temporary browser profiles and home directories are scoped to
the supervised worker and removed after its exit. This is not proof of safety
for hostile pages or mutually untrusted tenants; Chromium's OS sandbox is off.
Package schema validation passed against railiance-master with rail-kubernetes
references. Digest rendering and Kubernetes client validation passed for all
seven objects. No server-side admission success is claimed. The namespace does
not exist and no cluster object was created in this session. At the recorded
capacity check, railiance01 was Ready with 3460m CPU requested out of 4000m;
the package requests 100m and uses Recreate. Recheck before deployment.
## Publication and owner dependencies
Three OCI upload attempts encountered final blob PUT failures (HTTP 499, then
HTTP 500); smaller layers uploaded. Remote manifest inspection returned
`manifest unknown`. Local image index:
`sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441`.
This digest identifies a built image, not an available registry release.
The founder selected railiance-platform lifecycle ownership. Admission request
`c82d75f3-3191-410d-a8c8-fd647bf7f3ef` and the final
[platform handoff](2026-10-13-platform-implementation-notification.json) await
an owner response. Cluster access binding, live restart and rollback remain
WP-0004-T01 work. There is no previous managed release for cross-version rollback.
The [reuse-surface handoff](2026-10-13-reuse-surface-implementation-notification.json)
asks the owner to publish verified local provider references without claiming
managed availability. Readback at 19:18 Europe/Berlin still showed the source-only
D3/A2/C2/R2 entry and planned provider; no maintained update was inferred from
message delivery. The [published discovery readback](2026-10-13-discovery-readback.json)
also returned the planned provider and D3/A2/C2/R2 from the live federated API.
WP-0004-T03 remains open. The
[content-explorer handoff](2026-10-13-content-explorer-implementation-notification.json)
delivers the home-page evidence without closing CONTENT-WP-0000-T04.
The development provider, failed intermediate check container and temporary
OnCo server were stopped. The final local provider remains on loopback port
18083 for inspection; it is a workstation process, not a managed endpoint.
Server artifacts expire after one hour; the linked safe evidence is retained.

View file

@ -0,0 +1,38 @@
[
{
"name": "desktop-6-keyboard-result.png",
"dimensions": [
1280,
800
],
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad",
"bytes": 26622
},
{
"name": "desktop-final.png",
"dimensions": [
1280,
800
],
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad",
"bytes": 26622
},
{
"name": "narrow-6-keyboard-result.png",
"dimensions": [
390,
844
],
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8",
"bytes": 24280
},
{
"name": "narrow-final.png",
"dimensions": [
390,
844
],
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8",
"bytes": 24280
}
]

View file

@ -0,0 +1,29 @@
{
"trial_exit": 0,
"intentional_ui_failure_exit": 2,
"failed_ui_status_command_exit": 0,
"unavailable_service_preflight": {
"argv": [
"python3",
"client/rapp_playwright.py",
"--url",
"http://127.0.0.1:18089",
"preflight"
],
"exit": 3,
"response": {
"error": "service_unreachable: establish the documented tunnel and retry"
}
},
"trial_directory_mode": "0700",
"trial_handle_mode": "0600",
"external_target_tested": false,
"status_without_url_override": {
"exit": 3,
"response": {
"error": "HTTP 404: not_found"
},
"service_url": "http://127.0.0.1:18081"
},
"failed_ui_status_command_url": "http://127.0.0.1:18083"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,24 @@
{
"api_version": "1",
"target": "fixture",
"label": "independent-final-ui-failure",
"timeout_ms": 30000,
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"steps": [
{
"action": "screenshot",
"name": "before-failure"
},
{
"action": "text",
"selector": "#count",
"value": "99999"
}
]
}

View file

@ -0,0 +1,11 @@
{
"argv": [
"client/rapp_playwright.py",
"--url",
"http://127.0.0.1:18083",
"trial",
"--out",
".local/independent-final-01"
],
"service_url": "http://127.0.0.1:18083"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,22 @@
{
"available": true,
"error": null,
"checked_at": "2026-10-13T17:14:29.108Z",
"browser_version": "156.0.8078.4",
"api_version": "1",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"execution_location": "workstation-release-check",
"release": "sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441",
"allowed_origins": [],
"fixture": true,
"active": 0,
"queued": 0,
"limits": {
"concurrency": 1,
"max_queue": 4,
"max_timeout_ms": 120000,
"retention_ms": 3600000,
"max_runs": 64
}
}

View file

@ -0,0 +1,184 @@
{
"id": "35124424-950a-4339-adea-45525b470cab",
"api_version": "1",
"status": "passed",
"error": null,
"label": "independent-browser-trial",
"target": "fixture",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
},
{
"name": "narrow",
"width": 390,
"height": 844
}
],
"timeout_ms": 60000,
"created_at": "2026-10-13T17:14:50.050Z",
"expires_at": "2026-10-13T18:14:50.050Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441",
"execution_location": "workstation-release-check",
"browser_version": "156.0.8078.4",
"completed_steps": 14,
"steps": [
{
"viewport": "desktop",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T17:14:56.554Z",
"selector": "h1"
},
{
"viewport": "desktop",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T17:14:57.033Z",
"selector": "#count"
},
{
"viewport": "desktop",
"index": 2,
"action": "press",
"status": "passed",
"at": "2026-10-13T17:14:57.233Z",
"key": "Tab"
},
{
"viewport": "desktop",
"index": 3,
"action": "focused",
"status": "passed",
"at": "2026-10-13T17:14:57.247Z",
"selector": "#increment"
},
{
"viewport": "desktop",
"index": 4,
"action": "press",
"status": "passed",
"at": "2026-10-13T17:14:57.341Z",
"key": "Enter"
},
{
"viewport": "desktop",
"index": 5,
"action": "text",
"status": "passed",
"at": "2026-10-13T17:14:57.354Z",
"selector": "#count"
},
{
"viewport": "desktop",
"index": 6,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T17:14:57.441Z"
},
{
"viewport": "narrow",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T17:15:00.135Z",
"selector": "h1"
},
{
"viewport": "narrow",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T17:15:00.349Z",
"selector": "#count"
},
{
"viewport": "narrow",
"index": 2,
"action": "press",
"status": "passed",
"at": "2026-10-13T17:15:00.547Z",
"key": "Tab"
},
{
"viewport": "narrow",
"index": 3,
"action": "focused",
"status": "passed",
"at": "2026-10-13T17:15:00.633Z",
"selector": "#increment"
},
{
"viewport": "narrow",
"index": 4,
"action": "press",
"status": "passed",
"at": "2026-10-13T17:15:00.644Z",
"key": "Enter"
},
{
"viewport": "narrow",
"index": 5,
"action": "text",
"status": "passed",
"at": "2026-10-13T17:15:00.654Z",
"selector": "#count"
},
{
"viewport": "narrow",
"index": 6,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T17:15:00.734Z"
}
],
"artifacts": [
{
"name": "desktop-6-keyboard-result.png",
"bytes": 26622
},
{
"name": "desktop-final.png",
"bytes": 26622
},
{
"name": "narrow-6-keyboard-result.png",
"bytes": 24280
},
{
"name": "narrow-final.png",
"bytes": 24280
}
],
"started_at": "2026-10-13T17:14:50.055Z",
"blocked_requests": 13,
"finished_at": "2026-10-13T17:15:01.861Z",
"downloads": [
{
"name": "desktop-6-keyboard-result.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "desktop-final.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "narrow-6-keyboard-result.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
},
{
"name": "narrow-final.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
}
]
}

View file

@ -0,0 +1,13 @@
{
"argv": [
"client/rapp_playwright.py",
"--url",
"http://127.0.0.1:18083",
"run",
"--spec",
".local/independent-final-01/failure-spec.json",
"--out",
".local/independent-final-ui-failure-01"
],
"service_url": "http://127.0.0.1:18083"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,57 @@
{
"id": "75c6b50e-94a1-467f-98fc-7e8937f4362e",
"api_version": "1",
"status": "failed",
"error": "ui_assertion_failed",
"label": "independent-final-ui-failure",
"target": "fixture",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"timeout_ms": 30000,
"created_at": "2026-10-13T17:15:12.803Z",
"expires_at": "2026-10-13T18:15:12.803Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441",
"execution_location": "workstation-release-check",
"browser_version": "156.0.8078.4",
"completed_steps": 1,
"steps": [
{
"viewport": "desktop",
"index": 0,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T17:15:22.422Z"
},
{
"viewport": "desktop",
"index": 1,
"action": "text",
"status": "failed",
"at": "2026-10-13T17:15:23.542Z",
"selector": "#count"
}
],
"artifacts": [
{
"name": "desktop-0-before-failure.png",
"bytes": 25740
}
],
"started_at": "2026-10-13T17:15:12.806Z",
"blocked_requests": 9,
"finished_at": "2026-10-13T17:15:29.015Z",
"downloads": [
{
"name": "desktop-0-before-failure.png",
"bytes": 25740,
"sha256": "141dcadca4774da5fefec835bb789c5221685d3f453070a593e69aa6ad4782ce"
}
]
}

View file

@ -0,0 +1,6 @@
{
"http_status": 404,
"body": {
"error": "not_found"
}
}

View file

@ -0,0 +1,140 @@
# Independent local consumer trial — 2026-10-13
The fresh consumer agent completed the documented Python/HTTP trial against
`http://127.0.0.1:18081` without installing Playwright or a browser, reading
provider source, or copying sibling fixtures. This proves the **local Docker
container** consumer path only. Preflight and every receipt identify
`execution_location: workstation-container`, `release: development`, service
0.1.0, Playwright 1.64.0 and Chromium 156.0.8078.4. It does not prove a Railiance
deployment or accept content-explorer's own task.
The agent first read the repository instructions and
[consumer contract](../consumer-contract.md), then ran:
```sh
python3 client/rapp_playwright.py preflight
python3 client/rapp_playwright.py trial --out .local/independent-trial-01
```
The fresh output directory was created by the documented client. Preflight
reported browser availability; the retained follow-up
[preflight](2026-10-13-independent-local-consumer/preflight.json) records the
same runtime identity. The successful run
`6b97d75c-0c39-415f-9cf4-f1813c205066` ran from
2026-10-13T16:43:17.124Z to 16:43:30.498Z.
## Evidence
| Check | Observed result |
| --- | --- |
| Keyboard and assertions | All 14 steps passed across desktop and narrow contexts: visible heading, count 0, Tab, focused increment button, Enter, count 1, screenshot. |
| PNG dimensions | Both desktop files are 1280×800; both narrow files are 390×844. Parsed the PNG IHDR directly and independently recomputed each download SHA-256. |
| Screenshot inspection | Narrow final image visibly shows Count: 1 and an orange focus outline around Increment; no horizontal overflow is visible. This is inspection, not automated visual-baseline acceptance. |
| Capability boundary | GET of the successful run with a deliberately incorrect bearer token returned HTTP 404 and `not_found`. |
| Deliberate UI failure | Run `d6b71146-4f82-4d9c-bf72-4d2b672080d2` expected impossible count 99999: `ui_assertion_failed`, client exit 2, failed desktop step index 1/action text, one prior screenshot preserved. |
| Target failure | Run `57b03fc8-4947-43a9-8c9b-058f1c46a9e0` navigated to an intentionally missing path on allowed `http://host.docker.internal:3300`: `target_unreachable`, client exit 3, zero completed steps/artifacts. Receipt does not distinguish HTTP status, connection, DNS or timeout cause. |
| Service connection failure | Preflight against unused local service port 18089 returned `service_unreachable: establish the documented tunnel and retry`, exit 3. The real service was not stopped. |
| Private handles | Each client-created output directory is 0700 and its run.json is 0600. No run handles or bearer capabilities are copied into retained evidence. |
Retained evidence:
- [Successful receipt](2026-10-13-independent-local-consumer/receipt.json),
[invocation](2026-10-13-independent-local-consumer/invocation.json),
[server trial specification](2026-10-13-independent-local-consumer/trial-spec.json)
and [independent artifact checks](2026-10-13-independent-local-consumer/artifact-checks.json).
- [Desktop screenshot](2026-10-13-independent-local-consumer/desktop-final.png)
and [narrow screenshot](2026-10-13-independent-local-consumer/narrow-final.png).
- [Wrong-token response](2026-10-13-independent-local-consumer/wrong-token.json).
- [UI failure receipt](2026-10-13-independent-local-consumer/ui-failure-receipt.json),
[specification](2026-10-13-independent-local-consumer/failure-spec.json),
[invocation](2026-10-13-independent-local-consumer/ui-failure-invocation.json)
and [preserved screenshot](2026-10-13-independent-local-consumer/ui-failure-prior-screenshot.png).
- [Target failure receipt](2026-10-13-independent-local-consumer/target-failure-receipt.json),
[specification](2026-10-13-independent-local-consumer/target-spec.json)
and [invocation](2026-10-13-independent-local-consumer/target-failure-invocation.json).
- [Client exits, unavailable service probe and permissions](2026-10-13-independent-local-consumer/client-outcomes.json).
## Usability findings
1. The first-command path worked without source inspection or dependency setup.
Error categories distinguish assertion, target and service connection failure.
2. At trial time, the contract's general “Client exit codes” wording did not
explain that `status --handle` returns 0 when successfully retrieving a failed
run. Both failed runs above reproduced this. A script must inspect JSON status;
clarify that command-specific behavior or align its exit codes.
3. Target diagnosis identifies the correct failure category but supplies no
sanitized reason such as HTTP status or connection failure. The documentation
points to browser-network reachability; more precise safe diagnostics would
reduce operator round trips. This test makes no claim about which transport
cause produced the observed error.
4. At initial reading, SCOPE.md, AGENTS.md and workplans/README.md still described
a planning-only repository despite the working client and service. The parent
delivery session was informed; those status statements need reconciliation
before publishing managed availability.
This report does not establish lifecycle cleanup, cancellation, restart recovery,
cross-run browser state isolation, hostile-page safety, a managed endpoint, or
consumer application acceptance; those require separate evidence.
## Final release verification — 2026-10-13, 17:14–17:16 UTC
Repeated the independent client trial against the corrected immutable release at
`http://127.0.0.1:18083`, using fresh client output directories:
```sh
python3 client/rapp_playwright.py --url http://127.0.0.1:18083 preflight
python3 client/rapp_playwright.py --url http://127.0.0.1:18083 trial --out .local/independent-final-01
python3 client/rapp_playwright.py --url http://127.0.0.1:18083 run --spec .local/independent-final-01/failure-spec.json --out .local/independent-final-ui-failure-01
```
The release recorded by preflight and both receipts is
`sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441`.
Execution location is `workstation-release-check`; service 0.1.0, Playwright
1.64.0 and Chromium 156.0.8078.4 remain unchanged. Preflight lists no allowed
external origins, and this repeat used only the provider fixture. No external
target, redirect diagnosis or target-unreachable probe was attempted against
this fixture-only container.
Run `35124424-950a-4339-adea-45525b470cab` passed all 14 steps, from
2026-10-13T17:14:50.055Z through 17:15:01.861Z, and the client exited 0. Independent
PNG header/hash checks again confirmed four screenshots: two at 1280×800 and two
at 390×844. Their hashes match the initial successful trial. The run-token
rejection probe again returned HTTP 404 / `not_found`. The new output directory
is 0700 and its private handle is 0600.
Run `75c6b50e-94a1-467f-98fc-7e8937f4362e` deliberately failed the count assertion
at desktop step index 1 with `ui_assertion_failed`, client exit 2, and the prior
1280×800 screenshot preserved with a verified hash. Preflight against unused
service port 18089 again produced `service_unreachable` and exit 3. Retrieving the
failed run with `--url http://127.0.0.1:18083 status --handle` succeeded with exit 0, now explicitly documented
in the consumer contract. AGENTS.md and README.md now describe implemented
verification commands and distinguish local evidence from managed availability.
Recovery usability detail: `--url` must also be repeated on `status` for a
nondefault endpoint. The first status invocation omitted it, queried the default
18081 service and returned HTTP 404 / exit 3; repeating with the correct 18083
override returned the expected failed-run receipt / exit 0. The private handle
does not automatically select the originating service URL.
Retained final evidence:
- [Preflight](2026-10-13-independent-final-consumer/preflight.json),
[successful receipt](2026-10-13-independent-final-consumer/receipt.json),
[invocation](2026-10-13-independent-final-consumer/invocation.json),
[artifact checks](2026-10-13-independent-final-consumer/artifact-checks.json),
[wrong-token response](2026-10-13-independent-final-consumer/wrong-token.json)
and [client outcomes](2026-10-13-independent-final-consumer/client-outcomes.json).
- [Desktop final](2026-10-13-independent-final-consumer/desktop-final.png),
[desktop keyboard](2026-10-13-independent-final-consumer/desktop-6-keyboard-result.png),
[narrow final](2026-10-13-independent-final-consumer/narrow-final.png)
and [narrow keyboard](2026-10-13-independent-final-consumer/narrow-6-keyboard-result.png).
- [Failed assertion receipt](2026-10-13-independent-final-consumer/ui-failure-receipt.json),
[specification](2026-10-13-independent-final-consumer/failure-spec.json),
[invocation](2026-10-13-independent-final-consumer/ui-failure-invocation.json)
and [preserved screenshot](2026-10-13-independent-final-consumer/ui-failure-prior-screenshot.png).
These are local final-release consumer checks. Read-only root configuration,
worker temporary-directory isolation and crash cleanup were reported by the
provider; this consumer trial does not independently inspect or establish those
properties. Managed platform admission remains pending, and no Railiance
deployment or content-explorer acceptance is claimed.

View file

@ -0,0 +1,38 @@
[
{
"name": "desktop-6-keyboard-result.png",
"dimensions": [
1280,
800
],
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad",
"bytes": 26622
},
{
"name": "desktop-final.png",
"dimensions": [
1280,
800
],
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad",
"bytes": 26622
},
{
"name": "narrow-6-keyboard-result.png",
"dimensions": [
390,
844
],
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8",
"bytes": 24280
},
{
"name": "narrow-final.png",
"dimensions": [
390,
844
],
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8",
"bytes": 24280
}
]

View file

@ -0,0 +1,34 @@
{
"trial_exit": 0,
"intentional_ui_failure_exit": 2,
"target_failure_exit": 3,
"failed_ui_status_command_exit": 0,
"failed_target_status_command_exit": 0,
"unavailable_service_preflight": {
"argv": [
"python3",
"client/rapp_playwright.py",
"--url",
"http://127.0.0.1:18089",
"preflight"
],
"exit": 3,
"response": {
"error": "service_unreachable: establish the documented tunnel and retry"
}
},
"permissions": {
".local/independent-trial-01": {
"directory_mode": "0o700",
"handle_mode": "0o600"
},
".local/independent-ui-failure-01": {
"directory_mode": "0o700",
"handle_mode": "0o600"
},
".local/independent-target-failure-01": {
"directory_mode": "0o700",
"handle_mode": "0o600"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,24 @@
{
"api_version": "1",
"target": "fixture",
"label": "independent-intentional-ui-failure",
"timeout_ms": 30000,
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"steps": [
{
"action": "screenshot",
"name": "before-failure"
},
{
"action": "text",
"selector": "#count",
"value": "99999"
}
]
}

View file

@ -0,0 +1,9 @@
{
"argv": [
"client/rapp_playwright.py",
"trial",
"--out",
".local/independent-trial-01"
],
"service_url": "http://127.0.0.1:18081"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,25 @@
{
"available": true,
"error": null,
"checked_at": "2026-10-13T16:43:51.651Z",
"browser_version": "156.0.8078.4",
"api_version": "1",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"execution_location": "workstation-container",
"release": "development",
"allowed_origins": [
"http://host.docker.internal:3300",
"https://onco.cc"
],
"fixture": true,
"active": 1,
"queued": 0,
"limits": {
"concurrency": 1,
"max_queue": 4,
"max_timeout_ms": 120000,
"retention_ms": 3600000,
"max_runs": 64
}
}

View file

@ -0,0 +1,184 @@
{
"id": "6b97d75c-0c39-415f-9cf4-f1813c205066",
"api_version": "1",
"status": "passed",
"error": null,
"label": "independent-browser-trial",
"target": "fixture",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
},
{
"name": "narrow",
"width": 390,
"height": 844
}
],
"timeout_ms": 60000,
"created_at": "2026-10-13T16:43:17.082Z",
"expires_at": "2026-10-13T17:43:17.082Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "development",
"execution_location": "workstation-container",
"browser_version": "156.0.8078.4",
"completed_steps": 14,
"steps": [
{
"viewport": "desktop",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T16:43:27.379Z",
"selector": "h1"
},
{
"viewport": "desktop",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:43:27.722Z",
"selector": "#count"
},
{
"viewport": "desktop",
"index": 2,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:43:27.838Z",
"key": "Tab"
},
{
"viewport": "desktop",
"index": 3,
"action": "focused",
"status": "passed",
"at": "2026-10-13T16:43:27.884Z",
"selector": "#increment"
},
{
"viewport": "desktop",
"index": 4,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:43:28.011Z",
"key": "Enter"
},
{
"viewport": "desktop",
"index": 5,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:43:28.027Z",
"selector": "#count"
},
{
"viewport": "desktop",
"index": 6,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:43:28.052Z"
},
{
"viewport": "narrow",
"index": 0,
"action": "visible",
"status": "passed",
"at": "2026-10-13T16:43:29.234Z",
"selector": "h1"
},
{
"viewport": "narrow",
"index": 1,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:43:29.476Z",
"selector": "#count"
},
{
"viewport": "narrow",
"index": 2,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:43:29.640Z",
"key": "Tab"
},
{
"viewport": "narrow",
"index": 3,
"action": "focused",
"status": "passed",
"at": "2026-10-13T16:43:29.663Z",
"selector": "#increment"
},
{
"viewport": "narrow",
"index": 4,
"action": "press",
"status": "passed",
"at": "2026-10-13T16:43:29.757Z",
"key": "Enter"
},
{
"viewport": "narrow",
"index": 5,
"action": "text",
"status": "passed",
"at": "2026-10-13T16:43:29.773Z",
"selector": "#count"
},
{
"viewport": "narrow",
"index": 6,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:43:29.814Z"
}
],
"artifacts": [
{
"name": "desktop-6-keyboard-result.png",
"bytes": 26622
},
{
"name": "desktop-final.png",
"bytes": 26622
},
{
"name": "narrow-6-keyboard-result.png",
"bytes": 24280
},
{
"name": "narrow-final.png",
"bytes": 24280
}
],
"started_at": "2026-10-13T16:43:17.124Z",
"blocked_requests": 0,
"finished_at": "2026-10-13T16:43:30.498Z",
"downloads": [
{
"name": "desktop-6-keyboard-result.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "desktop-final.png",
"bytes": 26622,
"sha256": "b15998824256b86c74f2bbb75f061061be65db0b4f3bb5444529ac69a723cdad"
},
{
"name": "narrow-6-keyboard-result.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
},
{
"name": "narrow-final.png",
"bytes": 24280,
"sha256": "49d27cb169b8e180ce93969c6d2b8619a7684cf035bb9dcae9cd91d8bd9952e8"
}
]
}

View file

@ -0,0 +1,11 @@
{
"argv": [
"client/rapp_playwright.py",
"run",
"--spec",
".local/independent-trial-01/target-spec.json",
"--out",
".local/independent-target-failure-01"
],
"service_url": "http://127.0.0.1:18081"
}

View file

@ -0,0 +1,30 @@
{
"id": "57b03fc8-4947-43a9-8c9b-058f1c46a9e0",
"api_version": "1",
"status": "failed",
"error": "target_unreachable",
"label": "independent-target-http-failure",
"target": "http://host.docker.internal:3300/independent-trial-deliberately-missing-page-20261013",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"timeout_ms": 30000,
"created_at": "2026-10-13T16:44:48.609Z",
"expires_at": "2026-10-13T17:44:48.609Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "development",
"execution_location": "workstation-container",
"browser_version": "156.0.8078.4",
"completed_steps": 0,
"steps": [],
"artifacts": [],
"started_at": "2026-10-13T16:44:48.614Z",
"blocked_requests": 0,
"finished_at": "2026-10-13T16:45:11.290Z",
"downloads": []
}

View file

@ -0,0 +1,19 @@
{
"api_version": "1",
"target": "http://host.docker.internal:3300/independent-trial-deliberately-missing-page-20261013",
"label": "independent-target-http-failure",
"timeout_ms": 30000,
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"steps": [
{
"action": "visible",
"selector": "h1"
}
]
}

View file

@ -0,0 +1,37 @@
{
"api_version": "1",
"target": "fixture",
"label": "independent-browser-trial",
"steps": [
{
"action": "visible",
"selector": "h1"
},
{
"action": "text",
"selector": "#count",
"value": "0"
},
{
"action": "press",
"key": "Tab"
},
{
"action": "focused",
"selector": "#increment"
},
{
"action": "press",
"key": "Enter"
},
{
"action": "text",
"selector": "#count",
"value": "1"
},
{
"action": "screenshot",
"name": "keyboard-result"
}
]
}

View file

@ -0,0 +1,11 @@
{
"argv": [
"client/rapp_playwright.py",
"run",
"--spec",
".local/independent-trial-01/failure-spec.json",
"--out",
".local/independent-ui-failure-01"
],
"service_url": "http://127.0.0.1:18081"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,57 @@
{
"id": "d6b71146-4f82-4d9c-bf72-4d2b672080d2",
"api_version": "1",
"status": "failed",
"error": "ui_assertion_failed",
"label": "independent-intentional-ui-failure",
"target": "fixture",
"viewports": [
{
"name": "desktop",
"width": 1280,
"height": 800
}
],
"timeout_ms": 30000,
"created_at": "2026-10-13T16:44:28.954Z",
"expires_at": "2026-10-13T17:44:28.954Z",
"service_version": "0.1.0",
"playwright_version": "1.64.0",
"release": "development",
"execution_location": "workstation-container",
"browser_version": "156.0.8078.4",
"completed_steps": 1,
"steps": [
{
"viewport": "desktop",
"index": 0,
"action": "screenshot",
"status": "passed",
"at": "2026-10-13T16:44:41.184Z"
},
{
"viewport": "desktop",
"index": 1,
"action": "text",
"status": "failed",
"at": "2026-10-13T16:44:41.508Z",
"selector": "#count"
}
],
"artifacts": [
{
"name": "desktop-0-before-failure.png",
"bytes": 25740
}
],
"started_at": "2026-10-13T16:44:36.279Z",
"blocked_requests": 0,
"finished_at": "2026-10-13T16:44:46.861Z",
"downloads": [
{
"name": "desktop-0-before-failure.png",
"bytes": 25740,
"sha256": "141dcadca4774da5fefec835bb789c5221685d3f453070a593e69aa6ad4782ce"
}
]
}

View file

@ -0,0 +1,6 @@
{
"http_status": 404,
"body": {
"error": "not_found"
}
}

View file

@ -0,0 +1,9 @@
✔ missing browser fails preflight and prevents admission (3280.374816ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4049.467658

View file

@ -0,0 +1,17 @@
{
"id": "e77247e5-ce25-4711-a64f-af81a9f7cbb1",
"from_agent": "rapp-playwright",
"to_agent": "railiance-platform",
"subject": "Browser package ready locally; admission and Forgejo OCI upload blockers",
"body": "Founder explicitly selected railiance-platform lifecycle ownership. Final source implementation and independent client evidence are in rapp-playwright; candidate local OCI index sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441, Playwright1.64.0/Chromium156.0.8078.4. Read-only Docker integration4/4 plus missing-browser negative1/1; independent final trial passed. No cluster mutation. Current node92.205.62.239 Ready, requests3460m/4CPU and9472Mi; package100m/256Mi limit1CPU/1Gi,2GiPVC,loopbackRBAC,Recreate. OCI publication to forgejo.coulomb.social/coulomb/rapp-playwright:0.1.0 failed repeatedly at final blob PUT: HTTP499 then500, including digests9a29e489bcc76cee14bece1a90741a2a7512f0c69ffefbe7f5d17500f24d8ba5 and071058c704d16066a57f826f0df652f0f8780f9e825cbf4242946b68f4fa79e0. Smaller layers uploaded; manifest publication not established. Please confirm admission/access lane and diagnose registry large-blob failures in owner scope. WP0003-T01 and WP0004-T01 remain explicit waits. Evidence/docs and sync commit will be in repo; no secret or credential request.",
"thread_id": "c82d75f3-3191-410d-a8c8-fd647bf7f3ef",
"read_at": null,
"archived_at": null,
"created_at": "2026-10-13T17:18:22.948833Z",
"kind": "message",
"expires_at": null,
"supersedes_id": null,
"reader": null,
"delivered_at": null,
"acknowledged_at": null
}

View file

@ -0,0 +1,17 @@
{
"id": "30503ee1-77f9-4e9e-a63a-2f70ac097823",
"from_agent": "rapp-playwright",
"to_agent": "reuse-surface",
"subject": "rapp-playwright implemented: independent local consumer evidence; managed admission pending",
"body": "WP-0004 provider handoff: service and Python stdlib client implemented in /home/worsch/rapp-playwright. docs/consumer-contract.md is the v1 command/API contract. docs/evidence/2026-10-13-independent-local-consumer.md retains independent successful fixture, two viewports, keyboard evidence, wrong-token/UI/target/service negatives. docs/content-explorer-pilot.md maps the pinned OnCo home-page pilot. make test-integration now passes real Chromium isolation, proxy redirects, cancellation/timeout/crash/restart/expiry under read-only Docker. Playwright 1.64.0 / Chromium 156.0.8078.4. Founder selected railiance-platform lifecycle ownership; managed admission remains pending (c82d75f3-3191-410d-a8c8-fd647bf7f3ef), so do not claim Railiance availability. Please update existing capability.infotech.headless-browser with local verified provider contract/evidence and the conditional managed command, then return published discovery readback. Keep source-only fallback explicit. Source commit/immutable image evidence will follow in docs/evidence. No duplicate capability.",
"thread_id": null,
"read_at": null,
"archived_at": null,
"created_at": "2026-10-13T17:12:22.927559Z",
"kind": "message",
"expires_at": null,
"supersedes_id": null,
"reader": null,
"delivered_at": null,
"acknowledged_at": null
}

71
docs/operations.md Normal file
View file

@ -0,0 +1,71 @@
# Service operations
Build with `make install test build`. `make test-integration` launches real Chromium
inside the pinned upstream image; it needs Docker, not a host browser. Publish
the tested image through the existing Forgejo OCI credential lane and read its
digest back. `make render IMAGE_REFERENCE=forgejo.coulomb.social/coulomb/rapp-playwright@sha256:…`
rejects floating tags. Never record a placeholder as a published digest.
The initial package uses a single replica, Recreate updates, 100m CPU/256Mi
requests, 1 CPU/1Gi limits, and a 2Gi local-path PVC. Artifacts expire after one
hour; they are transient evidence, not a backup service. Consumers download
what they need to retain. Recreate avoids a surge on the capacity-constrained
single-node cluster and preserves the RWO volume.
Before activation, confirm the node identity and current resource headroom,
platform lifecycle ownership/admission, publication/pull access, and private
consumer connectivity. Run `make server-dry-run IMAGE_REFERENCE=…`, inspect
`kubectl diff -f .rendered/workload.yaml` on railiance01, then deploy the reviewed
manifest with `make deploy IMAGE_REFERENCE=…`. No existing workload or policy
needs relaxation. Platform confirmation is tracked in WP-0004-T01.
For the first installation, after admission, run `make bootstrap-namespace
IMAGE_REFERENCE=…` before the full server dry run. Kubernetes does not persist
a dry-run Namespace, so namespace-scoped objects cannot be server-validated
until the namespace exists. This bootstrap dry-runs and diffs only the reviewed
Namespace before creating it. The full deployment also diffs before applying.
The `playwright-consumer` Role grants only pod discovery and port-forward. Bind
an approved existing user/group through the identity/platform owner. This repo
does not mint credentials or silently bind all authenticated users. Callers with
existing authorized Kubernetes access can use the documented port-forward.
The pod has no service-account token and no cluster permissions.
Readiness performs the authenticated-protocol preflight over pod loopback. It
requires a successful browser launch; liveness checks the process only. The pod
uses non-root UID/GID 1000, read-only root, dropped capabilities and RuntimeDefault
seccomp. Tini reaps orphaned browser processes. Chromium's OS sandbox is disabled
for this trusted-target pilot; it is not approved for hostile websites or
mutually untrusted tenants. The HTTP/CONNECT origin gate, private-address checks,
blocked workers/WebSockets, per-run process/context and resource limits cover
the documented application-level boundary, not browser-kernel exploits.
## Verify and recover
After establishing the consumer tunnel, `make verify-live TRIAL_OUT=.local/live-N`
must produce four PNGs, focus/keyboard assertions and a receipt. Check the live
container image and imageID against the published digest, PVC binding, deny-all
ingress and ready replica. Exercise wrong run token, deliberate UI failure,
unreachable target, cancellation and service restart; a passing health endpoint
alone is insufficient.
After restart, accepted nonterminal runs become `interrupted`; completed runs
remain retrievable until expiry. A client disconnect does not cancel an accepted
job, so all runs have enforced deadlines. Supervisor cancellation closes the
browser; after two seconds it kills the recorded worker/browser process groups.
Service shutdown drains/cancels bounded work. Corrupt receipts cause startup to
fail closed; restore or quarantine the affected transient run directory through
the owner, without exposing capability hashes or screenshots in logs.
Rollback: retain the previous immutable image reference and rendered revision,
review its schema compatibility, then run `make rollback IMAGE_REFERENCE=…`.
Verify the replacement's imageID and repeat the trial. For the first release,
test recovery by reapplying the same verified digest; do not call that proof of
cross-version rollback. Once a second release exists, exercise an actual digest
change and return to the reviewed release. No automated rollback migrates or
deletes the PVC. V1 receipts are JSON; future incompatible formats require an
explicit migration/rollback decision.
Upgrade Playwright and its base image together, update the lockfile, repeat the
unit/browser/client tests and consumer pilot, then publish a new immutable image.
The Python client checks protocol v1 and does not install Playwright.