Add public/private room clients and local agent hosting - #85
Merged
Merged
Conversation
# Conflicts: # .github/workflows/release.yml # .github/workflows/rust.yml # AGENTS.md # Cargo.toml # README.md # crates/vhalla-cli/Cargo.toml # crates/vhalla-cli/README.md # crates/vhalla-rooms-node/src/unix.rs # crates/vhalla-rooms-node/src/unix/ingress_tests.rs # crates/vhalla-rooms-node/src/unix/seen_tests.rs # docs/game-replay.md
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
A draft reserved under an enabled public-activity policy becomes unresumable once certified room history advances to a disabling revision: resume rightly refuses under current policy, and the exact pending draft used to stay stranded forever. The new browser recover action mirrors native recover-history. It loads only the existing reservation and author floor (never initializing either), verifies the unchanged unsigned request against its retained enabling revision through the shared historical check, rebinds only the certified evaluation basis through the existing CAS reservation path, re-reads persisted state before signing, and finalizes the identical request. The result is local continuity material: no current posting permission, past admission or delivery is claimed. The fixture now commits a mid-run certified revocation bundle to the shared journal on a trigger file, and the qualification proxy exposes it to the journey. The real-Chromium run reserves a lobby draft under the enabled policy, revokes, syncs the new bundle, refuses current-policy resume twice with the draft preserved, then recovers the unchanged request under its retained enabling revision. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Durable fork quarantine was implemented and tested in the kernel but the browser DOM journey never exercised it: fabricating a divergent control requires the owner device signer, which is kernel-internal. A second device of the same account cannot produce one either, since reconcile_control anchors on the recorded owner device. The kernel gains a local-qualification-only hook that mirrors the existing fork test: load a retained control, replace its change body with an owner-removal, re-sign with the authenticated owner signer and return the encoded evidence. It is read-only on retained state, refuses non-owner sessions and compiles only into acceptance builds. The browser reaches it through a gated Request/Response::Divergent variant (tags 33/119) dispatched by the real worker; the response codec still verifies the signature and scope at the wire boundary, and the new codec test covers truncation, trailing bytes, wrong variant, foreign scope, unsigned payloads and oversize. The 'divergent-proof' qualification phase hands the proof to the panel journey, which feeds it to the member through the actual .vhproof observation path: the kernel writes its durable fault, the worker ends terminally, and a fresh document in the same browser context reopens the exact locator to show quarantine, retained fork proof, refused sends and readable retained history. The journey reload now performs a real document teardown — a fresh target in the same browser context — instead of Page.reload, because a CDP call bound to a dying execution context can be dropped silently. Every CDP call is bounded so a dropped reply fails fast naming the method, post-reload readiness waits on unlock (an existing account boots to Locked), and launch flags disable only the scheduling throttles that let an occluded target starve captureScreenshot. Receipt: passed, manifest 8ee8935e, 3 contexts, zero network writes, screenshots at 1280/768/390. Expired envelopes remain the remaining retained-control edge case; owner succession and live-custody transfer are still separate work. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The last uncovered retained-control edge case was expiry: the kernel's apply path refuses a control whose caller clock is past the local or owner enrollment validity, but the browser session only ever supplies Date.now(), so the DOM journey could never reach the branch. The validity interval is already an explicitly caller-clocked check — the signature does not make the clock trustworthy. A gated Request::ApplyControlAt (tag 34, bounded envelope + caller second, ordinary Membership reply) now forwards an explicit clock through the real worker dispatch, and a local-qualification-only 'expire-apply' phase feeds it a real next-floor removal control at a timestamp past every journey enrollment. The kernel refuses with its ordinary time error, publishes nothing, and the worker ends terminally. The journey then performs a real document teardown on the fresh-device member and applies the identical envelope under the wall clock: it commits, the roster shrinks, the member can still send, and fork evidence stays clean — proving the expired refusal mutated nothing and quarantined nothing. Codec coverage adds the new frame's round-trip, truncations, trailing bytes, wrong-variant decode and oversize refusal. Receipt: passed, manifest 12fb68ad, 3 contexts, zero network writes, screenshots at 1280/768/390. Retained-control edge cases are now fully covered in the DOM journey; remaining private-room gaps are relay transport, owner-device succession and live-custody transfer. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The private_indexeddb example exercises IndexedPrivateStore against real IndexedDB (exact CAS, stale tabs, quota denial, aborted/canceled writes, missing/corrupt/oversized records, orphan keys, bounded refusal) but no workflow step built or ran it. Wire it into the existing fixture block and document the harness coverage; the remaining gate items shrink accordingly. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The opaque relay boundary had only in-process and file-mailbox adapters. relay/net.rs adds a bounded framed TCP service around the existing mailbox: a 64-hex admission token read from a 0600 file or pipe, explicit numeric IP:PORT endpoints, submit/page/fetch ops with deadlines and bounded wire pages, and a scan() that persists a private cursor after each item so killed or offline catch-up resumes exactly while conflicting retained files fail closed. CLI relay-serve/relay-submit/relay-scan expose it without opening identity or room custody. A cross-process test journey serves, submits, retries idempotently, refuses a wrong token, scans, kills and restarts the service, resumes, and applies the delivered ciphertext into the member room. It remains a local operator-controlled reference adapter — no DNS, TLS or public Internet claim — and a receipt is still retention only, never member acceptance. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Extend the indexeddb_qualification fixture to cover the remaining open-side gate items: queued pending opens still occupy their bounded slots (a fifth refuses Bounds), a future dropped mid-open cancels the request so its late success closes the connection and frees the slot, an open queued behind a pending delete aborts its own upgrade when canceled, and a foreign version bump fires the adapter's versionchange path — live handles close, latch needs_reopen, and the bumped database refuses v1 opens. The harness gains foreign-connection hook modes and terminates a dedicated writer mid-flight, then verifies only the committed checkpoint survives. Empirically Chromium never reports blocked on the adapter's own request at the pinned schema version — equal-version opens queue behind in-flight creates — so that arm is documented as a defensive branch for other engines. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Owner device loss previously meant the room's owner authority died with the custody. This adds a handoff primitive: the live predecessor device signs a ControlChange::Succession carrying an account-signed grant that pins scope, account, predecessor, the exact successor enrollment, the carrying control sequence, and a validity interval. The successor must already be rostered, so the handoff is an atomic state.owner pivot with no roster churn; the predecessor demotes to ordinary membership. State retains a bounded grant chain (16) and resolves owner authority by control sequence via owner_device_at, so historical controls stay attributed to the generation that signed them. Joins, contact offers (v2), invite packets (v3), checkpoints, retained controls, recovery archives, and transport all verify the current owner through the chain rather than pinning the anchor device for the room's lifetime. Fixes two latent bugs the succession tests exposed: member-joined MLS groups did not inherit the ratchet-tree extension flag, so a promoted successor emitted welcomes without ratchet trees; and the retained checkpoint required the roster to contain the local enrollment byte-for-byte, which broke validation after a promoted successor renewed its credential. The checkpoint now pins at device level. Qualified end-to-end in kernel tests, the CLI fresh-device journey, and the real Chromium private-panel journey: handoff commits, predecessor demotes and applies successor controls in order, successor renews, and demoted predecessor cannot regain owner operations. Safe live-custody transfer remains explicitly unfinished. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The succession chain bumped the invite packet magic; two version pins still cited \x02. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Workspace clippy --all-features flags drain(..).collect() into the same collection type. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sender-local outbox sequences are not globally unique: two members
publishing into one namespace both start at sequence 1, so a mailbox
keyed by sender sequence either collides or conflates distinct items.
The relay now assigns each retained item its own increasing position.
- RelayReceipt reports position + digest + duplicate; dedup stays
digest-based and same-operation different-bytes still conflicts.
- RelayPage carries PositionedItem { position, item }; the sender
sequence remains committed metadata inside the item digest.
- Socket adapter wires position in submit receipts and page records,
rejects nonpositive/non-increasing positions, and fetches/scans by
position; scan files and cursors are position-keyed.
- FileStore bumps to format 2 (position INTEGER PRIMARY KEY, digest
UNIQUE); a v1 sequence-keyed database refuses rather than migrating.
- CLI relay-get takes --position; receipts and page JSON report both
position and sender-local sequence.
Adds multi-sender overlap/interleave coverage and a CLI socket journey
where the member replies through the same mailbox at position 3 while
its own outbox sequence is distinct.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
flock locks attach to the open file description, so a process spawning a child transiently shares every inherited descriptor between fork and exec; under load that window outlasts a peer's close() and a concurrent opener sees WouldBlock even though the holder is gone. The CI suite has been flaking on exactly this (Err(Busy) at Store::open immediately after a drop), including on main. acquire_shared already retries ~30s for the same reason; give acquire_exclusive a short bounded retry (20 x 25ms ~= 500ms). A genuinely held lock still refuses within the bound, never blocks — only the transient release window is absorbed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The socket adapter required four hand-tracked commands per direction (export, submit, scan, apply), which made bidirectional delivery impractical. Two explicit composites close the loop while keeping every existing boundary: - relay-push pages a bounded local outbox prefix (default 16 records, kernel MAX_PAGE_RECORDS) and submits each artifact, reporting sender sequence beside assigned mailbox position; confidential offer issuance is counted under skipped_secret and never leaves the store. - relay-pull runs the durable position-cursor scan into --dir, then applies every retained item in position order through the same authenticated dispatch as relay-apply. Deterministic refusals (the puller's own echo, stale or out-of-order controls) reopen custody and are retried on the next pull, so items heal once predecessors arrive. KeyPackage/contact-request envelopes stay gated behind their dedicated explicit commands. Neither composite prints plaintext. The CLI socket journey now proves a member's reply flows back through the same mailbox: its outbox (contact request + message) lands at positions 3 and 4 beside the owner's overlapping sender sequences, the owner pull accepts the reply, refuses the own-echo items, and skips the request for its explicit command path. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
relay-pull now retries refused items in a bounded fixpoint (eight passes) instead of requiring one pull per dependency depth: an item delivered before its parent heals in the same invocation once the parent lands at a later position. The socket journey proves it with two chained renewal controls submitted parent-last. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Resolves the site conflict that left the PR unmergeable and silently skipped pull_request workflows: keep the new foil wordmark and GitHub header action from #86 while retaining the primary Docs/Readiness nav that docs.ts propagates to every documentation page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
relay-submit/relay-scan/relay-push/relay-pull now accept exactly one transport: the token-authenticated socket, or --mailbox DIR opening the durable FileStore directly under filesystem custody. A synced folder or explicitly copied mailbox carries the same position-ordered items with no listener at all — the offline-scheduling path the relay reference needed. net::scan is generic over a new PageSource trait implemented by SocketRelay, FileStore and Store. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The directory journey now also exercises relay-submit/relay-scan over --mailbox, and relay-pull drops the FileStore after staging items so its exclusive lock is not held through the room apply pass. A flag-conflict test refuses mixed or partial transports before any custody write. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
relay-pull skips KeyPackage and contact-request envelopes because they belong to explicit member commands, but nothing extracted their inner payload from a retained .vhrelay. relay-unwrap verifies one canonical item and writes only its payload, so a staged contact request feeds request/accept/join byte-exactly while authentication stays with the dedicated command. The socket journey now proves the staged request unwraps to the same bytes the accept path consumed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The browser gate added on this branch probes document.images after readyState complete. Chrome never decodes an SVG img element served as application/octet-stream, so the foil mark reported naturalWidth 0 and the home page failed. Map .svg to image/svg+xml, matching what the deployed host returns; the probe now passes locally end to end. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Require predecessor-signed succession proofs, preserve bounded relay scan custody and publication ordering, and recover browser archive errors safely. Add isolated TLS, agent-compartment, archive and dead-device policy qualification with review evidence and CI coverage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds maintained public/private room clients and a local, mostly persistent Mac workflow for existing CLI agents. Codex and Devin can use one explicitly granted private room through MCP; native and browser clients retain encrypted work across outages without resetting custody, consent or retry budgets. No paid host or public domain is required for this workflow.
Validation: 420 selected Rust tests, 43 workspace doctests, 49 workflow/security script tests, five recovery-policy tests and 16 broker tests passed. Final workspace formatting and all-target/all-feature strict Clippy passed. The fresh 47-graph advisory gate reported no active vulnerabilities; inactive/archive findings and 15 warnings remain recorded. Installed Codex 0.155.1 and Devin 3000.11.1 each discovered all five tools and called exact-grant status. Actual Mac LaunchAgent lifecycle and Tailcat/TLS explicit restart journeys passed. The production private-panel/archive journey passed at desktop and narrow widths. The same bundle passed all 11 real browser/gateway/TLS delivery checks, including credential replacement, outage recovery and malformed-receipt refusal. The retained TLS prototype passed nine tests and strict lint after a minimal local-dependency lockfile repair. The private-store fixture separately verifies absent-schema refusal and preservation after FORMAT loss in real Window/Worker contexts. Exact-head CI passed all 60 source jobs, four CodeQL analyses and the authentic CodeQL verdict (65 checks). The paginated alert inventory is empty and all review threads are resolved.
The approved 364 baseline CodeQL dispositions were individually applied and independently read back (356 synthetic fixtures, eight false positives). Rules and security checks remain enabled; fresh current-head analysis passed separately with zero open alerts.
Operational limits: tests use synthetic state on one Mac. Independent-machine availability, reboot/sleep/wake and install-free remote browsers are not qualified. Tailcat v0.7.0 required explicitly restarting its client forward after a server restart, preserving the same key and port. Browser send status currently reports relay retention, not verified peer-acceptance status. TLS leaf renewal remains a documented operator boundary before its 365-day expiry.
Execution and retained qualification evidence · CLI agent setup · Local host setup.