worktree-all-nvrs-option #21

Merged
elirtf merged 7 commits from worktree-all-nvrs-option into dev 2026-08-26 18:33:47 +00:00
Owner
No description provided.
Plug in a box, it prints a code, someone types it, the box is linked. Replaces
trusted_keys.json, which only worked on the machine you were sitting at and had no
notion of who a box belonged to.

The relay issues the code, not the box. A box choosing its own could pick a guessable
one, and the relay cannot enforce uniqueness or a rate limit on a namespace it does not
control. The public key is bound at issue time, so a box cannot show one identity and
enrol another.

A claim code is a bearer credential — whoever redeems it gets someone else's cameras —
so it is treated as one:
- expires in 15 minutes, and works exactly once
- stored only as a SHA-256, so a readable state file does not hand over every box
  waiting to be claimed
- 5 attempts per minute per address; 32^8 is large, but an attacker with an unlimited
  fast loop is a different problem from one getting five tries a minute
- travels in a header, not a query string: query strings land in access logs, proxy
  logs and browser history
- every failure returns the same response, so a near miss is not distinguishable
- reconnecting replaces the outstanding code rather than adding one, or every restart
  would leave another valid code alive

One deliberate loosening, with its condition written down: an unknown box now gets
CLOSE_NOT_ENROLLED rather than the same rejection as a bad signature, because the agent
needs to tell the difference to start pairing and cannot ask any other way without local
state that drifts out of step with the relay. That leaks whether a box id exists, which
is acceptable only because box ids are 64 bits of randomness generated on the box —
nothing to enumerate, and knowing one exists grants nothing without the private key. If
box ids ever become predictable, that branch has to go back to being indistinguishable.

Codes avoid I, L, O and U — someone is reading this off a screen and 0/O and 1/I are the
classic misreads — and claiming accepts what people actually type: lower case, spaces,
missing dashes.

Store is JSON with atomic writes at 0600 (`ponytail:` marks it — Supabase replaces this
one module, nothing else). Atomic because a relay killed mid-write leaves truncated JSON,
and a corrupt enrollment file means every box in the fleet failing to authenticate at once.

19 new tests, all on what must fail: reuse, expiry, wrong codes, plaintext storage,
world-readable state. Checked the expiry test is not vacuous — disabling the expiry filter
makes it fail.

Verified end to end: unknown box prints a code, a wrong guess gets 404, the sixth rapid
attempt gets 429, the real code (typed lower case without the dash) claims it, the box
links itself within seconds reporting live disk usage, and replaying the code gets 404.
336 tests pass; the production stack was untouched throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Claim-code pairing replaces the hand-edited trusted key file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Three problems, all mine, all found by someone trying to follow it.

**Three words for one thing.** The code said "enroll" in places, "claim" in others, and
I said "pairing" out loud. Everything is now "pair": PairingStore, new_code(), pair(),
is_paired(), T_PAIR, CLOSE_NOT_PAIRED, tests/test_pairing.py.

**A whole second handshake that earned nothing.** An unknown box used to be told to
reconnect to a separate /enroll endpoint and repeat its id and public key — which it had
already sent in the auth frame it just made. 107 lines and three protocol messages to ask
the same question twice. Gone: the relay now answers the connection it already has with a
code instead of a welcome, and closes. One path, one handshake, two outcomes.

**A code that changed while you were typing it.** Running the flow properly — waiting a
realistic amount of time before typing — showed the code on screen was already dead. An
unpaired box retries every 15 seconds and every retry minted a fresh code. The old rule
("reconnecting replaces the outstanding code") was aimed at a real worry, a trail of codes
that all still work, but it fixed that by breaking the feature. Reconnecting now returns
the same live code, which gives one working code per box AND one that survives long enough
to type. Verified by waiting through several reconnects before pairing.

The test double in test_relay_handshake.py became a real PairingStore on a temp dir. It
needed a new method every time the store grew one; the real thing is less code and cannot
drift from what the relay actually calls.

Net: 957 -> 894 lines with more of the flow covered, 338 tests pass, ruff and bandit clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Simplify pairing: one handshake, consistent naming, stable code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Pairing solved a problem that only exists with many boxes and many customers. There is
one box and no customers, and will not be for a while. I replaced a working simple thing
(a key file) with a complicated thing for users who do not exist — codes, hashing,
expiry, single use, rate limiting, a claim endpoint, a store with atomic writes, and 19
tests to hold it all up.

Adding a box is now: the box prints a line, you paste it into relay/allowed_keys.txt,
the relay accepts it. That is the whole feature.

Deleted: relay/store.py, the /claim endpoint, the rate limiter, T_PAIR, and
tests/test_pairing.py. The handshake tests go back to a plain dict — a real store was
only needed because the fake kept growing methods.

Kept, because it is small and does the actual work: the signed-challenge handshake. Every
connection still proves the box holds a private key generated on it that never leaves it.
That is what stops someone impersonating a box, and it is unchanged. CLOSE_NOT_PAIRED
becomes CLOSE_UNKNOWN_BOX, which is what it always meant.

The agent now prints exactly the line to paste when the relay does not know it, and then
checks back once a minute instead of every fifteen seconds — there is nothing it can do
until a human acts, so it should not fill the log while it waits.

894 -> 647 lines. 326 tests pass, ruff and bandit clean. Verified end to end: empty
allowlist, box prints its line, paste it, restart the relay, box connects and reports
live disk usage.

A `ponytail:` note in the relay records when this stops being enough — when someone who
is not the operator has to add a box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Replace the pairing-code system with a plain allowlist file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ag1iD139Ec63dPLTNATX3s
Checking it selects every NVR; unchecking clears the lot. Backend already
accepts the full ID list, so no API change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hFgd5LTUH2ioMp9x9Wf5w
elirtf deleted branch worktree-all-nvrs-option 2026-08-26 18:33:47 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
elirtf/opus!21
No description provided.