docs(p2p): a proposal for signaling that needs no one to carry values - #74
Merged
Conversation
Opening a P2P connection today means a human carries four values between the two peers, in two round trips. The last of them only exists after the client has started connecting, so the exchange happens while the client sits in a retry loop — a structure that has already produced a failure we spent a day chasing (`p2p_mode_migration_plan.md` §7 #21). Only one of those four is a decision: whether this endpoint may reach this camera. The rest is mechanical. So the proposal keeps the decision with a person and moves it to enrollment, where it happens once, and takes the per-connection exchange to nothing. Three ways in, because they serve different situations: same-account auto-grant for your own devices, a pairing code or QR for granting access to someone else, and an approval prompt where no shared secret is acceptable. At runtime the client discovers what it may reach, connects without a capability, and the proxy tells the listener to bind. The piece that does not exist yet is a channel from the proxy to a listener that is waiting: the relay leg is opened per connection, so today there is nowhere to deliver "someone wants in". The proposal puts an event stream on the control plane, and reconciles pending connections on every reconnect rather than keeping an event log — a stream that drops is normal, and the reconciliation is what makes it correct. The phases are ordered so the manual steps disappear at phase 3, on polling, without waiting for the proxy work that phase 5 needs. Written from what this repository shows. The proxy is not in it, so §6 onward is a proposal to be checked against the real spec — recorded as risk #1 rather than presented as settled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…at it broke The two questions the proposal left open are answered by reading `ISEKAI-link-server`. One of the answers invalidates the option it made the default. There is no grant-equivalent in the proxy — four tables, none of them a standing authorization — so building one is not duplication. And the owner is available: `peer_listeners` stores `owner_sub`, with a `listener_owner_sub()` the connect handler already calls. But available is not the same as permitted to authorize on. The server spec names as a defence boundary that relay authorization is per-Endpoint and **must not pass on a user match**, and the connect handler says in as many words that it captures each party's sub "for auditing and correlation", not for authorization. Same-account auto-accept, as written, was on the wrong side of that line. So the mechanism changed and the convenience survived: an owner match now creates a *grant*, and the connect path authorizes on the grant. What is checked at connect time is still an Endpoint, the permission is revocable and listed rather than implied, and the audit trail says which grant let a peer in. Correcting against the implementation moved several other things. Connections are created in `relay` immediately and have no pending state, so the proposal no longer invents one — automation changes who calls bind, not the state machine. Approval prompts do need a new state, which is why they are last. Permissions are exact-match with no wildcards, so new endpoints need new strings issued by Identity — a second repository, named as a dependency rather than discovered later. Capability turns out to be one-shot with a 30–300s TTL, which is the sharpest argument in the document that it was never meant to be carried by hand. Phases are now split by repository, and the manual steps disappear at phase 4 on polling, before any of the streaming work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
A proposal for replacing the manual exchange with automatic signaling:
docs/p2p_signaling_plan.md. Document only — no code changes.The problem
Opening a P2P connection means a human carries four values between the peers, in two round trips. The last one only exists after the client has started connecting, so the exchange happens while the client sits in a retry loop. That structure already produced a failure we spent a day chasing — the manual work did not fit inside the 120-second deadline (
p2p_mode_migration_plan.md§7 #21).Only one of the four is a decision: whether this endpoint may reach this camera. The rest is mechanical.
The shape of the proposal
Keep the decision with a person, move it to enrollment where it happens once, and take the per-connection exchange to nothing.
Three ways in, because they serve different situations:
Default is A + B. For the common case — your own camera from your own phone — enrollment disappears entirely.
At runtime: the client lists what it may reach (
GET /v1/peer/listeners), connects without a capability, and the proxy tells the listener to bind. Nothing is carried.The piece that does not exist yet
There is no channel from the proxy to a listener that is waiting. The relay leg is opened per connection, so today there is nowhere to deliver "someone wants in".
The proposal puts an NDJSON event stream on the control plane (
GET /v1/peer-listeners/{id}/events) — existing auth, existing transport, separate from the relay data path — and reconciles pending connections on every reconnect rather than keeping an event log. A stream that drops is normal; the reconciliation is what makes it correct.Alternatives considered and why they lost are in §5.4.
Phasing
Ordered so that the manual steps disappear at phase 3, on polling, without waiting for the proxy work phase 5 needs. The event stream is a latency and load improvement, not a prerequisite.
What needs checking before any of this is built
Written from what this repository shows. The proxy is not in it, so §6 onward is a proposal, recorded as risk #1 rather than presented as settled. Two questions decide the shape:
§13 leaves three design decisions open on purpose: where a grant belongs, what becomes of
capability, and how multi-camera setups are presented.🤖 Generated with Claude Code