Skip to content

Add public/private room clients and local agent hosting - #85

Merged
0thernet merged 70 commits into
mainfrom
devin/overlay-profiles
Sep 22, 2026
Merged

0thernet merged 70 commits into
mainfrom
devin/overlay-profiles

Conversation

@0thernet

@0thernet 0thernet commented Sep 20, 2026

Copy link
Copy Markdown
Member

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.

  • Fixed-room MCP exposes status, inbox, prepare, queue and delivery metadata under finite, one-use grants with an explicit inference-disclosure declaration. Existing CLI processes retain their ambient authority; this interface does not claim OS containment.
  • TLS authenticates the selected CA, name and namespace before scoped credentials. Durable jobs preserve exact ciphertext and charged attempts; relay retention and authenticated member-processing claims remain separate. Correcting denied credentials requires an explicit new session and does not renew budgets.
  • A dedicated Mac LaunchAgent supervises the bounded opaque mailbox. Tailcat uses a saved key and fixed client port. The browser uses a separate authenticated loopback gateway, immutable connection binding, strict IndexedDB custody and explicit synchronization.
  • Native/browser archives retain separate authenticated read-only snapshots, exact interrupted import and bounded streaming export. Archives never become live devices; lost owner custody cannot create new recovery authority.
  • Release requires both native CLI archives, the macOS viewer and the tested production browser bundle. Browser bytes are checked against the successful delivery receipt; exact-main CodeQL analyses and a fully paginated zero-open-alert gate remain mandatory.
  • Integrates main's intentional Platonik removal while retaining the current public/private clients and standalone witness infrastructure.

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.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@0thernet 0thernet changed the title Build guarded public clients and verifiable puzzle exchange Build public room clients, durable author recovery, and product documentation Sep 20, 2026
Comment thread browser/tools/qualify_product.mjs Fixed
Comment thread browser/tools/qualify_storage.mjs Fixed
Comment thread browser/tools/qualify_product.mjs Fixed
@0thernet 0thernet changed the title Build public room clients, durable author recovery, and product documentation Build public and private room foundations, recovery, and product documentation Sep 20, 2026
0thernet and others added 21 commits September 21, 2026 18:34
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.
Comment thread crates/vhalla-browser-storage/src/outbox/continuity/tests.rs Dismissed
Comment thread crates/vhalla-browser-storage/src/outbox/continuity/tests.rs Dismissed
Comment thread crates/vhalla-browser-storage/src/outbox/continuity/tests.rs Dismissed
Comment thread crates/vhalla-browser-storage/src/outbox/continuity/tests.rs Dismissed
Comment thread crates/vhalla-browser-storage/src/outbox/continuity/tests.rs Dismissed
Comment thread crates/vhalla-public-protocol/src/response/tests.rs Dismissed
Comment thread crates/vhalla-public-protocol/src/response/tests.rs Dismissed
Comment thread crates/vhalla-public-protocol/src/response/tests.rs Dismissed
Comment thread crates/vhalla-public-protocol/src/response/tests.rs Dismissed
Comment thread crates/vhalla-public-protocol/src/response/tests.rs Dismissed
@0thernet 0thernet changed the title WIP: Public and private room clients, recovery, and docs Add public/private room clients and local agent hosting Sep 22, 2026
@0thernet
0thernet marked this pull request as ready for review September 22, 2026 20:51
@0thernet
0thernet merged commit 32a1946 into main Sep 22, 2026
65 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants