# Viewing access rules and posture

<!-- [doc->REQ-ACL-VIEW-ROSTER] --><!-- [doc->REQ-ACL-VIEW-DRILLDOWN] --><!-- [doc->REQ-ACL-NODE-VIEW] --><!-- [doc->REQ-SUBNET-STATUS-MODES] -->

Access views are **roster-first**: instead of one flat dump of every rule,
each view lists the *access entities* — the subnets, machines, and endpoints
that rules or modes actually name — grouped by type, each with its rule count
and (for a subnet or this machine) its mode. The granular rule list is
readable only per named entity, because reading a posture is a comparison
per entity, not a scroll.

## Per-endpoint: `spt endpoint access`

```text
spt endpoint access             # every ruled endpoint's roster
spt endpoint access wanda       # one endpoint's roster
```

A roster line per ruled entity, grouped subnets → machines → endpoints:

```text
access entities ruled for 'wanda':
  bignet - mode: closed (DISCOVER open (default)) - 2 access rules
  quietnet - mode: open
  HFENDULEAM - mode: open
  flynn@kitsubito (bignet, quietnet) - 1 access rule
```

- A subnet appears when rules name it **or** when its captured mode decides
  the endpoint's fallback posture — a mode source is part of the picture
  even with zero explicit rules.
- A `closed` mode names what it does **not** close. `DISCOVER` — being found —
  is on by default, and no blanket posture reaches it, so a closed table
  renders `closed (DISCOVER open (default))`. Where someone has written an
  explicit open for it the line reads `DISCOVER open (pinned)` instead: a pin
  outlives a change of the default and is a different fact from the default
  itself. The only things that close it are a deny row naming the surface (per
  subject) and, node-wide, `spt api access-node-surface-mode DISCOVER closed`
  from this node's engine room.
- `WEB` — a served file fetched by another subnet member — is also on by
  default within the subnet: registering a file is already a deliberate act of
  exposure, so a blanket closed posture does not close it. A deny row naming
  `WEB` (per subject) or the node-wide surface mode does. The owning node
  checks the row under the fetching node's handshake-proven identity and
  answers `403` with a body naming `WEB`. The two sides of a `WEB` rule: its
  **origin** matches the fetching node or that node's subnet (no sender
  endpoint exists to name yet); its **subject** is the endpoint that
  registered the file, or the node itself for the index and the docs.
  <!-- [doc->REQ-WEB-CROSS-NODE-PROXY] -->
- This machine (colored like it is in `spt endpoint list`) appears with its
  own mode when one is set.
- A ruled endpoint resolves to `id@machine (shared subnets)` where this
  node can see it, and renders bare where it cannot — being unresolvable
  does not hide the rule.
- Entities with no rules for the target are omitted entirely.

Drill down to the granular rules of one named entity:

```text
spt endpoint access wanda --subnet-rules bignet
spt endpoint access --endpoint-rules flynn
spt endpoint access --node-rules <pubkey-hex>
```

<!-- [doc->REQ-ER-RULESET-TABLE] --><!-- [doc->REQ-ER-RULESET-NODE-NAMES] -->
Each prints the matching rules as a **markdown table** (subject, surfaces,
deciding tier, verdict) — the same renderer the engine room's session briefing
uses, so the grid you are briefed with and the grid you can ask for again never
drift. Markdown because a briefing is read wherever an agent relays it: a
space-aligned grid is reflowed into a run-on by a chat surface, and a table that
stops being a table in transit is not one. Cells stay padded, so the same render
is still column-aligned in a terminal.

A **node subject reads as the node's name**, not its pubkey:

```text
| SCOPE    | TIER          | SUBJECT        | SURFACE | ORIGIN | DECISION | FROM   |
| -------- | ------------- | -------------- | ------- | ------ | -------- | ------ |
| node     | node-rule     | subnet:bignet  | DISCOVER| -      | allow    | manual |
| sparrow  | endpoint-rule | node:ENLYZEAM  | MSG     | -      | deny     | manual |
```

A node this machine cannot name — no roster entry, no advertised label — keeps
its **full** pubkey hex instead. Never blank, never an error, and never
shortened: a truncated hash is the thing that was unreadable in the first place,
and the full one is at least pasteable into a rule. The name is a rendering
only; `--json` keeps the full hex as the row's `subject` and carries the
resolved name beside it, so anything keying on the pubkey is unaffected.

One naming caveat: an endpoint literally named `allow`, `revoke`, or `open`
parses as the editing subcommand, so it has no positional path — reach its
rules through the drill-down flags instead.

## Node tier: `spt node access`

```text
spt node access
```

The node-tier roster: the entities the node-wide rules name, this machine's
own mode (with per-surface exceptions, e.g.
`closed (MSG open, DISCOVER open (default))`), and the captured subnet modes
— the tier every hosted endpoint falls through to when nothing more specific
matches. This is also the tier the engine room's per-surface verb writes, so a
node that has closed being found reads `closed (DISCOVER closed)` here.

