Skip to content

Add trusted anchors for durable offline mailbox handoffs - #158

Open
aakashH242 wants to merge 2 commits into
reason-machines:mainfrom
aakashH242:feat/durable-owner-anchor
Open

aakashH242 wants to merge 2 commits into
reason-machines:mainfrom
aakashH242:feat/durable-owner-anchor

Conversation

@aakashH242

@aakashH242 aakashH242 commented Oct 26, 2026 •

Copy link
Copy Markdown

This implements the same-person trusted anchor discussed in #139. A cloud workspace can hand a message and its existing small text attachments to the user's always-on VPS while the laptop is off. Once the anchor confirms durable storage, the sender can go away; the anchor keeps custody until the laptop saves the message.

What changes

  • Enable an anchor locally with anchor = true. It advertises through Presence.features and remembers only permanent mailboxes (terminal and listen --as), so a fresh workspace can discover an offline laptop without a registration command.
  • Keep direct delivery first. The anchor accepts only the same person's machines, preserves the original source identity and message ID, and uses separate acknowledgments for anchor acceptance and recipient storage.
  • Store complete envelopes in SQLite, with limits of 1,000 pending messages, 64 MB of payloads, and 10,000 retry receipts. Full anchors explicitly refuse acceptance and the sender retains its copy. Retries do not extend the seven-day lifetime from the original send.
  • Cancel saved routes and pending mail on mailbox removal or machine revocation. Drop notices remain readable through the anchor's listen --as terminal inbox, including after a terminal rename. Failed notice writes cannot undo revocation or make cancelled mail deliverable again.
  • Gate new frames on capabilities announced on the current connection, including reconnects after a downgrade. Existing message paths remain available for older engines and people without an anchor.
  • Flush received attachment data before the storage acknowledgment, with directory synchronization on Unix. The existing attachment limits and recipient policies still apply.

As agreed in the issue, the anchor is trusted and can read the stored mail. This does not add an encryption layer for an untrusted anchor. Larger artifacts, R2, and task allocation remain follow-ups. README and DESIGN.md describe setup, custody, and limits.

Validation

Built and tested on Linux, including an unmodified 0.12.17 binary for the mixed-version checks:

Check Result
cargo fmt --check Passed
cargo clippy --all-targets --locked -- -D warnings Passed
cargo test --locked 305 unit tests and 2 installer tests passed
tests/e2e/anchor.py 32 live checks passed
tests/e2e/reconnect.py 41/41 checks passed
tests/e2e/two_machines.py 150/150 checks passed

The new e2e script uses disposable homes and test identities over real iroh connections. It covers fresh-workspace discovery, offline delivery with attachments, sender shutdown, anchor restart, lost acceptance and recipient acknowledgments, direct/forwarded deduplication, direct delivery first, capacity refusal and recovery, original-send expiry with CLI notice retrieval, unknown mailboxes, revocation learned from another machine, and mixed versions. Unit regressions also cover SQLite write failures, cancellation persistence, source/acknowledgment checks, renamed terminal inboxes, and current-connection capability gating.

The full Rust suite ran from a byte-identical copy on a Linux filesystem: the mounted checkout rejects its target directory, which affects two existing file tests. No tests were skipped or changed for that workaround. Local review logs and generated files are not included.

Hosted handoff limitation

As clarified in Lars's reply, Reason already stores hosted inboxes centrally, keyed by the hosted connection and its as name. There is no per-conversation ID. The remaining gap is mail that has not yet reached Reason: it can still be waiting on the computer carrying the session or in peers' outboxes for that computer.

This PR covers permanent mailboxes. It does not transfer those computer-bound hosted-session queues when the carrier changes. The local daemon-facing simulation verifies that mail saved on the laptop is not silently migrated by registering an ordinary session on the VPS, and remains retrievable when the laptop returns. It does not exercise Reason's hosted inbox or actual reason network __relay service.

Rebased onto current main (bcf045c, v0.12.17), preserving upstream delivery receipts, idempotent sends, usage tracking, and native agent support. A new regression test verifies that peer-scoped acknowledgments preserve receipt states and count acceptance only once. README and DESIGN.md now describe the hosted handoff boundary above.

macOS/Windows execution and physical power-loss testing were not run locally.

Closes #139.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-26T19:06:59.329393Z 36fa049 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copy link
Copy Markdown
Author

Hey @larstalian, the PR for #139 is ready for review. Thanks for helping work through the design.

I also wanted to ask about the hosted inbox limitation, as a separate follow-up. If mail reaches a hosted assistant through the laptop just before it goes offline, that mail stays on the laptop even if the next call goes through the VPS.

Would it make sense to let the VPS be the only computer handling hosted-assistant calls, while the laptop continues running the network for local agents? The idea would be to keep hosted inboxes in one place, so switching computers doesn’t leave mail behind.

Two questions before proposing any changes:

  1. Does Reason choose hosted-call computers only from those with an active reason network __relay connection? If the laptop stopped running that bridge but kept its network daemon running, would hosted calls use the online VPS?

  2. Does the bridge provide a stable conversation ID across calls and reconnects? We’d need to recognise the same conversation and keep using its existing inbox.

