# Endpoint access-control model (visibility / whitelist / grants)

## Status

accepted (2026-06-02)

**Amended 2026-06-15 (ADR-0020):** the reply-vs-new correlation below is keyed on
the `<EVENT>` envelope's **`from` attribute** (correlate an inbound `from` against
the endpoint's prior outbound), **not** the `__REPLY_TO__` token. `__REPLY_TO__`
was a mis-elevated relic and is removed from spt-core; the sender-identity
granularity is unchanged (the relic carried the same `from`).

<!-- [doc->REQ-MSG-SENDER-STAMP] -->
**Amended 2026-07-29 (milestone A W2b, `REQ-MSG-SENDER-STAMP`):** the
origin-node bullet below says the gate checks the origin node **"not the sender
endpoint's identity"**. That was true for as long as no wire record carried a
sender identity worth trusting; it is **no longer the whole truth.** The access
gate now ALSO checks a **daemon-stamped, session-proven sender endpoint** —
`WanMessage.sender_proven`, the first tier of the subject chain the 2026-07-28/29
access-control grill ratified (CONTEXT §endpoint access whitelist, ADR-0053).
Precisely:

- The sender tier fires on the **MSG** family only; the other five wire-inbound
  families carry no sender endpoint and are decided exactly as this ADR describes.
- The stamp is sourced from the sending session (`roster::detect_self_id` plus a
  real perch), **never** from the caller-supplied `--from`, which remains
  reply-routing metadata and never an authorization subject (KNOWN-HAZARDS 7.5).
- **It does not replace the origin-node gate, it precedes it.** The node subject
  is still the QUIC-handshake-proven remote id, and a sender that matches no tier-1
  rule is judged by the node tier exactly as before. **Absence abstains**: a
  record with no stamp (any N-1 sender) reaches the node tier unchanged.
- Trust boundary, so no reader inflates "authenticated sender": the origin NODE is
  proven cryptographically; the endpoint WITHIN it is ASSERTED by the sending
  daemon. Strength equals `REQ-MSG-6`'s ratified boundary (trust = subnet
  membership, node = human-proxy). It defeats an **agent** forging `--from` — the
  adversary this milestone's threat model names — and does **not** defend against
  a malicious member node.

The retired half of the original sentence — "the sender endpoint's *location* is
irrelevant" — still stands: a rule names a sender endpoint, never where it lives.

## Context

Three access/discovery concepts accreted around the multi-subnet model and risked overlapping into a muddle: **endpoint visibility** (ADR-0006 — is an endpoint routable in a subnet at all?), a new **per-endpoint access whitelist** (who may *remotely reach* an endpoint), and the existing **consent grant store** (may *this agent* do *this high-risk thing*?). A new **resource advertisement** (free-text service blurb) also needed to know which access layer gates it. Folding the whitelist into the grant store was tempting but the two differ on two axes — subject granularity (origin **node/user** vs **agent**) and default polarity (**open** vs **deny**) — so merging would corrupt both. This ADR fixes the layering.

## Decision

Model access as **three orthogonal, nested gates**, evaluated in order:

1. **Subnet visibility** (ADR-0006, per-`(endpoint, subnet)`) — is the endpoint routable in this subnet at all? *Excluded* = invisible + unroutable.
2. **Endpoint access whitelist** (new, per-endpoint) — may *your node* (later: *your user*) remotely reach it?
3. **Capability grants** (existing consent store, `capability × agent × target-node`, default-deny) — may *this agent* perform *this high-risk capability* (remote-exec, instantiate-anywhere)?

**Resource advertisement** (the free-text discovery blurb / "subnet resource registry") is gated by gates 1–2: an endpoint a viewer can't reach never advertises what it can do.

### Access whitelist semantics

- **Origin-node gate.** It checks the **node the inbound interaction originates from** (where the operator is acting), **not** the sender endpoint's identity or home — *"the operator must be on a whitelisted machine; the sender endpoint's location is irrelevant."* — **superseded in part, see the 2026-07-29 W2b amendment above: a daemon-stamped proven sender endpoint IS now a subject (MSG family), ahead of this node check and abstaining when absent.**
- **Stateful-firewall model**, not a blanket inbound block:
  - **Outbound** → any **visible** node (the whitelist never restricts who the endpoint talks *to*).
  - **Inbound reply** correlated to the endpoint's own prior outbound (`__REPLY_TO__`) → allowed from any visible node ("established/related").
  - **Unsolicited/direct inbound** (new control or message) → only from a **whitelisted** node ("new inbound").
- **Same-node operation is always allowed** (you're at the hardware; the home node never whitelists itself). The whitelist gates **remote (cross-node)** reach only.
- **Default empty = open** (any subnet-visible node — current behavior); setting a whitelist *restricts*.
- **Node-tier ships M4** (Ed25519 node pubkeys). **User-tier is deferred** to the per-(subnet, user) model (ADR-0006 cross-user seam); the schema reserves an inert `users` field until then.
- **Distinct from grants, same plumbing:** enforced at the target endpoint's node, synced as security material near the trust store (not the context repo), subnet-settable, revocable — but a **different table + polarity** (origin-node / default-open vs agent-subject / default-deny).

## Consequences

- New v1 requirements registered: `REQ-SEC-1` (access whitelist, M4 node-tier), `REQ-INST-14` (resource advertisement, M4), `REQ-PAIR-7` (subnet icon, M4 data / frontend render) — all `required_stages = []` until M4.
- The routing/messaging path (M4) gains a whitelist check on inbound: classify reply-vs-new (via `__REPLY_TO__` correlation) before applying the origin-node gate.
- The registry projection that backs resource advertisement must apply the visibility (and later whitelist) filter per viewer.
- **Forward (cross-user, ADR-0006 seam):** the whitelist's `users` tier and the "nodes belonging to whitelisted users" semantics activate with the per-(subnet, user) model.
- Builds on ADR-0006 (visibility, multi-subnet) and the existing consent grant store (CONTEXT §Consent & security gates).