## Governing this node's own traffic

<!-- [doc->REQ-ACL-INTRA-NODE-SELF] -->

A rule whose subject is **this node's own id** governs traffic authored *here*
— one agent on this machine reaching another. It is an ordinary rule at the
tiers you already have: no separate tier, no separate command, no schema of its
own.

You never type your own 64-character pubkey for it. `--node` takes three
spellings and they are tried in this order:

```text
spt node access deny --surfaces MSG --node self
spt endpoint access allow ling --surfaces MSG --node KITSUBITO
spt endpoint access allow ling --surfaces MSG --node 6e4abfa9aa08...
```

| spelling | means |
|---|---|
| a 64-hex pubkey | that node, exactly — tried first, so nothing can shadow it |
| `self` | this node; a reserved word, so a machine *labelled* `self` cannot take it |
| a node name | the node carrying that name, as `spt subnet status --nodes` shows it |

**The rule stores the hex, whichever you typed.** A name is presentation and a
lease that can move to another machine; the identity is the key. If a name is
carried by two machines the command refuses and names both, rather than picking
one and writing a rule about a node you did not mean. In every view the stored
subject renders like any other node subject — `node:<name>` when the name
resolves, the full hex otherwise — never as `self`: `self` is something you
type, not something a rule is.

Written at **node scope** it governs intra-node traffic for every endpoint
hosted here. Written on **one endpoint** it governs what other same-node
endpoints may do to that endpoint, and endpoint rules are consulted before
node-wide ones, so a per-endpoint allow re-opens one target through a node-wide
deny.

Two things it deliberately does not do. **Modes still abstain for local
traffic** — a `closed` posture is about who may reach this machine from
outside it, and closing the node has never meant stopping the agents on it
talking to each other; only a rule that *names* the local origin speaks about
local traffic. And a **correlated reply is still admitted** with no rule of its
own, own-node deny included, so an agent you messaged can always answer you.

**On a node with no identity yet, `--node self` refuses** rather than minting
one. Writing an access rule must not create the identity that rule is about.
Give the node an identity with `spt subnet create <name>` or
`spt subnet join` first.

> **Coverage, as it stands today.** A rule naming a specific *sender* endpoint
> is matched from the sender the delivery path supplies, and the notify path
> supplies it in a different shape from `spt send` and `spt ring` — so a
> sender-subject rule written for `ling` does not match traffic arriving by
> notify. Rules whose subject is a **node** (including `self`) are unaffected.
> Tracked as releases#215.

## Subnet modes: `spt subnet status`

A subnet is a *subject* of rules and a *source* of modes — never a
rule-holding target, so there is no `spt subnet access`. Its mode facts live
on its own view:

```text
spt subnet status bignet
```

states three things, in words even when absent:

- **declared by subnet** — the posture the subnet declares, as this node
  knows it;
- **captured here** — what this node actually enforces as its fallback
  (captured at join or at mint, changed only through the engine room's
  refresh);
- **PENDING** — a declared change this node has seen but not adopted, with
  when it was seen. Your posture does not change until the engine room
  adopts it.

<!-- [doc->REQ-SUBNET-STATUS-MODES-EVERY-VIEW] -->
The **bare** `spt subnet status` — no subnet named — carries the first two of
those facts for every subnet it lists, as a `MODE` and a `CAPTURED HERE`
column. Both views state an absence in words rather than leaving a cell
blank: a subnet that declares `closed` while this node captured nothing is
enforcing **open**, and that is exactly the state a membership-only table
would have shown as an ordinary healthy row.

<!-- [doc->REQ-SUBNET-MINT-CAPTURES-MODE] -->
### The node that mints a subnet captures its own declaration

Creating a subnet is the consenting act, so `spt subnet create` records the
mode it just declared into this node's access store — the same capture a
joining node performs at join. Without it a `--closed` subnet is asymmetric
by construction: every joiner enforces the posture while the minting node,
having nothing captured, falls through to implicit open on every surface.

A subnet minted by an **older build** captured nothing, and nothing
back-fills it silently: a subnet record carries no "minted here" marker, so
*declared present and captured absent* cannot be told apart from *joined
before subnet modes existed* — and retro-capturing the second would close a
running mesh without anyone asking for it. Heal such a node explicitly, from
its engine room:

```text
spt api access-refresh bignet
```

The create ceremony prints that remedy itself, so the state is discoverable
from the command that produces it.

## No remote rule-read

Rules are **node-sovereign**. There is no verb, on any surface, that reads
another node's access rules — viewing a node's rules means running the CLI
on that node. What a subnet *declares* travels as an advisory; what a node
*enforces* is its own, and only its own console will tell you.