For this first follow-up, I’d be fine with hosted calls reporting that the VPS is unavailable when it’s down, rather than quietly starting a different inbox elsewhere.

I’d keep this PR focused on the agreed permanent-mailbox handoffs. Happy to help with the hosted inbox follow-up separately if that approach fits.

@larstalian

Copy link
Copy Markdown
Member

Thanks for this! We are reviewing it now (the full suite plus our own checks) and will come back here with findings.

@aakashH242

Copy link
Copy Markdown
Author

Thanks. Looking forward to your response.

@larstalian

Copy link
Copy Markdown
Member

Sorry for the churn: main moved quite a bit in the last two days (v0.12.14 to v0.12.17, with changes in mail.rs, store.rs, proto.rs and the daemon), and GitHub now shows conflicts here. Could you rebase onto current main when you get a chance? We'll review the rebased head right away.

@larstalian

Copy link
Copy Markdown
Member

Thanks, and agreed on keeping this PR to the permanent-mailbox handoffs. Short answers: (1) yes; (2) no, there's no conversation id. The stable identity is the connection (and its as name), and the hosted inbox already lives on Reason's side, not on a computer.

1. Only computers whose relay is checking in. Reason only sees a computer through its reason network __relay, which long-polls Reason. A computer counts as online for hosted calls only while its relay has checked in within about the last minute. A laptop whose relay isn't running is never picked, so calls go to the VPS.

Two things matter for your plan:

  • The relay restarts on its own. Where the Reason CLI manages the engine, the daemon keeps __relay running. If the relay stops, the daemon starts it again after 30 s, backing off to 10 minutes while it keeps stopping. reason connect's background service runs a relay too. So today there's no supported way to keep the laptop's daemon running but take its relay off. (A standalone engine install, not managed by the Reason CLI, never runs a relay.)
  • A hosted session doesn't move back. Each hosted session is carried by one of the person's online computers. If that computer stops checking in, another one takes the session over within a minute or two. It stays there when the first computer comes back; a returning computer doesn't take it back.

2. No conversation id. Hosted apps don't send one, and the hosted MCP endpoint keeps no per-conversation state. Two things are stable:

  • The connection: one OAuth grant, the same through token refreshes. A reconnect of the same app gets its name back.
  • as names: within a connection, a chat or bot can name itself with as. That gives it its own address and inbox while it keeps using the name.

check_inbox reads an inbox that Reason keeps for that name, not for a computer. Mail handed over by any of the person's computers lands in the same inbox, and the wake webhook is on Reason's side too. So "one place for hosted inboxes" already holds there.

What is tied to a computer is the engine-side session and its address:

  • Mail not handed over yet: mail that reached the laptop's engine but wasn't handed to Reason before the laptop went offline waits on the laptop. Its relay hands it over once it reads that session again: while Reason has it carry the session, and for a day after the session last sent from there.
  • Mail held by peers: while the laptop is offline, peers hold mail addressed to the session there until it is back.
  • Suffixed address: after a move, the new computer's engine may register the session as <name>-2 while the old computer still has the plain name (live, or kept for a session that went offline). It asks for the plain name back every five minutes.

That computer-bound handoff is the part a follow-up would need to cover. Happy to go through it with you separately, as you suggest.

@aakashH242
aakashH242 force-pushed the feat/durable-owner-anchor branch from 36fa049 to 8d1e45d Compare November 2, 2026 06:26

Copy link
Copy Markdown
Author

Hey @larstalian, I’ve rebased this PR for #139 onto current main (bcf045c, v0.12.17) and pushed the updated commit, 8d1e45d. GitHub now shows no merge conflicts.

The conflict fixes keep the new delivery receipts and usage tracking, along with the anchor’s checks on which computer sent an acknowledgment. I added a regression test for acknowledgments from the wrong computer and repeated acknowledgments.

I reran the checks on the rebased code:

  • Formatting and Clippy with warnings denied: passed.
  • Rust tests: 305 unit tests and 2 installer tests passed.
  • Anchor tests: 32 checks passed, including compatibility with an unmodified v0.12.17 engine.
  • Reconnect tests: 41/41 passed.
  • Two-machine tests: 150/150 passed.

Thanks for explaining the hosted inbox side. I had misunderstood where the inbox lives. I’ve corrected the README, DESIGN.md and PR description: Reason already holds the hosted inbox centrally; the remaining gap is mail waiting on a computer or its peers before it reaches Reason. I also understand there’s no conversation ID, and a managed laptop can’t currently keep its daemon running with the relay disabled.

This PR stays focused on permanent-mailbox handoffs. Happy to work through the computer-bound hosted handoff separately. The hosted test here is only a local engine simulation; I haven’t tested the actual Reason hosted service.

The CI run currently says action_required. Could you check whether it needs approval? The rebased PR is ready for your review.

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.

Optional durable anchor for encrypted offline handoffs

2 participants