diff --git a/.gitignore b/.gitignore index 9fd4227..997fd50 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ helm/*.yaml !helm/*-probe.yaml !helm/vergabe-demo-company-role.yaml !helm/vergabe-demo-company-database.yaml +!helm/playwright-consumer-access.yaml # ArgoCD repository credentials — encrypt locally, never commit argocd/repositories/*.repository.sops.yaml diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index c6a8686..dc7f3aa 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -69,7 +69,7 @@ | task | RPF-WP-0035-T13 | done | — | workplans/RPF-WP-0035-credential-lane-implementation.md | | task | RPF-WP-0036-T01 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md | | task | RPF-WP-0036-T02 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md | -| task | RPF-WP-0036-T03 | todo | — | workplans/RPF-WP-0036-platform-service-assurance.md | +| task | RPF-WP-0036-T03 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md | | task | RPF-WP-0036-T04 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md | | task | RPF-WP-0036-T05 | done | — | workplans/RPF-WP-0036-platform-service-assurance.md | | task | RPF-WP-0036-T06 | wait | — | workplans/RPF-WP-0036-platform-service-assurance.md | @@ -94,12 +94,12 @@ | task | RPF-WP-0043-T05 | done | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md | | task | RPF-WP-0044-T01 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | | task | RPF-WP-0044-T02 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | -| task | RPF-WP-0044-T03 | todo | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | -| task | RPF-WP-0044-T04 | todo | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | -| task | RPF-WP-0044-T05 | todo | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | -| task | RPF-WP-0044-T06 | todo | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | +| task | RPF-WP-0044-T03 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | +| task | RPF-WP-0044-T04 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | +| task | RPF-WP-0044-T05 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | +| task | RPF-WP-0044-T06 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | | task | RPF-WP-0044-T07 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | -| task | RPF-WP-0044-T08 | wait | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | +| task | RPF-WP-0044-T08 | done | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | | task | RPF-WP-0045-T01 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | | task | RPF-WP-0045-T02 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | | task | RPF-WP-0045-T03 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | @@ -116,7 +116,7 @@ | task | RPF-WP-0047-T02 | done | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md | | task | RPF-WP-0047-T03 | done | — | workplans/RPF-WP-0047-bao-coulomb-social-notice-page.md | | task | RPF-WP-0048-T01 | done | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md | -| task | RPF-WP-0048-T02 | todo | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md | +| task | RPF-WP-0048-T02 | wait | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md | | task | RPF-WP-0048-T03 | done | — | workplans/RPF-WP-0048-activity-core-gitops-adoption.md | | task | RPF-WP-0049-T01 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md | | task | RPF-WP-0049-T02 | done | — | workplans/RPF-WP-0049-platform-root-custody-record-delivery.md | diff --git a/docs/adr/ADR-0006-playwright-consumer-group.md b/docs/adr/ADR-0006-playwright-consumer-group.md new file mode 100644 index 0000000..d7316f1 --- /dev/null +++ b/docs/adr/ADR-0006-playwright-consumer-group.md @@ -0,0 +1,54 @@ +--- +id: RPLAT-ADR-0006 +title: "ADR-0006 — Give Playwright consumers a dedicated namespace-scoped group" +status: accepted +revision: "1.0" +owner: railiance-platform +date: "2026-10-13" +last_reviewed: "2026-10-13" +review_interval: 6m +--- + +## Context + +rapp-playwright's published private pilot has a namespace Role but deliberately +no consumer binding. The user requested establishing a group during the Platform +open-work review. Existing cluster documentation provides administrative access; +the live binding inventory contains no non-system consumer group. + +## Decision + +Establish Kubernetes RBAC group subject `playwright-consumers`, bound only to +the package-owned `playwright-consumer` Role in namespace `playwright`. Platform +owns the RoleBinding; rapp-playwright retains the Role and workload source. +The initial membership policy includes all identified platform humans and coding +agents, as explicitly directed by the user. No per-use-case membership restriction +is introduced now. The identity/cluster owners must implement an authenticated +group claim for those entities before general consumer availability is accepted. + +## Consequences + +The Role permits pod get/list, pod port-forward creation and get of Deployment +`playwright`. It grants no Secrets, pod exec, workload writes, identity +impersonation or other namespaces. The namespace must remain dedicated to this +service because pod discovery and forwarding cover all its pods. Membership +confers browser-job submission for the reviewed trusted targets; it does not +authorize private targets, hostile pages or mutually untrusted tenant workloads. + +Kubernetes has no separate Group object to create. This establishes an RBAC +subject and permissions, not a KeyCape group, login integration or membership. +Administrator impersonation can verify the permission boundary but cannot prove +consumer authentication. RPF-WP-0056 retains those outstanding acceptance steps. + +Rollback removes only the Platform RoleBinding after reviewing its exact live +identity; it does not delete the Namespace, workload or artifact PVC. Existing +port-forwards require separate termination when revoking a consumer's active +access. Do not treat removal of a binding as proof that a running tunnel closed. + +## Alternatives considered + +Binding all authenticated users would broaden browser access beyond the human/coding-agent +identity population. Sharing the administrator kubeconfig would convey unrelated cluster +authority. Both are rejected. Selecting OIDC configuration or issuing client +certificates without an owner-supported membership route remains outside this +bounded group-binding change. diff --git a/docs/evidence/2026-10-13-playwright-managed/after-restart-receipt.json b/docs/evidence/2026-10-13-playwright-managed/after-restart-receipt.json new file mode 100644 index 0000000..0b82d98 --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-managed/after-restart-receipt.json @@ -0,0 +1,184 @@ +{ + "id": "b11fd28b-58fe-4b20-a15e-32b18ea76f2b", + "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-13T18:52:59.076Z", + "expires_at": "2026-10-13T19:52:59.076Z", + "service_version": "0.1.0", + "playwright_version": "1.64.0", + "release": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "execution_location": "railiance01/playwright", + "browser_version": "156.0.8078.4", + "completed_steps": 14, + "steps": [ + { + "viewport": "desktop", + "index": 0, + "action": "visible", + "status": "passed", + "at": "2026-10-13T18:53:01.091Z", + "selector": "h1" + }, + { + "viewport": "desktop", + "index": 1, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:53:01.214Z", + "selector": "#count" + }, + { + "viewport": "desktop", + "index": 2, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:53:01.323Z", + "key": "Tab" + }, + { + "viewport": "desktop", + "index": 3, + "action": "focused", + "status": "passed", + "at": "2026-10-13T18:53:01.360Z", + "selector": "#increment" + }, + { + "viewport": "desktop", + "index": 4, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:53:01.393Z", + "key": "Enter" + }, + { + "viewport": "desktop", + "index": 5, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:53:01.407Z", + "selector": "#count" + }, + { + "viewport": "desktop", + "index": 6, + "action": "screenshot", + "status": "passed", + "at": "2026-10-13T18:53:01.454Z" + }, + { + "viewport": "narrow", + "index": 0, + "action": "visible", + "status": "passed", + "at": "2026-10-13T18:53:02.526Z", + "selector": "h1" + }, + { + "viewport": "narrow", + "index": 1, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:53:02.786Z", + "selector": "#count" + }, + { + "viewport": "narrow", + "index": 2, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:53:02.877Z", + "key": "Tab" + }, + { + "viewport": "narrow", + "index": 3, + "action": "focused", + "status": "passed", + "at": "2026-10-13T18:53:02.891Z", + "selector": "#increment" + }, + { + "viewport": "narrow", + "index": 4, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:53:02.896Z", + "key": "Enter" + }, + { + "viewport": "narrow", + "index": 5, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:53:02.905Z", + "selector": "#count" + }, + { + "viewport": "narrow", + "index": 6, + "action": "screenshot", + "status": "passed", + "at": "2026-10-13T18:53:02.915Z" + } + ], + "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-13T18:52:59.078Z", + "blocked_requests": 12, + "finished_at": "2026-10-13T18:53:03.351Z", + "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" + } + ] +} diff --git a/docs/evidence/2026-10-13-playwright-managed/desktop-6-keyboard-result.png b/docs/evidence/2026-10-13-playwright-managed/desktop-6-keyboard-result.png new file mode 100644 index 0000000..91a2040 Binary files /dev/null and b/docs/evidence/2026-10-13-playwright-managed/desktop-6-keyboard-result.png differ diff --git a/docs/evidence/2026-10-13-playwright-managed/desktop-final.png b/docs/evidence/2026-10-13-playwright-managed/desktop-final.png new file mode 100644 index 0000000..91a2040 Binary files /dev/null and b/docs/evidence/2026-10-13-playwright-managed/desktop-final.png differ diff --git a/docs/evidence/2026-10-13-playwright-managed/narrow-6-keyboard-result.png b/docs/evidence/2026-10-13-playwright-managed/narrow-6-keyboard-result.png new file mode 100644 index 0000000..c2ad4e9 Binary files /dev/null and b/docs/evidence/2026-10-13-playwright-managed/narrow-6-keyboard-result.png differ diff --git a/docs/evidence/2026-10-13-playwright-managed/narrow-final.png b/docs/evidence/2026-10-13-playwright-managed/narrow-final.png new file mode 100644 index 0000000..c2ad4e9 Binary files /dev/null and b/docs/evidence/2026-10-13-playwright-managed/narrow-final.png differ diff --git a/docs/evidence/2026-10-13-playwright-managed/negative-checks.json b/docs/evidence/2026-10-13-playwright-managed/negative-checks.json new file mode 100644 index 0000000..5aad21a --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-managed/negative-checks.json @@ -0,0 +1,8 @@ +{ + "wrong_token": true, + "wrong_artifact_token": true, + "ui_failure_distinguished": true, + "bounded_cancellation": true, + "private_target_refused": true, + "preflight_healthy": true +} diff --git a/docs/evidence/2026-10-13-playwright-managed/preflight.json b/docs/evidence/2026-10-13-playwright-managed/preflight.json new file mode 100644 index 0000000..7d3b61e --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-managed/preflight.json @@ -0,0 +1,24 @@ +{ + "available": true, + "error": null, + "checked_at": "2026-10-13T18:49:56.214Z", + "browser_version": "156.0.8078.4", + "api_version": "1", + "service_version": "0.1.0", + "playwright_version": "1.64.0", + "execution_location": "railiance01/playwright", + "release": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "allowed_origins": [ + "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 + } +} diff --git a/docs/evidence/2026-10-13-playwright-managed/receipt.json b/docs/evidence/2026-10-13-playwright-managed/receipt.json new file mode 100644 index 0000000..af78292 --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-managed/receipt.json @@ -0,0 +1,184 @@ +{ + "id": "6a51e164-cec5-46ed-89b1-92b93dd16d23", + "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-13T18:50:40.554Z", + "expires_at": "2026-10-13T19:50:40.554Z", + "service_version": "0.1.0", + "playwright_version": "1.64.0", + "release": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "execution_location": "railiance01/playwright", + "browser_version": "156.0.8078.4", + "completed_steps": 14, + "steps": [ + { + "viewport": "desktop", + "index": 0, + "action": "visible", + "status": "passed", + "at": "2026-10-13T18:50:42.655Z", + "selector": "h1" + }, + { + "viewport": "desktop", + "index": 1, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:50:42.815Z", + "selector": "#count" + }, + { + "viewport": "desktop", + "index": 2, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:50:42.851Z", + "key": "Tab" + }, + { + "viewport": "desktop", + "index": 3, + "action": "focused", + "status": "passed", + "at": "2026-10-13T18:50:42.911Z", + "selector": "#increment" + }, + { + "viewport": "desktop", + "index": 4, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:50:42.924Z", + "key": "Enter" + }, + { + "viewport": "desktop", + "index": 5, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:50:42.937Z", + "selector": "#count" + }, + { + "viewport": "desktop", + "index": 6, + "action": "screenshot", + "status": "passed", + "at": "2026-10-13T18:50:42.949Z" + }, + { + "viewport": "narrow", + "index": 0, + "action": "visible", + "status": "passed", + "at": "2026-10-13T18:50:43.919Z", + "selector": "h1" + }, + { + "viewport": "narrow", + "index": 1, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:50:44.063Z", + "selector": "#count" + }, + { + "viewport": "narrow", + "index": 2, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:50:44.180Z", + "key": "Tab" + }, + { + "viewport": "narrow", + "index": 3, + "action": "focused", + "status": "passed", + "at": "2026-10-13T18:50:44.228Z", + "selector": "#increment" + }, + { + "viewport": "narrow", + "index": 4, + "action": "press", + "status": "passed", + "at": "2026-10-13T18:50:44.260Z", + "key": "Enter" + }, + { + "viewport": "narrow", + "index": 5, + "action": "text", + "status": "passed", + "at": "2026-10-13T18:50:44.275Z", + "selector": "#count" + }, + { + "viewport": "narrow", + "index": 6, + "action": "screenshot", + "status": "passed", + "at": "2026-10-13T18:50:44.314Z" + } + ], + "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-13T18:50:40.557Z", + "blocked_requests": 12, + "finished_at": "2026-10-13T18:50:44.840Z", + "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" + } + ] +} diff --git a/docs/evidence/2026-10-13-playwright-managed/runtime.json b/docs/evidence/2026-10-13-playwright-managed/runtime.json new file mode 100644 index 0000000..481d6c1 --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-managed/runtime.json @@ -0,0 +1,40 @@ +{ + "observed_at": "2026-10-13T18:52:58.325239+00:00", + "image": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "replica_ready": true, + "pvc_phase": "Bound", + "pvc_uid": "da36929a-1d69-457a-ad99-7580ef86ab18", + "pvc_size": "2Gi", + "initial_pod_uid": "3475ad46-d9d8-4b97-99b7-54b4d984d2f2", + "restarted_pod_uid": "a22030a4-c12d-4e97-be8d-98a8d80808c2", + "same_image_restart_completed_artifacts_verified": true, + "preflight_after_restart": { + "available": true, + "error": null, + "checked_at": "2026-10-13T18:51:59.595Z", + "browser_version": "156.0.8078.4", + "api_version": "1", + "service_version": "0.1.0", + "playwright_version": "1.64.0", + "execution_location": "railiance01/playwright", + "release": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "allowed_origins": [ + "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 + } + }, + "access_path": "Existing authenticated operator SSH; administrator impersonation of playwright-consumers; loopback-only port-forward", + "actual_consumer_authentication_proven": false, + "cross_version_rollback_proven": false, + "abrupt_interruption_recovery_proven": false, + "source_workload_sha256": "d5d3f3d816dd4320059b4fcdc8409fb84d6075aeb1f409f4d112263d7e14191d" +} diff --git a/docs/evidence/2026-10-13-playwright-rbac.json b/docs/evidence/2026-10-13-playwright-rbac.json new file mode 100644 index 0000000..cd070e8 --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-rbac.json @@ -0,0 +1,194 @@ +{ + "schema": "platform-playwright-rbac/v1", + "observed_at": "2026-10-13T18:49:52.369304+00:00", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "role_uid": "d24e8b51-7456-4000-8816-b4a49a06ca37", + "binding_uid": "190eb32f-8649-4b36-a525-8936cf3ceb51", + "group": "playwright-consumers", + "checks": [ + { + "member": true, + "attributes": { + "verb": "get", + "resource": "pods", + "subresource": "", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": true, + "allowed": true, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "list", + "resource": "pods", + "subresource": "", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": true, + "allowed": true, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "create", + "resource": "pods", + "subresource": "portforward", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": true, + "allowed": true, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "get", + "resource": "deployments", + "subresource": "", + "group": "apps", + "namespace": "playwright", + "name": "playwright" + }, + "expected": true, + "allowed": true, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "get", + "resource": "deployments", + "subresource": "", + "group": "apps", + "namespace": "playwright", + "name": "other" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "get", + "resource": "secrets", + "subresource": "", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "create", + "resource": "pods", + "subresource": "exec", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "create", + "resource": "pods", + "subresource": "", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "patch", + "resource": "deployments", + "subresource": "", + "group": "apps", + "namespace": "playwright", + "name": "playwright" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "create", + "resource": "pods", + "subresource": "portforward", + "group": "", + "namespace": "default", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": true, + "attributes": { + "verb": "create", + "resource": "rolebindings", + "subresource": "", + "group": "rbac.authorization.k8s.io", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": false, + "attributes": { + "verb": "list", + "resource": "pods", + "subresource": "", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + }, + { + "member": false, + "attributes": { + "verb": "create", + "resource": "pods", + "subresource": "portforward", + "group": "", + "namespace": "playwright", + "name": "" + }, + "expected": false, + "allowed": false, + "passed": true + } + ], + "passed": true, + "actual_consumer_authentication_proven": false +} diff --git a/docs/evidence/2026-10-13-playwright-source-review.json b/docs/evidence/2026-10-13-playwright-source-review.json new file mode 100644 index 0000000..b8e3557 --- /dev/null +++ b/docs/evidence/2026-10-13-playwright-source-review.json @@ -0,0 +1,16 @@ +{ + "schema": "platform-playwright-source-review/v1", + "source_commit": "70268154c06cf78c5d9e1f4eb900213ffd436d1b", + "source_sha256": { + "declarations/rapp.yaml": "5b030686f1e3ca737805e681fc8b714e1452f879a3fa30bbac8ca30798bb3343", + "manifests/workload.yaml": "7d76ad38e25597b629660d31782867d84d7241020a2e450e346a3792ce43290c", + "releases/0.1.0.json": "364dcbc16fd50155b7d8eddb0ecb87b1709f0efdc8a41c9f15afd81e5a18506e", + "docs/platform-admission.md": "c37dc9f77d5be5bc55c873af98e8ecb0bc945ddd37ea42010d169f5d472b53c0", + "docs/operations.md": "fea5f9e25752c0199839559da5bba0ebca77be7c4906dbce4b7b6278ce5b4dab" + }, + "image": "forgejo.coulomb.social/coulomb/rapp-playwright@sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441", + "provider_unit_tests_passed": 3, + "managed_runtime_accepted": false, + "group": "playwright-consumers", + "member_identities": [] +} diff --git a/docs/playwright-consumer-access.md b/docs/playwright-consumer-access.md new file mode 100644 index 0000000..3e8f0a9 --- /dev/null +++ b/docs/playwright-consumer-access.md @@ -0,0 +1,47 @@ +# Playwright consumer access + +The release 0.1.0 pilot is running in namespace `playwright` on railiance01. +Platform owns its lifecycle and the `playwright-consumers` group binding; +rapp-playwright owns the browser implementation, package Role and client. + +The initial membership policy includes all identified platform humans and coding +agents. The group binding is installed and tested. Mapping authenticated platform +identities into that Kubernetes group is still open in RPF-WP-0056-T03; a KeyCape +group alone would not establish Kubernetes access. No administrator kubeconfig +is distributed and no broad `system:authenticated` binding is installed. + +Once the caller has the supported group identity and API route: + +```sh +kubectl -n playwright port-forward deployment/playwright 18081:8080 --address 127.0.0.1 +``` + +In another terminal, from the existing rapp-playwright checkout: + +```sh +python3 client/rapp_playwright.py preflight +python3 client/rapp_playwright.py trial --out .local/first-managed-trial +``` + +Use a new output directory each time. Keep `run.json` private: it contains the +run capability. Download retained receipts/screenshots before the one-hour +artifact expiry. Current targets are the bundled fixture and public OnCo home +page; additional target admission remains separate from group membership. + +Platform can recheck the native permission boundary without changing grants: + +```sh +python3 scripts/verify_playwright_access.py --output /tmp/playwright-rbac.json +``` + +The verifier uses administrator impersonation solely for authorization checks. +Its success is not consumer-login proof. It checks the cluster UID, exact Role +and group binding, allowed discovery/forwarding and denied exec, Secrets, +workload writes, foreign scope and nonmembers. + +October 13 evidence: [RBAC](evidence/2026-10-13-playwright-rbac.json), +[runtime and restart](evidence/2026-10-13-playwright-managed/runtime.json), +[responsive keyboard trial](evidence/2026-10-13-playwright-managed/receipt.json). +Native operator-driven trials passed before and after a same-image restart; +completed screenshots survived with identical hashes. Actual consumer login, +abrupt-interruption recovery and cross-version rollback remain unproven. diff --git a/helm/playwright-consumer-access.yaml b/helm/playwright-consumer-access.yaml new file mode 100644 index 0000000..fbadbf5 --- /dev/null +++ b/helm/playwright-consumer-access.yaml @@ -0,0 +1,18 @@ +# Group membership and authentication are managed by the identity/cluster owners. +# This binding does not issue credentials or make anyone a member. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: playwright-consumers + namespace: playwright + labels: + app.kubernetes.io/part-of: rapp-playwright + app.kubernetes.io/managed-by: railiance-platform +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: playwright-consumer +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: playwright-consumers diff --git a/intakes/RPF-IN-0012-task-execution-and-wrapped-delivery-review.md b/intakes/RPF-IN-0012-task-execution-and-wrapped-delivery-review.md new file mode 100644 index 0000000..04f2772 --- /dev/null +++ b/intakes/RPF-IN-0012-task-execution-and-wrapped-delivery-review.md @@ -0,0 +1,38 @@ +--- +id: RPF-IN-0012 +kind: intake +type: intake +title: "Return independent task-execution contract and wrapped-delivery owner review" +repo: railiance-platform +status: open +owner: railiance-platform +requested_by: secrets-engine +origin: handoff +origin_ref: SECRETS-WP-0013-T01 +priority: high +created: "2026-10-11" +updated: "2026-10-11" +--- + +Review the proposed SECRETS-EXEC-20261011-v1 metadata-only mediated execution +contract and return the exact Platform owner workplan/task binding and reviewed +revision. Identity/approval/PDP owners still owe independent returns. This intake +records no foreign owner assent and requests no credential value or native action. + +D3 P3=b separately selects response-wrapped issuer delivery. The existing issuer +primitive is hardened in Secrets Engine: native KV-v2/AppRole creation path and +TTL/accessor are verified, a fresh private recipient file is exclusively reserved +and fsynced before mint, and uncertain attempts cannot silently overwrite/remint. +Disposable local OpenBao proves a first unwrap and specific consumed-token denial; +it does not prove estate custody or delivery. Review the exact actor/path/audience, +deadline, authorization/consume, destination, audit, replay/crash reconciliation and +rollback requirements for a separate delivery operation. Metadata execute-ccr +explicitly excludes wrap/KV-value delivery and cannot lend its grant to this lane. + +Source: secrets-engine/docs/task-scoped-openbao-execution-contract.md and +src/secrets_engine/{openbao,wrap}.py; tests/test_wrapped_issuer_native.py is an +isolated test, not a production proof. The already D2-approved repository issuer +has its own prepared bounded command and attended Platform login requirement; +no second repository scope approval is requested and its waiting task is not +reopened by this source review. Owner-prepared execution must precede any new +attended item. Genuinely expanded scope belongs in the single daily red batch. diff --git a/scripts/verify_playwright_access.py b/scripts/verify_playwright_access.py new file mode 100644 index 0000000..eb5360a --- /dev/null +++ b/scripts/verify_playwright_access.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Read-only native RBAC checks; impersonation is not consumer login proof.""" +import argparse +from datetime import datetime, timezone +import json +from pathlib import Path +import shlex +import subprocess + + +CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' +GROUP = 'playwright-consumers' +ROLE = 'playwright-consumer' + + +def verify(target): + def kubectl(args, body=None): + result = subprocess.run( + ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', target, + shlex.join(['kubectl', '--request-timeout=10s', *args])], + input=json.dumps(body) if body is not None else None, + capture_output=True, text=True, timeout=20, check=True) + return json.loads(result.stdout) + + uid = kubectl(['get', 'ns', 'kube-system', '-o', 'json'])['metadata']['uid'] + if uid != CLUSTER_UID: + raise RuntimeError('wrong cluster') + binding = kubectl(['get', 'rolebinding', GROUP, '-n', 'playwright', '-o', 'json']) + role = kubectl(['get', 'role', ROLE, '-n', 'playwright', '-o', 'json']) + if binding['subjects'] != [{'apiGroup': 'rbac.authorization.k8s.io', + 'kind': 'Group', 'name': GROUP}]: + raise RuntimeError('consumer binding subjects differ') + if binding['roleRef'] != {'apiGroup': 'rbac.authorization.k8s.io', + 'kind': 'Role', 'name': ROLE}: + raise RuntimeError('consumer binding role differs') + expected = [ + {'apiGroups': [''], 'resources': ['pods'], 'verbs': ['get', 'list']}, + {'apiGroups': [''], 'resources': ['pods/portforward'], 'verbs': ['create']}, + {'apiGroups': ['apps'], 'resources': ['deployments'], + 'resourceNames': ['playwright'], 'verbs': ['get']}, + ] + if role['rules'] != expected: + raise RuntimeError('consumer role rules differ') + cases = [ + (True, 'get', 'pods', '', '', 'playwright', '', True), + (True, 'list', 'pods', '', '', 'playwright', '', True), + (True, 'create', 'pods', 'portforward', '', 'playwright', '', True), + (True, 'get', 'deployments', '', 'apps', 'playwright', 'playwright', True), + (True, 'get', 'deployments', '', 'apps', 'playwright', 'other', False), + (True, 'get', 'secrets', '', '', 'playwright', '', False), + (True, 'create', 'pods', 'exec', '', 'playwright', '', False), + (True, 'create', 'pods', '', '', 'playwright', '', False), + (True, 'patch', 'deployments', '', 'apps', 'playwright', 'playwright', False), + (True, 'create', 'pods', 'portforward', '', 'default', '', False), + (True, 'create', 'rolebindings', '', 'rbac.authorization.k8s.io', 'playwright', '', False), + (False, 'list', 'pods', '', '', 'playwright', '', False), + (False, 'create', 'pods', 'portforward', '', 'playwright', '', False), + ] + checks = [] + for member, verb, resource, subresource, group, namespace, name, expected_allowed in cases: + attributes = dict(verb=verb, resource=resource, subresource=subresource, + group=group, namespace=namespace, name=name) + args = ['--as=rpf-playwright-rbac-probe', '--as-group=system:authenticated'] + if member: + args.append('--as-group=' + GROUP) + # Raw review endpoint avoids kubectl's unrelated CRD-discovery permission. + review = kubectl([*args, 'create', '--raw', + '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', + '-f', '-'], { + 'apiVersion': 'authorization.k8s.io/v1', 'kind': 'SelfSubjectAccessReview', + 'spec': {'resourceAttributes': attributes}}) + status = review['status'] + if status.get('evaluationError') or type(status.get('allowed')) is not bool: + raise RuntimeError('authorization review incomplete') + checks.append(dict(member=member, attributes=attributes, + expected=expected_allowed, allowed=status['allowed'], + passed=status['allowed'] == expected_allowed)) + return dict(schema='platform-playwright-rbac/v1', + observed_at=datetime.now(timezone.utc).isoformat(), cluster_uid=uid, + role_uid=role['metadata']['uid'], binding_uid=binding['metadata']['uid'], + group=GROUP, checks=checks, passed=all(c['passed'] for c in checks), + actual_consumer_authentication_proven=False) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--target', default='railiance01', choices=['railiance01']) + parser.add_argument('--output', required=True, type=Path) + args = parser.parse_args() + result = verify(args.target) + args.output.write_text(json.dumps(result, indent=2) + '\n') + print(json.dumps({'passed': result['passed'], 'checks': len(result['checks']), + 'evidence': str(args.output)})) + raise SystemExit(0 if result['passed'] else 1) diff --git a/workplans/README.md b/workplans/README.md index d582987..407962f 100644 --- a/workplans/README.md +++ b/workplans/README.md @@ -1,25 +1,27 @@ # Current platform work -Reviewed September 27, 2026. Eight workplans remain blocked with 23 waiting -tasks. No active, ready or proposed source workplan remains. Completed work -retains its IDs and managed UUIDs, including plans under `archived/`. +Reviewed October 13, 2026 against source workplans and the live inbox. Source +files remain authoritative; the generated brief contains historical aliases. +Completed plans retain their IDs and managed UUIDs, including archived plans. -| Workplan | Remaining tasks | Next dependency | +| Workplan | Open tasks | Next work or dependency | | --- | --- | --- | -| [RPF-WP-0015](RPF-WP-0015-audit-core-custody-and-recovery-coordination.md) | T02/T03 | Fresh recovery windows, synthetic sender/abort owner and snapshot/quorum/access evidence | -| [RPF-WP-0027](RPF-WP-0027-keycape-live-secret-exposure-recovery.md) | T03/T06 | Complete historical resolver custody/routing contract; T05 closed from September 23 operator disposition and owner receipt | -| [RPF-WP-0035](RPF-WP-0035-credential-lane-implementation.md) | T02/T03/T06/T07/T08 | Exact service registration/custody, human memo acceptance, reader/disablement returns, governed npm migration and formerly-valid audit-bearer revocation proof | -| [RPF-WP-0036](RPF-WP-0036-platform-service-assurance.md) | T03/T04/T06 | Current recovery/cadence/custody evidence, Q2 production delivery and owner/projection acceptance; local archive adapter implemented | -| [RPF-WP-0038](RPF-WP-0038-forgejo-scaleway-primary-coverage.md) | T04 | Durable scheduled caller, canonical verified inventory and quota/retention gates; September 15 cutover/expiration hold persists | -| [RPF-WP-0043](RPF-WP-0043-policy-nexus-argocd-onboarding.md) | T02/T03/T04 | Cross-owner chart/values agreement, committed values and source access, zero-diff proof and adoption | -| [RPF-WP-0044](RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md) | T03–T08 | Per-app promotion gates, issue-core credentials/ownership, target-revenue hook acceptance, ESO adoption/observation and readable old-controller inventory | -| [RPF-WP-0048](RPF-WP-0048-activity-core-gitops-adoption.md) | T02 | Healthy observation through September 28 at 16:06:22 Berlin plus bounded authenticated release broker/admission and rollback proof | +| [RPF-WP-0015](RPF-WP-0015-audit-core-custody-and-recovery-coordination.md) (active) | T02, T03 | Recovery windows, sender/abort owner and snapshot/quorum/access evidence | +| [RPF-WP-0027](RPF-WP-0027-keycape-live-secret-exposure-recovery.md) (blocked) | T03, T06 | Resolver and custody owner contracts | +| [RPF-WP-0035](RPF-WP-0035-credential-lane-implementation.md) (blocked) | T02, T03, T07, T08, T10, T12 | Execution integration and owner acceptance; verified-source compiler correction tested October 13 | +| [RPF-WP-0036](RPF-WP-0036-platform-service-assurance.md) (blocked) | T03, T04, T06 | Bounded scheduler, telemetry receiving contract and ownership returns | +| [RPF-WP-0038](RPF-WP-0038-forgejo-scaleway-primary-coverage.md) (blocked) | T04 | Durable caller, verified inventory and quota/retention; expiration/cutover hold persists | +| [RPF-WP-0043](RPF-WP-0043-policy-nexus-argocd-onboarding.md) (blocked) | T03, T04 | Owner values/render diff, then root automation before adoption | +| [RPF-WP-0044](RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md) (blocked) | T04, T06, T07 | Attended issue-core issuer login, ordered ESO adoption and root automation | +| [RPF-WP-0048](RPF-WP-0048-activity-core-gitops-adoption.md) (blocked) | T02 | Activity-core promotion evidence | +| [RPF-WP-0053](RPF-WP-0053-coulomb-paired-backup-admission.md) (proposed) | T01, T02, T03, T04, T05 | Paired archive execution/custody, retrieval/recovery and monitoring admission | +| [RPF-WP-0055](RPF-WP-0055-keycape-client-secret-rotation-residual.md) (backlog) | T01 | Unpromoted rotation residual; do not implement without demand/risk promotion | +| [RPF-WP-0056](RPF-WP-0056-playwright-consumer-admission.md) (blocked) | T03 | Dedicated Playwright consumer group, managed pilot and authenticated identity route | -Latest [review and evidence](../history/2026-09-27-loose-end-review.md). -No new task or workplan was created. Existing completed credential reviews, -delivery, retention projection and rotations are not reopened by these waits. +October 13 review corrected WP-0038-T04's stale `todo` state to the documented +`wait`. RPF-IN-0012 remains an open independent execution/wrapped-delivery review; +the compiler fix does not close it. Playwright publication is resolved; its new +Platform workplan records the user-requested group and actual activation evidence. -Source files are authoritative. The generated brief still contains retired -aliases and stale statuses; do not recreate work from it. Routine synchronization -uses the exact-commit Repo Manager receipt described in AGENTS.md. Legacy alias -repair remains the scoped owner dependency in WP-0036-T06. +Routine synchronization uses the exact-commit Repo Manager receipt described in +AGENTS.md. Legacy alias repair remains the scoped WP-0036-T06 dependency. diff --git a/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md b/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md index e352ac5..d8a1baa 100644 --- a/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md +++ b/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md @@ -8,7 +8,7 @@ status: blocked flavor: implementation owner: codex created: "2026-09-06" -updated: "2026-10-03" +updated: "2026-10-13" state_hub_workstream_id: "7beec1a7-aa82-5a36-9a66-6b60008a2455" --- @@ -72,7 +72,8 @@ removed. Evidence: `docs/evidence/forgejo-scaleway-restore-2026-09-06.json`. ```task id: RPF-WP-0038-T04 -status: todo +status: wait +blocking_reason: "Durable scheduled caller, verified inventory and quota/retention gates remain open; September 15 prohibits expiration and scheduled secondary cutover." priority: high state_hub_task_id: "a4807df0-ec96-58f1-9cbd-42b1dcde0d6f" ``` diff --git a/workplans/RPF-WP-0056-playwright-consumer-admission.md b/workplans/RPF-WP-0056-playwright-consumer-admission.md new file mode 100644 index 0000000..437a8fd --- /dev/null +++ b/workplans/RPF-WP-0056-playwright-consumer-admission.md @@ -0,0 +1,122 @@ +--- +id: RPF-WP-0056 +type: workplan +title: "Admit the Playwright pilot and establish scoped consumer group access" +domain: financials +repo: railiance-platform +status: blocked +owner: codex +topic_slug: railiance +created: "2026-10-13" +updated: "2026-10-13" +--- + +Incoming owner request: State Hub c82d75f3-3191-410d-a8c8-fd647bf7f3ef, +superseding publication return a282c300-eaef-4cd6-99b0-f4fb255203b3; +consumer RAPP-PLAYWRIGHT-WP-0004-T01. The user selected establishing a group +in this session. Platform owns lifecycle admission; implementation remains in +rapp-playwright. Publication is complete, and must not remain a blocker. + +## Review the immutable package and pilot boundary + +```task +id: RPF-WP-0056-T01 +status: done +priority: high +``` + +Reviewed rapp-playwright revision 70268154c06cf78c5d9e1f4eb900213ffd436d1b: +declaration, release, workload, admission and operations contract. The published +image is sha256:8d6fd5a8172cf906bb503642ed620761945c5138b86f6396c36f65cbe6def441. +Three provider contract/network/proxy tests pass independently in this session. +One Recreate replica requests 100m CPU/256Mi memory, limits 1 CPU/1Gi, and uses +a 2Gi transient artifact PVC with one-hour retention. Non-root, read-only root, +no service-account token, dropped capabilities, loopback listener, denied ingress +and public HTTPS/DNS egress match the proposed trusted-target pilot. +Chromium's OS sandbox is disabled; this is no hostile-page or cross-tenant +isolation acceptance. Initial targets remain the bundled fixture and public +https://onco.cc; no private preview, source import or data-license grant follows. + +## Establish and verify the consumer group RBAC boundary + +```task +id: RPF-WP-0056-T02 +status: done +priority: high +``` + +User requested a group; use exact Kubernetes group `playwright-consumers`. +Platform binding: helm/playwright-consumer-access.yaml, referring to the package's +existing namespace Role `playwright-consumer`. Bootstrap only its reviewed +Namespace and Role, then bind the group. No cluster-wide role, Secret access, +pod execution, workload mutation, credential issuance or group membership grant. +Verify live allowed discovery/port-forward and denied execution/Secrets/foreign +namespace/workload changes, plus nonmember denial. Impersonated authorization +checks prove RBAC only, not actual consumer authentication or connectivity. + +## Connect platform identities and prove managed consumer operation + +```task +id: RPF-WP-0056-T03 +status: wait +priority: high +blocked_on: message-from:railiance-cluster +blocking_reason: "User admitted all identified platform humans and coding agents; their supported Kubernetes authentication/group-mapping route remains unimplemented." +``` + +The session's initial readback found only built-in/system user/group bindings; +the cluster documentation describes an administrator kubeconfig. A group subject +in RBAC does not create an identity-provider group or convey membership. The user admitted all identified platform humans and coding agents; establish +their existing-owner authentication and group-mapping route, +including expiration/revocation. Do not distribute the admin kubeconfig or infer +that a KeyCape group is already understood by the Kubernetes API. + +The immutable pilot is now deployed after fresh capacity, server dry-run and +diff: 1/1 Ready, 2Gi PVC Bound and matching imageID. A real group-identity trial +is still required; administrator impersonation is recorded separately. +Retain screenshots/keyboard receipts, wrong-token/assertion/target negatives, +cancellation and restart/interrupted/completed-artifact recovery. One available +release supports same-image recovery only; cross-version rollback stays unproven. +Keep RAPP-PLAYWRIGHT-WP-0004-T01 open until its owner accepts those receipts. + + +## October 13 native implementation + +ADR-0006 records the group decision. Namespace, package Role and Platform binding +are applied on the pinned railiance01 cluster. Thirteen native authorization +checks pass: intended discovery/forwarding is allowed; Secrets, exec, writes, +foreign namespaces, foreign deployment and nonmember access are denied. The +initial verifier's kubectl schema discovery needed unrelated CRD access; using +the native SelfSubjectAccessReview endpoint corrected the verifier without any +permission expansion. Re-run scripts/verify_playwright_access.py with --output. + +Fresh capacity before deployment: 3460m/4000m CPU requested, 9472Mi memory +requested and 126Gi free storage. Adding 100m/256Mi plus a 2Gi PVC fits that +scheduling headroom; aggregate CPU/memory limits were already overcommitted. +The declared single-browser limits remain in force. Full package server dry-run +and diff passed and the published immutable image reached Ready. + +Initial fixture trial through administrator-impersonated group port-forward +passed desktop/narrow keyboard and screenshot checks. This demonstrates native +RBAC and browser operation; it does not turn the administrative SSH identity +into a generally available consumer authentication route. T03 retains that gap. + +## Activate the bounded pilot and verify operator-driven recovery + +```task +id: RPF-WP-0056-T04 +status: done +priority: high +``` + +The published package is running. Thirteen RBAC checks and six native negative/ +health checks pass, including wrong receipt/artifact token, deliberate assertion +failure, bounded cancellation and private-target refusal. Actual Deployment +restart reached Ready with a new pod UID, the same immutable image and the same +PVC; all four completed artifacts were retrieved and their hashes matched. +Evidence: docs/evidence/2026-10-13-playwright-managed/ and +docs/evidence/2026-10-13-playwright-rbac.json. This is same-image recovery, not +cross-version rollback or abrupt-interruption proof. T03 remains open for actual +consumer identity/group mapping and the remaining package-owner acceptance. + +Group decision mirrored to State Hub d2426028-0b04-4ea2-b199-d7f9e19e38e6.