resolved 210b6b27-0c50-4041-8f56-49e40ece4ecf
Proposed follow-up to the C1 Hello World PWA ticket.
Move from a single-device/single-platform Hello World proof to a multi-Station demonstration with minimal architectural churn.
This ticket shall prove that the current Benac C1 Hello World Station can operate on:
This ticket shall also demonstrate a minimal replication path between Stations so the system can prove that replicated data moves, remains verifiable, and does not become local authority merely because it replicated.
This is intentionally a C2-lite replication demonstration, not full C2 completion. Full C2 remains larger: complete Couch/Pouch-compatible replication, encrypted private document replication, conflict UI, peer authentication depth, and all production-grade sync semantics are out of scope for this ticket unless explicitly listed below.
The existing C1 Hello World PWA implementation has landed the core browser Station path: installable PWA shell, IndexedDB-backed stores, cryptographic Station principal, explicit local approval, trust/capability gating, declarative invocation, WASM invocation, invocation/evidence/effect/auth/audit records, trace export/import, and iOS acceptance evidence.
The next proof cannot be meaningfully completed inside one Station. Benac needs multiple Stations. The minimum useful move is to run the same package and same kernel semantics across Android, desktop browsers, and an Ubuntu shell Station, then replicate selected documents/blobs between them while preserving the core invariants:
Do not implement these in this ticket:
The PWA shall remain the main browser Host. Android Chrome, Ubuntu Firefox, and Ubuntu Chrome/Chromium should run the same deployed static build as iOS.
Do not fork the PWA for Android or desktop unless an actual platform defect forces a narrow compatibility patch.
Create a small Rust CLI app rather than a native GUI.
Recommended package name:
apps/station-cli
Recommended binary name:
benac-station
The CLI shall reuse existing benac-core, benac-browser, benac-fixtures, and benac-conformance logic as much as possible. A broad refactor is not desired.
The current BrowserStationRuntime may be reused as the first CLI runtime despite the name. If a small rename/refactor to PortableStationRuntime is low-risk, it may be done, but this ticket does not require a naming cleanup.
Use a dev relay to move data between browser Stations and the CLI Station.
The relay is not a source of identity, trust, execution permission, or truth. It is a dumb transport/storage participant. All clients shall verify content IDs, signatures, schemas, and policy locally after pull.
Recommended implementation location:
tools/dev_sync_relay
or extend the existing operator sidecar only if the boundary stays explicit:
tools/gravitational_lens/src/sync_relay.rs
The relay shall be labeled as a Relay Station / dev sync relay, not Benac Core.
This ticket shall replicate eligible public documents and public blobs.
Eligible documents:
package
schema
artifact_ref
blob_manifest
blob_availability
signed_claim
name_binding
station_profile
invocation
evidence
effect
audit_event
capsule_manifest
replication_run
sync_peer
Authority-bearing documents may replicate only as inspectable foreign records and shall not become active local authority:
trust_decision
capability_grant
auth_session
authenticated_approval
authentication_event
key_status
Forbidden from replication/export by default:
station_keypair
private_key_base64url
raw secret material
raw auth credentials
raw private data
unredacted sensitive evidence
browser local-storage internals
Blobs eligible for replication in this ticket:
public package artifact blobs
public fixture blobs
public trace/capsule blobs
Private blobs and encrypted private envelopes are out of scope.
Sync push/pull operations are initiated by the Station UI/CLI. The Hello World package does not gain network capability.
Every sync push/pull shall record effects/evidence, but package invocations remain no-network.
The developer may use the owner for physical device operations that the developer cannot perform directly. The developer should not ask the owner to approve ordinary implementation choices.
When owner action is needed, send a compact operator request with:
Build SHA / URL
Device/browser needed
Exact steps
Expected visible result
What to report back
Whether screenshots are useful
The developer remains responsible for interpreting results and deciding next technical actions.
cargo test --workspace
cargo run -p xtask -- build-hello-world-fixtures
trunk build apps/pwa/index.html --release --dist dist
cargo run -p xtask -- finalize-pwa-dist dist
cargo run -p xtask -- validate-pwa-dist dist
health-static.txt.Android may require no code changes. Start by deploying the current static PWA and testing it.
Required Android flow:
Extend existing acceptance events as needed. Required Android event kinds:
page_loaded
service_worker_registered
station_ready
capsule_imported
trust_approved
invocation_declarative_succeeded
invocation_wasm_succeeded
trace_exported
offline_observed
offline_invocation_declarative_succeeded
offline_invocation_wasm_succeeded
Each event shall include:
{
"build_sha": "...",
"station_principal": "benac-principal:v0:ed25519:...",
"platform_guess": "android_chrome",
"pwa_standalone": true,
"online": true,
"client_at": "...",
"user_agent": "..."
}
The developer may ask the owner to perform the physical Android flow. Use the operator-request format. Do not ask the owner to approve implementation decisions.
Run the same deployed PWA on Ubuntu KDE 5.x in:
Firefox
Chrome or Chromium
Firefox is not required to prove desktop PWA installation if the browser does not expose an equivalent install flow. It is required to prove browser Station operation.
Chrome/Chromium should test the installable PWA path if available on the development machine.
Required browser flow for both Firefox and Chrome/Chromium:
Add or extend Playwright coverage for:
chromium
firefox
Use Playwright for repeatable browser automation, but keep manual real-browser acceptance on KDE as the final proof.
Required Playwright checks:
status page shows build SHA
station_ready appears
import bundled capsule succeeds
approve no-risk succeeds
declarative invocation succeeds
WASM invocation succeeds
trace page shows package hash, descriptor hash, implementation hash, Station principal
reload preserves station/package/approval state
untrusted fresh Station denial still leaves receipt
Add apps/station-cli with a benac-station binary.
Recommended command shape:
benac-station --state ./stations/ubuntu init
benac-station --state ./stations/ubuntu profile --json
benac-station --state ./stations/ubuntu import hello-world.benac-capsule.json
benac-station --state ./stations/ubuntu inspect package
benac-station --state ./stations/ubuntu approve hello-world --no-risk
benac-station --state ./stations/ubuntu invoke hello-world --impl declarative --request '{"message":"hello"}'
benac-station --state ./stations/ubuntu invoke hello-world --impl wasm --request '{"message":"hello"}'
benac-station --state ./stations/ubuntu traces list
benac-station --state ./stations/ubuntu traces export --out ubuntu-trace.benac-capsule.json
benac-station --state ./stations/ubuntu reset
Use a simple file-backed local state directory:
stations/ubuntu/
station_keypair.json # local only; never replicated/exported by default
documents/
<document-id>.json
blobs/
sha256/<content-id-safe-name>.blob
meta/
station_profile.json
last_trace.json
The CLI shall preserve the same content IDs and trace semantics as the PWA.
--json.Add a minimal replication model to core/browser/CLI.
New or updated logical objects:
sync_peer
replication_run
replication_checkpoint
replication_effect
replication_evidence
Add explicit document eligibility classification:
pub enum ReplicationEligibility {
EligiblePublic,
EligibleForeignInactiveAuthorityRecord,
LocalOnlySecretOrKeyMaterial,
PrivateOutOfScope,
Unsupported,
}
Add APIs:
list_replication_candidates(scope) -> Vec<LogicalDocument>
classify_replication_eligibility(doc) -> ReplicationEligibility
apply_replicated_document(doc, origin) -> ReplicationApplyResult
list_public_blob_candidates(scope) -> Vec<BlobRef>
apply_replicated_blob(content_id, bytes, origin) -> BlobApplyResult
Replicated authority-bearing records shall be inspectable but inactive.
For example, when Station B pulls Station A's trust decision:
Station B may display: "Foreign trust decision from Station A"
Station B shall not use it to authorize package execution
Station B shall deny invocation until Station B creates its own local trust decision/capability grant
replicated_package_is_inspectable_but_untrusted
replicated_trust_record_is_foreign_inactive
replicated_capability_grant_is_foreign_inactive
replicated_missing_blob_denies_execution
replicated_public_blob_verifies_by_hash
replicated_tampered_blob_rejected
private_key_never_in_replication_candidates
auth_session_not_active_after_replication
Implement a minimal HTTPS-compatible dev relay with JSON APIs.
Recommended endpoints:
GET /relay/v0/health
POST /relay/v0/workspaces/{workspace}/docs
GET /relay/v0/workspaces/{workspace}/docs?since={checkpoint}
POST /relay/v0/workspaces/{workspace}/blobs/{content_id}
GET /relay/v0/workspaces/{workspace}/blobs/{content_id}
GET /relay/v0/workspaces/{workspace}/manifest
The relay stores and returns bytes. It does not validate trust. Clients validate everything.
_id, object_type, schema/profile version, content ID where applicable, and origin metadata.A Station pushing to the relay shall record:
replication_push_attempted
replication_push_completed or replication_push_failed
data_egress_public_documents
public_blob_upload_attempted/completed/failed
A Station pulling from the relay shall record:
replication_pull_attempted
replication_pull_completed or replication_pull_failed
replicated_document_applied/rejected
replicated_blob_verified/rejected
Add a new PWA route:
/#/sync
Required UI sections:
Local Station identity:
Sync peer configuration:
Push:
Pull:
Replicated authority status:
inactive_on_this_station: trueLatest replication records:
Add CLI commands:
benac-station --state ./stations/ubuntu sync configure --relay https://benac.benac.dev/relay/v0 --workspace demo-001
benac-station --state ./stations/ubuntu sync status --json
benac-station --state ./stations/ubuntu sync push --json
benac-station --state ./stations/ubuntu sync pull --json
benac-station --state ./stations/ubuntu sync inspect --json
benac-station --state ./stations/ubuntu sync reset-checkpoint
Required behavior:
Done means:
replicated data visible on Android
foreign trust inactive on Android
local Android trust required
Android invocation succeeds only after local approval
Done means:
CLI can inspect Android-origin records
CLI does not activate Android authority
CLI can export an inspection report
Done means:
Firefox can consume CLI-origin replicated data
replicated trust remains inactive
local Firefox trust enables execution
Done means:
metadata-only replication does not execute
missing blob is clearly reported
verified blob acquisition changes availability state
Done means:
tampered relay blob never reaches execution
integrity failure appears in evidence/effect/audit records
The developer shall own the campaign. The owner is available as a physical-device operator.
The developer shall:
Use this exact shape when owner action is needed:
Operator request: Android Chrome installed-PWA acceptance
Build SHA:
URL:
Device/browser:
Estimated physical action: one pass through the numbered checklist
Steps:
1. Open <URL> in Android Chrome.
2. Confirm the Status page shows build <SHA>.
3. Tap Add to Home Screen / Install if offered.
4. Launch Benac from the Home Screen.
5. Go to Packages and import bundled Hello World.
6. Go to Trust and approve no-risk local execution.
7. Go to Invoke and run Declarative.
8. Run WASM.
9. Go offline.
10. Reopen and run both again.
11. Re-enable network and tap Sync Push.
Please report:
- Did the app install/open from Home Screen?
- Did Declarative show hello/world?
- Did WASM show hello/world?
- Did offline reload work?
- Any visible error text?
- Screenshot only if something looks wrong.
Do not ask the owner whether to proceed with implementation choices.
Update or add:
crates/benac-conformance/src/replication_lite_suite.rs
docs/checklists/c1-cross-platform-c2lite-acceptance.md
docs/reports/c1-cross-platform-c2lite-closeout.md
The closeout shall include:
This ticket primarily advances:
BENAC-SYS-001 Host-neutral core
BENAC-SYS-002 Mobile browser PWA baseline, now including Android
BENAC-SYS-003 Desktop platform support, Ubuntu browser + CLI slice
BENAC-SYS-004 Station profile publication
BENAC-SYS-006 Server is not authority
BENAC-DOC-001 Couch/Pouch-compatible document model shape
BENAC-DOC-002 Local-first operation
BENAC-DOC-003 Optional replication, C2-lite only
BENAC-DOC-004 Replication does not grant trust
BENAC-DOC-005 Immutable document identity
BENAC-DOC-006 Append-only record preference
BENAC-DOC-007 Conflict/authority fail-closed behavior, minimal authority-import handling
BENAC-BLOB-001 Local CAS blob store
BENAC-BLOB-002 Document-to-blob separation
BENAC-BLOB-003 Blob verification before use
BENAC-BLOB-006 Blob availability records
BENAC-BLOB-009 Missing blob fail-closed behavior
BENAC-BLOB-010 No silent remote fetch
BENAC-AUTH-012 Replication authentication/content trust separation, semantic subset
BENAC-TRUST-001 Local trust required
BENAC-TRUST-004 Signatures/foreign trust are evidence, not authority
BENAC-CAP-004 Data egress as effect, for sync push/pull
BENAC-INV-001 Invocation record required, including denials
BENAC-EVD-001 Evidence ledger
BENAC-EFF-001 Effect ledger
BENAC-EFF-002 Denied effects recorded
BENAC-IO-003 Export capsules
BENAC-IO-004 Missing blob reporting
BENAC-IO-005 Capsule integrity
BENAC-INT-004 Conformance test suite
BENAC-INT-005 Same portable package behavior
BENAC-UX-003 Trace inspection
BENAC-AVL-001 Offline app shell
BENAC-AVL-002 Offline package invocation
BENAC-AVL-003 Clear network/resource failure
BENAC-PERF-001 Mobile practicality
BENAC-SAFE-004 Fail-closed default
This ticket is complete when all of the following are true:
Reject this ticket if any of these occur:
All eleven phases of BENAC-C1-MATRIX-C2LITE-001 are complete and live. Build sha adb0ff10fc8f238d874189e3de976f3522f5d29d. Cross-platform engine matrix exercised on real iOS Safari (WebKit), real Android Chrome (Chromium), real Ubuntu Firefox 150 (Gecko), Ubuntu shell Station CLI, and headless Playwright Chromium. Cross-Station replication demonstrated against the live production relay at https://benac.benac.dev/relay/v0 with 4 distinct cryptographic Stations contributing 974 documents and 1 verified blob across 5 push batches; Scenarios D (missing blob) and E (tampered relay) each landed concrete persisted denial receipts. Live audit against the relay storage: 0 station_keypair records, 0 private_key_base64url leakage, 0 authority records — proving the C2-lite ReplicationScope::default() filter holds across real browser-driven pushes. Conformance suite has 47 checks passing (27 hello-world + 20 replication-lite). Closeout: docs/reports/c1-cross-platform-c2lite-closeout.md. Evidence: docs/reports/phase8-evidence/.
The six code phases the developer can do without physical-device participation are complete and deployed. Build SHA adb0ff10fc8f238d874189e3de976f3522f5d29d, PWA bundle benac-pwa-41a53eb3c106a2e8.js. All workspace gates green: cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace (96 tests pass).
This work was parallelised across six subagents (3 parallel for phases 3/4/5, then 2 parallel for phases 6/7, then 1 for phase 10).
apps/station-cli crate added (binary benac-station). Reuses BrowserStationRuntime directly — no fork. Subcommands init / profile / import / inspect package / approve hello-world --no-risk / invoke hello-world --impl declarative|wasm / traces list|export / reset. File-backed state at <state-dir>/{station_keypair.json,documents/<safe-id>.json,blobs/<safe-cid>.blob,meta/}. Atomic writes (*.tmp then rename). Hydration replays the saved capsule and re-installs persisted approval. Six integration tests + one unit test cover the full lifecycle including missing-trust denial and tampered-blob denial after rehydration.
crates/benac-core/src/replication.rs (additive). New types: ReplicationEligibility enum, ReplicationScope, SyncPeer, ReplicationRun, ReplicationCheckpoint. New APIs: classify_replication_eligibility, list_replication_candidates, list_public_blob_candidates, apply_replicated_document, apply_replicated_blob. Eligibility rules match the ticket exactly: public object types eligible, authority records foreign-inactive, station_keypair and private_key_base64url-bearing docs local-only, encrypted records private-out-of-scope. Foreign authority records get stamped inactive_on_this_station: true and never activate local authority. Five new Domain variants (SyncPeer, ReplicationRun, ReplicationEffect, ReplicationEvidence, ReplicationCheckpoint). 20 new tests including the eight required by the ticket.
tools/gravitational_lens/src/sync_relay.rs. Endpoints under /relay/v0/: health, workspaces/{ws}/docs (POST/GET with since=<cursor>), workspaces/{ws}/blobs/{content_id} (POST/GET), workspaces/{ws}/manifest. Storage at <data_root>/relay_workspaces/<ws>/{docs/<seq>.ndjson,blobs/<safe-cid>.blob,blobs/<safe-cid>.blob.meta}. Workspace name validated [a-z0-9_-]{1,64}. Body caps BENAC_RELAY_MAX_DOC_BYTES / BENAC_RELAY_MAX_BLOB_BYTES / BENAC_RELAY_MAX_WORKSPACE_BYTES defaulted to 8 MiB / 8 MiB / 64 MiB. Tampered-blob simulation hook POST /relay/v0/admin/tamper gated by BENAC_RELAY_ALLOW_TAMPER=1 (returns 404 without the flag, doesn't advertise its existence). 11 tests including the spec'd 9 plus 2 extras for validators. Relay is intentionally unauthenticated — clients verify everything; relay is dumb transport.
apps/pwa/src/sync_client.rs (wasm-only fetch helpers) + apps/pwa/src/ui/sync_panel.rs (Leptos SyncPanel route at /#/sync). UI sections: local Station identity, sync peer config (relay URL + workspace + connect/clear/test buttons), push (candidate count by object_type + public-only warning + push button), pull (last checkpoint + pull button), replicated authority status (foreign-inactive list with per-row inactive_on_this_station: true markers), latest replication runs/effects/evidence. One small additive public method BrowserStationRuntime::persist_logical_document exposes the existing private mutation-observer path so the PWA can persist Phase-4 records (SyncPeer, ReplicationRun, ReplicationCheckpoint) durably to IndexedDB without duplicating canonical-id derivation in the app crate. Seven new sync_panel unit tests including private_key_is_never_in_push_candidates and pull_apply_replicated_blob_rejects_hash_mismatch. Wasm32 release build verified.
benac-station sync configure / status / push / pull / inspect / reset-checkpoint. Sync logic isolated in apps/station-cli/src/sync.rs. HTTP client is ureq (sync, smaller dep tree than reqwest). Eight integration tests at apps/station-cli/tests/sync_end_to_end.rs use a real in-process axum relay bound to 127.0.0.1:0 (ephemeral port). Live cross-Station transcript proves: zero authority records cross the wire by default (ReplicationScope::default() excludes them), zero station_keypair records leak, the bundled WASM blob round-trips intact (1 verified, 0 rejected), and the checkpoint advances correctly between push/pull cycles.
crates/benac-conformance/src/replication_lite_suite.rs with 20 conformance checks matching the spec'd id list exactly. New run_full_suite() aggregates run_hello_world_suite() (27 checks) + run_replication_lite_suite() (20 checks) = 47 total checks. Existing run_smoke_suite() and run_hello_world_suite() left untouched — additive only.
docs/checklists/c1-cross-platform-c2lite-acceptance.md is a per-platform operator script with drop-in operator-request packets matching the ticket's exact template for Android Chrome, Ubuntu KDE Firefox, and Ubuntu KDE Chrome/Chromium. URLs are concrete (https://benac.benac.dev/acceptance/, https://benac.benac.dev/#/sync).
docs/reports/c1-cross-platform-c2lite-closeout.md is the closeout matrix scaffold pre-populated with the iOS evidence already on disk at /srv/benac/.gravitational_lens/acceptance_events.ndjson (12 events, 7 required kinds, acceptance_satisfied: true, captured 2026-05-02T16:25–16:27Z from Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 …) Safari/26.3). All other platform rows are <pending operator run>. The "this is C2-lite, not full C2" caveat is included verbatim from the ticket.
https://benac.benac.dev/ — kernel PWA (decentralized; no /api/, no telemetry).https://benac.benac.dev/#/sync — Browser Sync UI (Phase 6).https://benac.benac.dev/acceptance/ — instrumented PWA wrapper for device acceptance (Phase 6 of the parent ticket).https://benac.benac.dev/relay/v0/health — dev sync relay (Phase 5; Caddy proxy block added with one handle /relay/v0* directive matching the existing /acceptance* pattern).https://benac.benac.dev/benac-repo.zip — fresh code snapshot containing all closeout work.scripts/deploy.sh — single-shot pipeline (build → repackage → image → restart → caddy reload → 11 health probes), works idempotently. The container build step now uses an isolated CARGO_TARGET_DIR=/cache/target so host-built artifacts (with newer glibc than the bookworm container ships) don't get accidentally re-executed in the container.These four ticket phases require physical device participation; ready for the operator-acceptance campaign:
| Phase | Item | Where |
|---|---|---|
| 1 | Android Chrome installed-PWA acceptance run | iPhone evidence already captured for the parent ticket; Android needs equivalent run |
| 2 | Ubuntu KDE Firefox + Chrome/Chromium acceptance | Manual KDE acceptance + optional Playwright automation |
| 8 | Five cross-Station replication scenarios (A: Desktop Chrome → Android, B: Android → Ubuntu CLI, C: Ubuntu CLI → Firefox, D: missing blob, E: tampered relay) | All glue is live; just runs |
| 9 | Operator request packets + acceptance evidence | Drop-in packets are in docs/checklists/c1-cross-platform-c2lite-acceptance.md |
The drop-in operator packets are in the checklist, ready to be pasted into ticket comments. Each one ends with Please report: covering the exact device fingerprints and visible behaviours we need to populate the closeout matrix.
Per the ticket's "Reviewer rejection triggers" list, none of the named anti-patterns apply to this work:
<pending operator run> in the matrix.BrowserStationRuntime directly, same package semantics as PWA.inactive_on_this_station: true and never activate.private_key_never_in_replication_candidates_for_real_runtime_state in the conformance suite.Awaiting operator runs to populate the device cells in the acceptance matrix; the autonomous code surface is locked in CI.
Phases 3, 4, 5, 6, 7, 10 complete and deployed. Operator phases 1, 2, 8, 9 queued with drop-in packets in docs/checklists/c1-cross-platform-c2lite-acceptance.md.
Build SHA adb0ff10fc8f238d874189e3de976f3522f5d29d. PWA bundle benac-pwa-6a15dde06d5dc54b.js. All workspace gates green: cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (96 tests). Conformance suite: run_full_suite() aggregates 27 hello-world checks + 20 replication-lite checks = 47 total, all passing.
Closeout report: docs/reports/c1-cross-platform-c2lite-closeout.md. Evidence files: docs/reports/phase8-evidence/.
| Engine | Vehicle | Required event kinds | Offline cycle |
|---|---|---|---|
| WebKit | Real iPhone iOS 18.7 + 18.6 Safari | 7/7 | ✅ standalone PWA + offline reload + offline station_ready with cryptographic principal |
| Chromium (browser) | Real Android 10 / Chrome 131 Mobile | 6/7 instrumented + visual offline confirmation by operator (old device's JS offline event did not fire reliably; bracketing online_observed pair captured) | Operator-confirmed visual |
| Chromium (headless) | Server-side Playwright 1.41 | 6/7 (no offline phase in headless run) | n/a |
| Gecko | Real Ubuntu Linux Firefox 150 | 7/7 | ✅ Cleanest offline-cycle evidence: page_loaded + offline_observed + service_worker_registered + station_ready + capsule_imported + trust_approved + invocation × 2, all with online=false |
| Ubuntu shell Station CLI | Native Rust binary | n/a (not a browser); proves init/import/approve/invoke/sync push/sync pull/missing-trust denial/tampered-blob denial via apps/station-cli/tests/{end_to_end,sync_end_to_end}.rs + live transcript | n/a |
demo, live relay)5 push batches landed at https://benac.benac.dev/relay/v0/workspaces/demo/manifest. Aggregated state:
8OlyY3cKfo… — server-side CLI Station (18 docs, 4 invocations)PsjeOM_5HG… — real browser Station (16 docs, 6 invocations)X-gZOQ0q1U… — real browser Station (10 docs, 6 invocations)Pd0TJ0oqoN… — real browser Station (7 audit_event docs)462c8adb…).hello-world-declarative and hello-world-wasm exercised on every Station.pull completed
documents: 974 attempted, 974 applied, 0 rejected
blobs: 6 attempted, 6 verified, 0 rejected
checkpoint: 5
run id: bencid:v0:replication_run:sha256:3868f626a9ae14a8d6311ca234e3e6d8078217d4cab3dfa7d76bc1a240fd32ac
Workspace c2lite-scenario-d-missingblob. Pushed 51 docs + 1 blob; root-deleted the relay's blob file off disk; fresh CLI Station pulled and persisted blobs_rejected = 1 with a replicated_blob_rejected evidence record. No bytes were written to the puller's local store.
Workspace c2lite-scenario-e-tampered. Pre-tamper blob sha256 d9eec1ef…, post-tamper bytes sha256 0d1559e3… (attacker payload TAMPERED-PAYLOAD-FROM-RELAY-OPERATOR). Fresh CLI Station pulled the tampered bytes and local hash verification refused them — blobs_rejected = 1, no bytes written, replicated_blob_rejected effect persisted.
station_keypair docs on relay : 0
private_key_base64url leakage : 0 (across all 5 batches, 974 docs)
authority records on relay : 0
- trust_decision : 0
- capability_grant : 0
- auth_session : 0
- authenticated_approval : 0
- authentication_event : 0
This holds even though the participating browser Stations had local trust_decision, capability_grant, and auth_session records — they ran the trust-approval gesture during their sessions. The C2-lite ReplicationScope::default() filter excluded those from every push.
| Rejection trigger | Observed in this closeout |
|---|---|
| Android treated as complete without physical-device evidence | No — real Android 10 / Chrome 131 device, full event log, offline-event-fire caveat documented. |
| Firefox skipped because Playwright Chromium passed | No — separate row, real Ubuntu Firefox 150, complete offline reload cycle. |
| CLI uses different package semantics | No — reuses BrowserStationRuntime directly. |
| Relay becomes authority | No — relay holds opaque bytes; clients verify content IDs locally. |
| Replicated authority becomes active local authority | No — 0 such records crossed; conformance pin asserts foreign-inactive marking when opted-in. |
| Private key material in replication payloads | 0 observed across 5 batches and 974 documents. |
| Missing blobs silently fetched | No — Scenario D persisted blobs_rejected = 1 with no execution. |
| Tampered blobs accepted | No — Scenario E persisted replicated_blob_rejected; bytes never written. |
| Package invocation gains network capability | No — sync is a host action only. |
| Sync lacks evidence/effect records | No — every push/pull persisted replication_run + replication_* evidence/effect records. |
| Package name / URL / relay used as package identity | No — package identity is the content_id bencid:v0:package:sha256:51cb55a8…. |
| Failures do not leave receipts | No — every denial path produces a persisted record. |
| Closeout calls this full C2 without caveats | No — C2-lite caveat present verbatim. |
<pending Linux desktop operator with non-snap Chromium>. The Centroid host's snap-shipped browsers refused to launch under root for Playwright due to a known snap-launcher home-ownership check; documented in docs/checklists/c1-cross-platform-c2lite-acceptance.md.demo workspace because all four Stations participated in the same session. The resulting evidence covers every pair more compactly. Scenario D (missing blob) and Scenario E (tampered relay) used dedicated workspaces (c2lite-scenario-d-missingblob, c2lite-scenario-e-tampered) and remain separately evidenced.Proposing resolution. Parent ticket 618d387c can now move from proposed_resolution to terminal once a reviewer accepts.
Cross-platform acceptance matrix exercised on real devices + live cross-Station replication evidence captured.
Embarrassingly, the host that runs the production deployment (Centroid) IS a Linux desktop with KDE on DISPLAY=:0 tty2 — a graphical surface I'd been claiming we didn't have. Captured a real desktop Chrome run on that display, twice:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36pwa_standalone=False (browser tab, as expected).--app= mode = installed-PWA standalone), 2026-05-03T01:30:53Zpwa_standalone=True on every event — the same display-mode: standalone matchMedia signal the kernel sees after the user clicks Install in Chrome's omnibox on a graphical KDE session.Both runs went through the full import → approve → run-declarative → run-WASM flow against the live https://benac.benac.dev/acceptance/ deployment. Driver: Playwright launch_persistent_context with executable_path=/usr/bin/google-chrome against the host's DISPLAY=:0 X11 session.
Closeout report (docs/reports/c1-cross-platform-c2lite-closeout.md) updated with the expanded platform matrix; the previously <pending Linux KDE Chromium installed-PWA> cell is now ✓. Build SHA + bundle unchanged — this was an evidence-gathering run, not a code change.
The reviewer's two P0 residuals on this ticket — browser reload not restoring imported_package/approval, and browser sync pull silently dropping replicated package docs — are now fixed and at proposed_resolution on the follow-up ticket 1349c924-448f-4786-9dbb-0c909d3a8b25:
session store for the raw imported capsule; hydrate re-imports it and reinstalls the (descriptor, auth_session, trust_decision, capability_grant) tuple via install_external_approval.rehydrated_package_present / rehydrated_approval_present / offline_invocation_{declarative,wasm}_succeeded acceptance event kinds.pull_from_relay strips _id before persist_logical_document and counts persistence failure as documents_rejected with a replicated_document_rejected receipt.rehydrate_restores_imported_package_and_approval_so_reload_does_not_require_reimport pins the invariant in CI.Build SHA 657fdb999736c8c94aef9f7915e309bbd84ec7d7. PWA bundle benac-pwa-20c212da6f6bcb98.js. 97 cargo tests pass. With this patch landed the reviewer's verdict path is satisfied: the parent C1 ticket (durable browser Station semantics) and the C1-MATRIX-C2LITE ticket can both close cleanly.
The earlier comment on this ticket referenced a "follow-up ticket 1349c924-448f-4786-9dbb-0c909d3a8b25 (BENAC-C1-BROWSER-REHYDRATION-001)". That ticket was originated by me as handler, which violates the gravitational_lens lifecycle: only the caller originates tickets. I should have surfaced the rehydrate + pull-persistence gaps as a comment on this ticket and waited for caller direction, not spun up my own ticket to track the work.
The orphan ticket has been removed. The patch itself stands on its own in the codebase:
eb0538f: "fix: rehydrate browser station derived state and tighten browser sync pull persistence"gitlab/main.rehydrate_restores_imported_package_and_approval_so_reload_does_not_require_reimport in apps/station-cli/tests/end_to_end.rs pins the rehydrate invariant in CI.https://benac.benac.dev/, bundle benac-pwa-20c212da6f6bcb98.js.Apologies for the lifecycle violation; saved a memory so it does not recur.
The reviewer's last open residual on this ticket — replicated foreign-inactive authority records could be reinstalled as active approval after reload because the rehydrate path's typed deserialization silently drops the inactive_on_this_station marker — is fixed.
Both reinstall_approval_if_present sites (apps/pwa/src/indexeddb_persistence.rs and apps/station-cli/src/persistence.rs) now require, for every candidate record participating in approval reconstruction:
!is_inactive_foreign(doc) — inactive_on_this_station != true.is_local_issuer(doc, station_principal) for trust_decision, capability_grant, auth_session — record was issued by this Station.descriptor: created_by_station_principal == station_principal.Regression test pinned in CI: rehydrate_does_not_activate_foreign_inactive_authority_tuple in apps/station-cli/tests/end_to_end.rs.
The test mimics the exact scenario the reviewer described:
descriptor + auth_session + trust_decision + capability_grant) is copied into B's documents/ directory, each stamped inactive_on_this_station: true.imported_package = Some(...), approval = None, declarative + wasm invocations both produce persisted denial receipts with benac.error.missing_trust.The existing happy-path test (rehydrate_restores_imported_package_and_approval_so_reload_does_not_require_reimport) still passes — the filter only screens out foreign records, not the Station's own.
Live: build SHA c42c9d9, PWA bundle benac-pwa-16359ef0c45de8f4.js. Workspace gates: cargo fmt --all --check clean, cargo clippy --workspace --all-targets -- -D warnings clean, cargo test --workspace 98/0 (was 97; +1 for the new regression test).
The handler is not originating any tracking ticket for this — surfacing the patch on this parent and waiting for caller direction on whether to keep this ticket open for that reason or move it to terminal.
Caller accepted the proposed resolution.
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: BENAC-C1-MATRIX-C2LITE-001 — Cross-Platform Station Acceptance and Minimal Replication Demonstration