Skip to content

docs(p2p): a proposal for signaling that needs no one to carry values - #74

Merged
masa-koz merged 2 commits into
mainfrom
docs/p2p-signaling-plan
Aug 2, 2026
Merged

docs(p2p): a proposal for signaling that needs no one to carry values#74
masa-koz merged 2 commits into
mainfrom
docs/p2p-signaling-plan

Conversation

@masa-koz

@masa-koz masa-koz commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

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:

enrollment grant to others unattended camera shared secret
A same-account auto-grant none no yes none
B pairing code / QR carry a code once yes yes short-lived code
C approval prompt press approve yes no none

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:

  1. Does the proxy spec already have something equivalent to a grant? (avoid building it twice)
  2. Does Identity give the control plane the endpoint's owner? Method A depends entirely on this — if not, B becomes the default.

§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

masa-koz and others added 2 commits August 1, 2026 14:26
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>
@masa-koz
masa-koz merged commit c589371 into main Aug 2, 2026
@masa-koz
masa-koz deleted the docs/p2p-signaling-plan branch August 2, 2026 04:46
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.

1 participant