Skip to content

fix(peers): a peer's request can only be allowed for that peer, for 30 days by default, and one peer cannot flood the Nest with cards - #787

Merged
kh0pper merged 6 commits into
mainfrom
fix/peer-allow-scoped
Nov 6, 2026
Merged

kh0pper merged 6 commits into
mainfrom
fix/peer-allow-scoped

Conversation

@kh0pper

@kh0pper kh0pper commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Security audit N1b-8.

A paired peer could make a one-click "Allow" fix-it card appear, and clicking it exposed the capability to every trusted peer, with no end date.

Scoped to the peer that asked. The card records the peer's verified instance id, the one its bearer token resolved to. A name or id sent in the request body is never used. Allow grants the capability to that peer only, and the exposure check consults per-peer grants alongside the existing all-peer list. Exposures already made for all peers are not migrated or narrowed.

Expiry. The card offers 30 days (the default), 7 days, or "until I remove it". An expired grant is refused at check time. Settings → remote exposure lists per-peer grants with their peer and expiry, and they can be renewed or removed there. A malformed expiry fails closed.

Rate limit. Each peer can have at most 3 open Allow cards, and at most one new card per capability per 24 h. A repeated request bumps the existing card's count instead of making another card.

Storage. A second local-only JSON setting, remote_exposed_tools_by_peer, sits beside the existing remote_exposed_tools. Neither is in the sync allowlist, so both stay on this instance. There is no schema change. Peer ids are shape-checked, and __proto__-style keys are refused.

Tests:

  • Allow grants only the requesting peer, and a second peer is still refused;
  • 30-day and 7-day expiry with an injected clock, and "until removed" never expires;
  • all-peer exposures are unchanged;
  • the open-card cap, the 24 h window and dedupe;
  • the card's identity comes from the verified token, not from the body.

Review follow-ups (four rounds):

  • Concurrent requests: the per-peer card limit holds when requests arrive at once. Each peer's decide-then-store runs serialised, and the queue is capped.
  • Capability ids: only capabilities in this instance's own catalog can raise a card. Anything else counts in one bounded bucket per peer, and the counter map is capped and pruned.
  • Repeat requests: a repeat request updates the existing card, including one written in the old key format. It never opens a second card.
  • Peer catalog: the capability catalog served to a peer marks that peer's own active grants as exposed, and never shows another peer's.
  • Revoking a peer: revoking removes its grants and closes its open cards. The trust sweeper does the same for a revoke written straight to the peer row.
  • Pairing tag: each card carries a short digest of the peer's verified token hash, never the hash itself. Allow refuses a card whose digest no longer matches the peer's current pairing, or that has none. So a card raised before a revoke can't grant the peer after it re-pairs under the same id. A missing hash at the gate leaves the card untagged and refused.
  • Closed cards: a remedy never runs on a closed card.

Host: 914 pass, 0 fail across the peer, exposure, fix-it, sharing, trust, capability, federation, revoke, sweeper, settings, health and public-hygiene files.

https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh

@kh0pper
kh0pper force-pushed the fix/peer-allow-scoped branch from 4f27a3e to d9c280f Compare November 6, 2026 03:42
…0 days by default, and one peer cannot flood the Nest with cards

The one-click Allow card exposed a capability to every trusted peer with no
end date. It now grants only the peer the request was verified as (its
instance id from the bearer token, never a name it sends), for 30 days, 7
days or until removed. An expired grant is refused and shown in Settings
for renewal or removal. Exposures made for all peers keep working as they
are.

Each peer gets at most 3 open Allow cards and one new card per capability
per 24 hours; a repeated request is counted on the existing card.

Storage is a second local-only setting beside the existing one, so there is
no schema change.

Claude-Session: https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh
… capabilities raise cards, and revoking a peer removes its grants

Review follow-ups: one serialised decide-then-upsert per verified peer; an
unknown capability id counts in one bounded bucket and never gets a card;
the counter map is capped and pruned; the capability catalog served to a
peer marks its own active grants exposed; revoking a peer removes its
grants; an unambiguous dedupe key; a remedy never runs on a closed card.

Claude-Session: https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh
…r's open cards are closed, and a flood is dropped, not queued

Review follow-ups: a dedupe acts on the existing card whatever key shape it
was written with; revoking a peer resolves its open Allow cards, and the
remedy refuses a peer that is no longer trusted; the per-peer queue is
capped and overflow is counted in memory; comment corrections.

Claude-Session: https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh
A card raised just before a revoke, or left open by a revoke written
straight to the peer row, could still grant the peer after it re-paired
under the same id. Each card now carries a short digest of the peer's
token hash, which changes on every re-pair and rotation; Allow refuses a
card whose digest no longer matches, or that has none. A new request from
a re-paired peer replaces its stale card, and the trust sweeper closes a
revoked peer's open cards too. The raw hash is never stored on the card.

Claude-Session: https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh
@kh0pper
kh0pper force-pushed the fix/peer-allow-scoped branch from d9c280f to 536ab32 Compare November 6, 2026 04:10
…and the sweeper lists cards once per sweep

Review follow-ups: no fallback re-read of the current peer row (a missing
hash tags nothing, and the card is refused); tests cover the verified-hash
path and an old-hash request after re-pair; the trust sweeper lists pending
cards once per sweep instead of once per revoked peer; a refusal message
that fits every case.

Claude-Session: https://claude.ai/code/session_015Vve3soBJ9qTcyeCCLyYyh
@kh0pper
kh0pper merged commit adb350c into main Nov 6, 2026
4 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.

1 participant