File created successfully at: C:\Users\decid\Documents\projects\spt-core\M5-PLAN.md (file state is current in your context — no need to Read it back)

# M5 Plan — shells, presence, deferred capabilities

> **Just-in-time, lightweight** — same pattern as `M0/M1/M2a/M2b/M3a/M3b/M3c/M4-PLAN.md`.
> Task layer authored in full 2026-06-04 at M5 start (see §Tasks).
> Branch: `dev-freeform`. Authoritative architecture: **CONTEXT §Shell model**,
> **CONTEXT §Consent & security gates**, **CONTEXT §Instances**,
> `docs/MANIFEST.md` §Shell adapters (`kind = "shell"`), ADR-0004 (daemon hosting),
> ADR-0009 (gate nesting), and `ROADMAP.md` §M5.

> **Upstream is done:** **M4 COMPLETE (2026-06-04)** — `spt-net` (WAN/pairing/registry),
> the multi-instance model (resting states, resources, home+fork), cross-node Psyche
> sync, WAN msg/remote-drive/xfer, the notif primitive, update peer-propagation, and the
> production trigger loops, proven on the real two-host rig (CI `26958175812`). M5 builds
> the *driven-surface* layer on those substrates: shells ride the broker process table +
> typed/binary payloads; presence rides the registry distribution; consent rides the
> security-material plumbing the access whitelist (REQ-SEC-1) already shaped.

## Scope decisions (user, 2026-06-04 — resolve the ROADMAP §M5 ↔ PRD/DEFERRED forks)

1. **Presence: resolution only.** Presence gossip (`last_active_node, last_active_endpoint, ts`
   subnet-wide) + a first-class most-recently-active resolution consumed by notif/update/
   consent/wake routing. The PresenceChannel *endpoint* (dispatch/bind/thread styles, the
   durable shell-agnostic thread) stays **deferred past v1** (DEFERRED.md stance holds).
2. **Consent: framework seam only.** The real grant store + interactive escalation ship;
   **both gated capabilities stay deferred** — remote command execution (REQ-REACH-2) *and*
   instantiate-anywhere. Consequence: the **remote fork arm** (cross-node `spt fork`
   placement) defers with instantiate-anywhere, despite the ROADMAP §M5 carried-seam
   listing — ROADMAP wording amended at D0.
3. **Shells: machinery + mock + one real shell.** Full shell hosting machinery proven by
   the generic mock shell adapter, **plus one minimal real shell — the OS-notification
   shell** (Windows toast / Linux `notify-send`) as the dogfood proof. This deliberately
   pulls one concrete shell out of the DEFERRED.md concrete-shells row; GameRobot-class
   surfaces stay deferred. Cross-node **spawn** stays deferred (rides instantiate-anywhere);
   cross-node **link** of an existing shell (owner active on another node) is in scope —
   it is part of the locked model ("an agent and its shell can link across nodes").
4. **Process:** plan authored directly from these decisions; further forks surfaced
   per-task as they arise.

## Goal

An agent embodies itself in driven surfaces: `spt shell spawn` mints an owner-exclusive
shell instance hosted by the daemon (broker-launched binary, three typed channels,
approval/cap gates), full sleep/wake from either end (owner cascade + wake-watcher), with
subnet presence resolution routing user-directed traffic to the right place — proven by a
real OS-notification shell rendering an agent's `spt notify` as a native toast on the
user's active node, and by the carried M4 seams going live (deferred-message resting
gate, remote suspend, relay rendezvous, notif-ack→apply orchestration, cross-runner CI).

## Scope

### In

- **Consent framework seam** (CONTEXT §Consent & security gates): grant store
  (`capability × agent × target-node`, target-enforced, subnet-settable, revocable, lives
  with security material near the trust store) + interactive escalation (consent prompt →
  most-recently-active session; allow-once / allow-always / deny) + pre-consent flag
  authoring (`can_shutdown`, `shell_wake_spawn_anywhere` as manifest/settings-authored
  grants). The two gated capability ids are **reserved, not implemented**.
- **Adapter registration lifecycle** (`REQ-INSTALL-4`): `spt adapter add <path|--github>`
  (manifest-first; install-is-first-update via the declared `[update]` avenue), soft
  `remove` + optional `uninstall` template; one command for `kind="harness"` and
  `kind="shell"`; the registered set feeds creation-time adapter selection, shell
  discovery, and the R-UPD-5 ripple.
- **Shell hosting machinery** (`REQ-EP-5` + new `REQ-SHELL-*`): shell perches
  (`perches/<owner>/shells/<adapter>-<n>/`), spawn-mints-instance vs relink/persistent/
  wake online switch, broker-launched binary + `api bind` local-link handshake, the three
  channels (command durable · text+file durable · sensory REST-only never-spooled),
  owner exclusivity, `require_approval` / `max_instances_per_owner` / `over_cap` gates
  (consuming the grant store), broadcast-vs-discovery, aliasing, context injection,
  `shell list/cmd/relink/teardown/rename`.
- **Shell sleep/wake**: offline-on-owner-offline, `persistent` auto-online,
  `wake_command` wake-watcher (exit-opcode supervision, exponential backoff + give-up),
  state-keyed wake resolution, `spt shutdown` owner cascade, `api owner-shutdown`
  (gated by `can_shutdown`).
- **Presence resolution** (new `REQ-PRES-1`): the presence datum gossiped subnet-wide
  (agent-interaction heartbeat; rides registry distribution) + one first-class
  most-recently-active resolution API replacing the per-feature precursors
  (notif first-fire, update consent, escalation, wake resolution).
- **Carried M4 seams**: `REQ-INST-6` deferred-message resting gate (seam documented in
  `spt-daemon::resting`), remote `spt suspend <id@node>` arm, relay rendezvous routing
  (D2f Q1 — consumer now exists), notif-ack → `apply_brain_only` apply orchestration
  (the production caller), cross-runner CI two-host job, KH 7.4
  (`REQ-HAZARD-DAEMON-SCHED-NONBLOCKING`) binding at the per-agent-runtime fan-out.
- **One real shell**: the **OS-notification shell** — minimal `kind="shell"` adapter +
  binary rendering a typed notify command natively; integrates the `notif_command`
  manifest seam (R-NOTIF-2's "render via the attached shell's template" M5 generalization).

### Out

- **PresenceChannel endpoint** (dispatch/bind/thread, durable cross-shell thread) —
  deferred past v1 (decision 1). The presence-resolution API is its future substrate.
- **Remote command execution** (`REQ-REACH-2`) + **instantiate-anywhere** — deferred
  (decision 2); the consent framework ships with their capability ids reserved. The
  **remote fork arm** and **cross-node shell spawn** defer with instantiate-anywhere;
  `shell_wake_spawn_anywhere` ships as grant *shape* only (wake resolution's
  no-reachable-instance branch refuses with a documented deferral).
- **Concrete shells beyond the OS-notification shell** (GameRobot, in-session-inject) —
  deferred (DEFERRED.md row amended, not removed).
- **Cross-user / per-(subnet,user)** generalizations — unchanged forward seams.
- **Shell-binary sandboxing** — unchanged accepted-risk stance (CONTEXT §binary-trust).

## Requirements

Existing inactive reqs that activate per task (rule 5): `REQ-EP-5`, `REQ-INSTALL-4`,
`REQ-INST-6`, `REQ-HAZARD-DAEMON-SCHED-NONBLOCKING` (7.4), `REQ-HAZARD-STALE-INDEX-LOCK`
(1.3 — binds when the daemon owns boot sweeps for the git-touching runtimes it hosts),
`REQ-HAZARD-STDIN-SESSION-ID` (2.2 — binds if/when the M5 adapter api surface grows a
stdin session-id path; re-assess at D3). New reqs to register first (rule 3) at D0:
`REQ-CONSENT-1` (grant store), `REQ-CONSENT-2` (interactive escalation),
`REQ-PRES-1` (presence gossip + MRA resolution), `REQ-SHELL-1` (shell hosting machinery:
channels/lifecycle/exclusivity), `REQ-SHELL-2` (sleep/wake watcher + cascade).
`REQ-INST-1`'s activation rationale (instantiate-anywhere) no longer fires at M5 —
update its rule-5 note at D0 to defer with the capability. `REQ-REACH-2` stays inactive.

## Clean-room posture

Nothing here has a sister-project analog to copy (the sister had no shells, no presence,
no consent store) — clean-room throughout. The OS-notification shell binary is original;
its OS integration uses stock platform surfaces (Windows toast API / `notify-send`).

## Tasks (expanded 2026-06-04 at M5 start)

> Sub-tasks are the executable unit; each lands with its tag in the same commit
> (TRACEABILITY rule 1). Activate listed reqs **at the sub-task that delivers them**
> (rule 5). Conventions carried: no `cargo fmt`; `traceable-reqs check` from repo root
> before declaring done; clippy `-D warnings` on Linux CI is a real gate; push each
> slice → watch the sha-pinned FINAL conclusion.

**Sequencing.** D0 is the doc/registry alignment. D1 (consent) and D2 (adapter
registration) are the floor — D3's gates consume D1's grant store and D3's shell
adapters must be registerable via D2. D4 (sleep/wake) sits on D3. D5 (resting arms) is
independent after M4 and can interleave. D6 (presence) upgrades routing consumers built
through D1/D4 — build the precursor-consuming API late enough to swap call sites once.
D7 (carried infra) is parallelizable; D7c (CI job) early pays off for everything after.
D8 (real shell) needs D2+D3+D4+D6. D9 is the closeout. **Each D-task is itself a
milestone-sized body** — expect a `/self-clear` + commune between most; commit atomically
per sub-task.

### D0 — Scope alignment + requirement registry prep
- **D0a** Amend `ROADMAP.md` §M5 to the decided scope (presence resolution only; consent
  framework seam only; remote-fork-arm + cross-node-spawn defer with instantiate-anywhere;
  mock + OS-notification shell). Amend `docs/DEFERRED.md`: presence-gossip row leaves the
  table (ships M5); concrete-shells row notes the OS-notification carve-out;
  instantiate-anywhere / remote-exec rows note the framework landed M5, capabilities still
  parked. CONTEXT touch-ups only where wording drifts from the above.
- **D0b** Register the new reqs (rule 3, all `required_stages = []` until their sub-task):
  `REQ-CONSENT-1`, `REQ-CONSENT-2`, `REQ-PRES-1`, `REQ-SHELL-1`, `REQ-SHELL-2`. Update
  `REQ-INST-1`'s rule-5 rationale (defers with instantiate-anywhere, not M5).
- **Acceptance:** `traceable-reqs check` green; docs consistent with the scope decisions.

### D1 — Consent framework seam (grant store + escalation)
- **D1a** Grant store: `capability × subject-agent × target-node` rows; **enforced at the
  target node**; settable subnet-wide (replicates as security material near the trust
  store — the REQ-SEC-1 plumbing); revocable. Capability ids `remote-exec` /
  `instantiate-anywhere` reserved-but-refusing; real v1 consumers are the D3 shell gates.
  · **REQ-CONSENT-1**
- **D1b** Interactive escalation: ungated action → consent prompt routed to the user's
  most-recently-active session (the M4 notif-producer path; upgraded to REQ-PRES-1 at
  D6b); **allow-once / allow-always (writes a grant) / deny**. · **REQ-CONSENT-2**
- **D1c** Pre-consent flag authoring: manifest `can_shutdown` + endpoint settings
  `shell_wake_spawn_anywhere` author grants through the same store (different authoring
  path, same model). Flag *shape* only for spawn-anywhere (behavior deferred).
- **Acceptance:** grant present → allowed; absent → escalation; deny blocks; allow-always
  persists + replicates; revocation propagates; gate nesting order (visibility →
  whitelist → grants, ADR-0009) holds in tests.

### D2 — Adapter registration lifecycle
- **D2a** `spt adapter add <path>` / `--github <user/repo>`: manifest-first validation,
  record under `{SPT_HOME}/…/adapters/` (copy for `file_pull`, pointer for `delegated`);
  **install is the first update** through the declared `[update]` avenue; one command for
  both adapter kinds. · **REQ-INSTALL-4 (impl/unit)**
- **D2b** `spt adapter remove`: soft-deregister (hidden from new-creation; live instances
  keep running) + optional manifest `uninstall` template once quiesced (or `--force`).
- **D2c** Wire the registered set as the source for: creation-time adapter selection
  (auto-if-one/ask-if-many), shell discovery (D3d), and the R-UPD-5 ripple set.
- **Acceptance:** add → validate → use → remove lifecycle E2E with mock harness + mock
  shell manifests; a removed adapter's live instance survives.

### D3 — Shell hosting machinery
- **D3a** Shell perch layout (`perches/<owner>/shells/<adapter>-<n>/`; `info.json` carries
  `type=Shell, owner, adapter_name, status, alias?` only — capabilities resolve from the
  manifest). `spt shell spawn <adapter> [--alias]` **mints** a new instance (canonical id
  `<adapter>-<n>`); `relink` / `teardown` / `list` / `cmd` / `rename`. Spawn:create ::
  relink/persistent/wake:online. · **REQ-EP-5 (impl/unit begins)**
- **D3b** Broker-launched binary (never user-launched) + `api bind` (type=Shell) with the
  **local-link handshake** (link token + channel encryption on every message); the three
  command-delivery modes (HTTP / stdin-via-broker / child-relay `api poll`).
- **D3c** The three content channels over the typed/binary substrate: **command**
  (agent→shell, durable/spooled), **text+file** (2-way durable; transfer
  progress-queryable), **sensory** (`api emit --type <t>`, shell→agent, **REST-only,
  never spooled**, dropped unless the owner is live). · **REQ-SHELL-1**
- **D3d** Gates + discovery: `require_approval` (`none|remembered|always`; manifest floor,
  node/endpoint may tighten) and `max_instances_per_owner` + `over_cap` consume the D1
  grant store (`spawn-shell × agent × (node, shell-adapter)` grants); broadcast policy
  (`subnet|same-node|none`) governs **discovery only**; discovery scope = own instances +
  instantiable adapters; owner exclusivity enforced; shell awareness injected into the
  agent's context. · **REQ-EP-5 (gates + discovery)**
- **D3e** Per-agent-runtime fan-out scheduling: the daemon now hosts many per-agent
  loops/processes (shells, watchers, pulse/Psyche) — each agent's bounded blocking work
  off the shared scheduler; one slow agent must not stall another's tick.
  · **REQ-HAZARD-DAEMON-SCHED-NONBLOCKING (7.4)**; boot-time stale-lock sweep for the
  git-touching runtimes the daemon hosts · **REQ-HAZARD-STALE-INDEX-LOCK (1.3)**;
  re-assess **REQ-HAZARD-STDIN-SESSION-ID (2.2)** — activate iff a stdin session-id api
  path now exists, else leave with an updated rationale.
- **Acceptance:** mock-shell E2E spawn → bind+handshake → command → sensory → teardown;
  non-owner refused; `remembered`/`always` approval + `over_cap` enforced through real
  grants; alias addressing works; `--no-default-features` lib build stays clean.

### D4 — Shell sleep/wake + owner cascade
- **D4a** Lifecycle: link-break **always** closes broker link + binary (manifest pre-close
  instruction + termination timeout); **ephemeral** ⇒ full teardown + history erase;
  **persistent** ⇒ perch offline, re-linkable (binary re-spawned on relink);
  offline-on-owner-offline; `persistent` auto-onlines with the owner. · **REQ-SHELL-2 begins**
- **D4b** `wake_command` wake-watcher: runs **while offline** (mutually exclusive with the
  binary), on the shell's node; **exit(wake-opcode)** → wake resolution; any other exit →
  respawn with exponential backoff + eventual give-up; one watcher per offline instance.
- **D4c** State-keyed wake resolution: owner dormant → online the shell; suspended →
  revive owner, then online; active elsewhere → attach/online; **no reachable instance →
  no-op with a documented refusal** (the `shell_wake_spawn_anywhere` fresh-spawn branch
  stays deferred with instantiate-anywhere; the D1c grant shape is its seam).
  · **REQ-SHELL-2**
- **D4d** `spt shutdown` (agent gracefully suspends its own endpoint: suspend boundary
  signoff fires, persistent shells cascade offline) + `api owner-shutdown` (shell
  suspends its owner directly, gated by the `can_shutdown` pre-consent grant).
- **Acceptance:** full sleep/wake cycle driven from both ends (owner shutdown → shell
  offline → watcher up → wake opcode → owner revived → shell online); crash-exit
  backoff observed; ephemeral-vs-persistent divergence proven.

### D5 — Resting-state arms (carried M4 seams)
- **D5a** Deferred-message resting gate: deferred (spool-only) rows **hold** while the
  instance is dormant/suspended, **release on the wake edge** (the documented
  `spt-daemon::resting` seam; extends KH 1.4/4.4 with the instance-state gate).
  · **REQ-INST-6**
- **D5b** Remote `spt suspend <id[@node]>` + remote `spt wake` completeness (suspend/wake
  any of your instances from any node — ungated, same trust class as remote-drive).
- **D5c** Remote fork arm: **document the deferral** (rides instantiate-anywhere,
  decision 2) — no build.
- **Acceptance:** deferred message held while resting, delivered exactly once after wake;
  remote suspend/wake proven cross-node (loopback int + rig leg at D9).

### D6 — Presence resolution
- **D6a** Presence datum `(last_active_node, last_active_endpoint, ts)` gossiped
  subnet-wide — agent-interaction heartbeat as the signal (the `last_active_ms` recency
  stamp generalized), riding the registry/notif distribution; visibility-gated like
  registry data. · **REQ-PRES-1 begins**
- **D6b** One first-class **most-recently-active resolution API**; swap the per-feature
  precursors to it: notif first-fire, update-consent delivery, consent escalation (D1b),
  wake resolution (D4c). · **REQ-PRES-1**
- **D6c** Document the PresenceChannel endpoint as the deferred consumer (dispatch/bind/
  thread styles build on this datum + the broker PresenceLog seam, REQ-EP-4) — seam note,
  no build.
- **Acceptance:** activity shifting nodes redirects notif/consent delivery cross-node;
  loopback int + rig leg at D9.

### D7 — Carried infra seams
- **D7a** Relay rendezvous routing (M4-D2f Q1): route the pre-trust pairing rendezvous
  via the relay using the existing `H(subnet-name ‖ TOTP-epoch)` token derivation
  (`spt-net::pairing::rendezvous`) — pairing beyond LAN/known-addr; the deferral's
  "no consumer until shells" trigger has fired.
- **D7b** Notif-ack → `apply_brain_only` orchestration: consent-notif acknowledgment
  drives the staged update apply — the **production caller** for `apply_brain_only`
  (closes the M4-D9 "no-production-caller" annotation).
- **D7c** Cross-runner CI two-host job: automate the `[twohost]` ladder as a cross-runner
  workflow (hfenduleam + gravity runners coordinated in one run; runbook stays the manual
  fallback). Gotchas #7/#13/#17 discipline baked in.
- **Acceptance:** relay-routed pairing E2E; a real update applies off a notif ack;
  the cross-runner job goes green on the rig unattended.

### D8 — The real shell: OS-notification shell (dogfood)
- **D8a** Minimal `kind="shell"` adapter + original binary: renders a typed notify
  command as a native OS notification (Windows toast / Linux `notify-send`);
  `broadcast = "same-node"`, `persistent = true`, sane `require_approval` default;
  registered via D2, hosted via D3/D4.
- **D8b** `notif_command` integration (R-NOTIF-2's M5 generalization): when presence
  resolves the user to an endpoint with this shell attached, a subnet notif renders via
  the shell's template — `spt notify` → native toast on the user's active node.
- **D8c** Cross-node owner↔shell **link**: owner active on node B relinks its persistent
  shell living on node A (commands ride Iroh; spawn stays same-node per decision 2).
- **Acceptance:** the dogfood demo — an agent's `spt notify` surfaces as a real OS toast
  on whichever rig host the user last touched; cross-node relink proven.

### D9 — Activation sweep + two-host E2E + closeout
- **D9a** Rig int legs (env-gated `[twohost]`, runbook-extended; or via the D7c job):
  remote suspend/wake · presence-shift redirect · cross-node shell relink + drive ·
  notif→toast. Tag `[int->REQ-*]` on the gated tests.
- **D9b** Sweep: every M5 req's `required_stages` satisfied or rule-5-deferred with
  rationale; `traceable-reqs check` green; amend ROADMAP (M5 delivered) + CONTEXT +
  DEFERRED; CI matrix (ubuntu + windows + traceability + cross-runner) green FINAL.
- **Acceptance:** scope decisions 1–3 demonstrably met; closeout recorded.

## Requirement → task coverage map

| Task | Activates |
|------|-----------|
| D0 | registers REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 (inactive); REQ-INST-1 rationale update |
| D1 | REQ-CONSENT-1, REQ-CONSENT-2 |
| D2 | REQ-INSTALL-4 |
| D3 | REQ-EP-5, REQ-SHELL-1, REQ-HAZARD-DAEMON-SCHED-NONBLOCKING, REQ-HAZARD-STALE-INDEX-LOCK (+ REQ-HAZARD-STDIN-SESSION-ID iff path exists) |
| D4 | REQ-SHELL-2 |
| D5 | REQ-INST-6 |
| D6 | REQ-PRES-1 |
| D7 | (no new activations — closes M4 deferral annotations) |
| D8 | REQ-EP-5/SHELL-1/2 + REQ-NOTIF-2 `int` evidence via the real shell |
| D9 | int sweep + closeout |

**Stays deferred (out of M5):** REQ-REACH-2, instantiate-anywhere (+ remote fork arm,
cross-node shell spawn, `shell_wake_spawn_anywhere` behavior), the PresenceChannel
endpoint (REQ-EP-4 stays at its M4 broker-seam stage), GameRobot-class shells,
cross-user generalizations, REQ-MIGRATE-1.


PowerShell

traceable-reqs check; echo "exit=$LASTEXITCODE"

Run traceability gate from repo root


129 requirements: 129 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
  [OK] REQ-API-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
  [OK] REQ-API-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       commune/signoff are file-drops, not commands
  [OK] REQ-ARCH-1  required: [impl]  stages: -doc +impl -unit -int
       Many small acyclically-layered crates
  [OK] REQ-ARCH-2  required: [impl]  stages: -doc +impl -unit -int
       Public SDK surface is spt-proto, spt-runtime, spt-msg
  [OK] REQ-ARCH-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Wire-protocol version independent of crate semver, N-1 compat window
  [OK] REQ-ARCH-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Copy-verbatim the commodity layer from the sister project
  [OK] REQ-DAEMON-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       One per-machine spt-daemon owning all per-machine state
  [OK] REQ-DAEMON-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Broker/brain split for seamless self-update
  [OK] REQ-DAEMON-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Any api invocation auto-starts the daemon if absent
  [OK] REQ-DAEMON-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Honor every KNOWN-HAZARDS invariant
  [OK] REQ-DOCS-1  required: []  stages: -doc -impl -unit -int
       Dual-audience docs (human + AI dev-agent), markdown once / two depths
  [OK] REQ-DOCS-2  required: []  stages: -doc -impl -unit -int
       Sub-10-minute runnable killer quickstart per audience
  [OK] REQ-DOCS-3  required: []  stages: -doc -impl -unit -int
       Diátaxis structure; one canonical way to do X
  [OK] REQ-DOCS-4  required: []  stages: -doc -impl -unit -int
       Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)
  [OK] REQ-DOCS-5  required: []  stages: -doc -impl -unit -int
       Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked
  [OK] REQ-EP-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Day-one endpoint types; open type system
  [OK] REQ-EP-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Agent endpoints vs Shells distinction in the type model
  [OK] REQ-EP-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Messaging payloads carry typed operation commands + file blobs
  [OK] REQ-EP-4  required: [impl, unit]  stages: -doc +impl +unit -int
       PresenceChannel broker endpoint (seam day-one)
  [OK] REQ-EP-5  required: []  stages: -doc -impl -unit -int
       Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope
  [OK] REQ-FRONT-1  required: []  stages: -doc -impl -unit -int
       Day-one launcher/manager frontend (list/launch/attach/init)
  [OK] REQ-HAZARD-CASCADE-WIPE-GUARD  required: [impl, unit]  stages: -doc +impl +unit -int
       No hard-delete of a parent hosting non-empty children (6.3)
  [OK] REQ-HAZARD-CONFLICT-BOTH-PRESERVED  required: [impl, unit]  stages: -doc +impl +unit -int
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
  [OK] REQ-HAZARD-CONPTY-DSR  required: [impl, unit]  stages: -doc +impl +unit +int
       ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
  [OK] REQ-HAZARD-DAEMON-HOSTED-LIVENESS  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)
  [OK] REQ-HAZARD-DAEMON-SCHED-NONBLOCKING  required: []  stages: -doc -impl -unit -int
       Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)
  [OK] REQ-HAZARD-DEFERRED-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred spool rows excluded from the event-stream drain (1.4)
  [OK] REQ-HAZARD-DEFERRED-SURVIVE-DRAIN  required: [impl, unit]  stages: -doc +impl +unit -int
       Deferred rows survive poll drain (4.4)
  [OK] REQ-HAZARD-DIRECT-WRITE-PRECEDENCE  required: [impl, unit]  stages: -doc +impl +unit -int
       Direct-write precedence marker (with node id) guards stale overwrite (6.5)
  [OK] REQ-HAZARD-DROP-FILE-SINGLE-WRITER  required: [impl, unit]  stages: -doc +impl +unit -int
       Drop files are daemon-owned single-writer (6.4)
  [OK] REQ-HAZARD-EBUSY-RENAME  required: [impl, unit]  stages: -doc +impl +unit -int
       tmp-write + atomic-rename + retry on Windows EBUSY (5.2)
  [OK] REQ-HAZARD-ECHO-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)
  [OK] REQ-HAZARD-ENVELOPE-DECODE-ORDER  required: [impl, unit]  stages: -doc +impl +unit -int
       Envelope decode order, ampersand decoded last (4.1)
  [OK] REQ-HAZARD-ENVELOPE-PARSER-SAFE  required: [impl, unit]  stages: -doc +impl +unit -int
       Two-slice envelope parser is panic-free and tolerant (4.2)
  [OK] REQ-HAZARD-EPHEMERAL-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
       Ephemeral perch cleanup on every ring exit path (3.1)
  [OK] REQ-HAZARD-EVENTPART-REASSEMBLY  required: [impl, unit]  stages: -doc +impl +unit -int
       EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently
  [OK] REQ-HAZARD-GEN-START-NOW  required: [impl, int]  stages: -doc +impl -unit +int
       gen_start = now() on cold-start and handoff (2.4)
  [OK] REQ-HAZARD-GRACE-BEFORE-SIGNOFF  required: [impl, unit]  stages: -doc +impl +unit -int
       Grace-period wait completes before composing INIT_SIGNOFF (1.1)
  [OK] REQ-HAZARD-HANDOFF-ARGV-COMPAT  required: [impl, unit]  stages: -doc +impl +unit -int
       Broker/brain IPC + handoff argv version-tolerant (2.3)
  [OK] REQ-HAZARD-ID-CHARSET  required: [impl, unit]  stages: +doc +impl +unit -int
       Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)
  [OK] REQ-HAZARD-INBOX-NO-DOUBLE  required: [impl, unit]  stages: -doc +impl +unit -int
       No double-delivery via legacy inbox (4.5)
  [OK] REQ-HAZARD-INFO-JSON-TORN-READ  required: [impl, unit]  stages: -doc +impl +unit -int
       State-file reads tolerate concurrent writes (1.2)
  [OK] REQ-HAZARD-LOCAL-API-AUTH  required: [impl, unit]  stages: -doc +impl +unit -int
       Every local `api` mutation authenticated to an endpoint/session (codex #13)
  [OK] REQ-HAZARD-PAIR-RATE-LIMIT  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)
  [OK] REQ-HAZARD-PAIR-SEED-ROTATION  required: [impl, unit]  stages: -doc +impl +unit -int
       Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)
  [OK] REQ-HAZARD-PAIR-TRANSCRIPT-BIND  required: [impl, unit]  stages: -doc +impl +unit -int
       Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)
  [OK] REQ-HAZARD-PARENT-PID-PREFER  required: []  stages: -doc -impl -unit -int
       Prefer stable parent PID / broker handle over ephemeral PID (2.1)
  [OK] REQ-HAZARD-PSYCHE-OUTBOUND-PROXY  required: [impl, unit]  stages: -doc +impl +unit -int
       Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)
  [OK] REQ-HAZARD-REGISTRY-CONCURRENT  required: [impl, unit]  stages: +doc +impl +unit -int
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
  [OK] REQ-HAZARD-REGISTRY-DIR-CREATE  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)
  [OK] REQ-HAZARD-REGISTRY-EPOCH-LEASE  required: [impl, unit]  stages: +doc +impl +unit -int
       Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)
  [OK] REQ-HAZARD-REGISTRY-STALE-CLEAN  required: [impl, unit]  stages: -doc +impl +unit -int
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-HAZARD-RESTART-IDEMPOTENT  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)
  [OK] REQ-HAZARD-SINGLE-PATH-SOURCE  required: [impl, unit]  stages: -doc +impl +unit -int
       Single path/registry source of truth; no layout ambiguity (6.1)
  [OK] REQ-HAZARD-SOFT-CLEANUP  required: [impl, unit]  stages: -doc +impl +unit -int
       Soft-cleanup preserves state, removes only the ready marker (6.2)
  [OK] REQ-HAZARD-STALE-INDEX-LOCK  required: []  stages: -doc -impl -unit -int
       Sweep stale lockfiles on daemon boot (1.3)
  [OK] REQ-HAZARD-STALE-SIGNOFF-SENTINEL  required: [impl, unit]  stages: -doc +impl +unit -int
       Stale signoff sentinel does not kill a fresh start (3.2)
  [OK] REQ-HAZARD-STDIN-SESSION-ID  required: []  stages: -doc -impl -unit -int
       Stdin session_id precedence over env (2.2)
  [OK] REQ-HAZARD-SUBPROCESS-TIMEOUT  required: [impl, unit]  stages: -doc +impl +unit -int
       Every harness/git subprocess has a timeout (5.3)
  [OK] REQ-HAZARD-UNC-PATH-STRIP  required: [impl, unit]  stages: -doc +impl +unit -int
       Strip Windows UNC prefix on serialized paths (5.4)
  [OK] REQ-HAZARD-UPDATE-ROLLBACK  required: [impl, unit]  stages: -doc +impl +unit -int
       Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)
  [OK] REQ-HAZARD-WAN-ORIGIN-AUTH  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)
  [OK] REQ-HAZARD-WINDOWS-PID-RECYCLE  required: [impl, unit]  stages: -doc +impl +unit -int
       Windows PID-recycling false positives guarded (5.1)
  [OK] REQ-HAZARD-WORKER-PATH  required: [impl, unit]  stages: -doc +impl +unit -int
       Single source of truth for Worker/Psyche perch location (1.5)
  [OK] REQ-INFRA-1  required: []  stages: -doc -impl -unit -int
       GitHub issue tracking for v1; tangled.org as migration target
  [OK] REQ-INST-1  required: []  stages: -doc -impl -unit -int
       endpoint ID vs instance split (adapter-agnostic ID)
  [OK] REQ-INST-10  required: [impl, unit]  stages: -doc +impl +unit -int
       Qualified addressing [subnet:]id[@node] + ambiguity forces qualification
  [OK] REQ-INST-11  required: [impl, unit]  stages: -doc +impl +unit -int
       spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)
  [OK] REQ-INST-12  required: [impl, unit]  stages: -doc +impl +unit -int
       Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync
  [OK] REQ-INST-13  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet-exclusive sync + per-endpoint subnet-membership list
  [OK] REQ-INST-14  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated
  [OK] REQ-INST-15  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)
  [OK] REQ-INST-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-node files, synced Psyche mind
  [OK] REQ-INST-3  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Dormant (warm) / suspended (cold) resting states
  [OK] REQ-INST-4  required: [impl, unit]  stages: -doc +impl +unit -int
       active to dormant/suspended fires a transition echo commune
  [OK] REQ-INST-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-tier context sync (live to all, project to same-project)
  [OK] REQ-INST-6  required: []  stages: -doc -impl -unit -int
       Deferred messages not delivered to dormant/suspended instances
  [OK] REQ-INST-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Subnet registry + bare-id resolution policy
  [OK] REQ-INST-8  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Remote-control mode distinct from local operation
  [OK] REQ-INST-9  required: [impl, unit]  stages: -doc +impl +unit -int
       Multi-subnet membership (same-user N subnets; cross-user seam)
  [OK] REQ-INSTALL-1  required: []  stages: -doc -impl -unit -int
       Two install paths; signed one-line script; OS-service registration
  [OK] REQ-INSTALL-2  required: []  stages: -doc -impl -unit -int
       Marketplace-repackaging-friendly install
  [OK] REQ-INSTALL-3  required: []  stages: -doc -impl -unit -int
       Idempotent + interactive-optional first run
  [OK] REQ-INSTALL-4  required: []  stages: -doc -impl -unit -int
       Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over
  [OK] REQ-MANIFEST-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Per-adapter manifest with adapter_name and min_spt_core_version
  [OK] REQ-MIGRATE-1  required: []  stages: -doc -impl -unit -int
       Auto-detect and migrate a legacy claude_skill_owl install
  [OK] REQ-MSG-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)
  [OK] REQ-MSG-2  required: [impl, unit]  stages: -doc +impl +unit -int
       spt binary CLI surface: send/ring/ready/poll/list/stop/whoami, stable arg shapes + exit codes
  [OK] REQ-MSG-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown
  [OK] REQ-NET-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       WAN messaging first-class, behind default-on net feature flag
  [OK] REQ-NET-2  required: [impl]  stages: -doc +impl +unit -int
       n0 relay default + self-host knob + plain-language disclosure
  [OK] REQ-NET-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Cross-node Psyche sync over P2P replaces gh-repo-sync
  [OK] REQ-NODE-IDENTITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex
  [OK] REQ-NOTIF-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts
  [OK] REQ-NOTIF-2  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)
  [OK] REQ-PAIR-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       TOTP-seeded SPAKE2 pairing
  [OK] REQ-PAIR-2  required: [impl, unit]  stages: -doc +impl +unit -int
       Local trust store with TOFU + warn-on-change
  [OK] REQ-PAIR-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Fetch current pairing code from any paired node
  [OK] REQ-PAIR-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Subnet naming on first pairing
  [OK] REQ-PAIR-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing
  [OK] REQ-PAIR-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)
  [OK] REQ-PAIR-7  required: []  stages: -doc -impl -unit -int
       Subnet icon (inline image metadata, GUI-only consumer)
  [OK] REQ-REACH-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Off-node remote-drive detection + file transfer
  [OK] REQ-REACH-2  required: []  stages: -doc -impl -unit -int
       Remote command execution (deferred, consent-gated)
  [OK] REQ-SEAM-ACTIVITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Activity/idle reported via api sentinels, not PTY quiescence
  [OK] REQ-SEAM-CAPABILITY  required: [impl, unit]  stages: -doc +impl +unit -int
       Hostable endpoint-types capability declaration
  [OK] REQ-SEAM-HISTORY  required: [impl, unit, int]  stages: -doc +impl +unit +int
       History subsystem (fetcher / locate-normalize / native store)
  [OK] REQ-SEAM-INJECT  required: [impl, unit]  stages: -doc +impl +unit -int
       inject-input methods configurable per activity-state
  [OK] REQ-SEAM-POSTSPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
       post-spawn / api bind seam with boot nonce
  [OK] REQ-SEAM-PSYCHE  required: [impl, unit, int]  stages: -doc +impl +unit +int
       spawn-psyche seam (fresh + resume templates)
  [OK] REQ-SEAM-RESUME  required: [impl, unit]  stages: -doc +impl +unit -int
       resume-session seam (fresh-with-preload / continue-existing)
  [OK] REQ-SEAM-SPAWN  required: [impl, unit]  stages: -doc +impl +unit -int
       spawn-session seam
  [OK] REQ-SEAM-UPDATE  required: [impl, unit]  stages: -doc +impl +unit -int
       Adapter-update avenue (file-pull / delegated command)
  [OK] REQ-SEC-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants
  [OK] REQ-START-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Adapters never resolve SPT_HOME; binary on PATH; api bridging only
  [OK] REQ-START-2  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Harness-hosted startup: api seed then listen
  [OK] REQ-START-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       spt-hosted startup: spawn-session then api bind (no file)
  [OK] REQ-START-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Adapter-injected env aliases (SPT/OWL/LIVE)
  [OK] REQ-STORE-1  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
exit=0


Check green. Commit + push plan, watch CI.


git add M5-PLAN.md && git commit -m @'
docs(plan): author M5-PLAN — scope locked at M5 start via user forks

Three forks resolved with user (2026-06-04): presence resolution only
(PresenceChannel endpoint stays deferred); consent framework seam only
(remote-exec + instantiate-anywhere + remote-fork-arm stay deferred);
shell machinery + mock + one real shell (OS-notification dogfood).
D0-D9 task layer authored; new reqs (REQ-CONSENT-1/2, REQ-PRES-1,
REQ-SHELL-1/2) register at D0 per rule 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@ && git push

Commit and push M5-PLAN.md


warning: in the working copy of 'M5-PLAN.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform a6f5cab] docs(plan): author M5-PLAN — scope locked at M5 start via user forks
 1 file changed, 309 insertions(+)
 create mode 100644 M5-PLAN.md
To https://github.com/SaberMage/spt-core.git
   becb6ec..a6f5cab  dev-freeform -> dev-freeform


$sha = "a6f5cab"; for ($i=0; $i -lt 40; $i++) { $r = gh run list --commit (git rev-parse $sha) --json status,conclusion,workflowName | ConvertFrom-Json; if ($r.Count -gt 0 -and ($r | Where-Object { $_.status -ne "completed" }).Count -eq 0) { $r | ForEach-Object { "$($_.workflowName): $($_.conclusion)" }; break }; Start-Sleep -Seconds 20 }

Watch CI runs for pushed sha until FINAL conclusion


Command running in background with ID: bi8iravrn. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bi8iravrn.output. You will be notified when it completes. To check interim output, use Read on that file path.


Edit

- **M5 — Shells, presence, deferred capabilities** (NEXT). Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam. Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, remote fork arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.

- **M5 — Shells, presence, deferred capabilities** (NEXT — scope locked with user 2026-06-04, see [`M5-PLAN.md`](./M5-PLAN.md)). Shell hosting machinery + sleep/wake proven by the mock shell adapter **plus one real shell** (the OS-notification dogfood shell); adapter registration lifecycle (`spt adapter add/remove`); **presence resolution only** (presence-datum gossip + a first-class most-recently-active resolution API — the PresenceChannel *endpoint* stays deferred past v1); the **consent framework seam only** (grant store + interactive escalation + pre-consent flags — remote-exec, instantiate-anywhere, and with them the remote-fork arm and cross-node shell *spawn*, stay deferred; cross-node shell *link* is in). Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.


The file C:\Users\decid\Documents\projects\spt-core\ROADMAP.md has been updated successfully. (file state is current in your context — no need to Read it back)

- **M3c — signed self-update. ✅ delivered (2026-06-03).** The gated, signed, ripple-capable self-update engine (no new crate — it lives in `spt-daemon`, signing primitives in `release.rs`): the **update-class taxonomy** (brain-only / broker-compatible / broker-breaking, ADR-0004 §A) with the **brain-only zero-interruption swap** that drives the M3b handoff substrate (snapshot→drop→re-attach) so a live PTY child + its output stream survive the logic swap **untouched** (REQ-UPD-3, proven E2E `tests/brain_swap.rs`) · **verify-before-handoff** (Ed25519 over signed release metadata + SHA-256 artifact binding; `plan_verified` is the front door — no unverified binary reaches a handoff, REQ-UPD-2) · **rollback hardening** (monotonic version, metadata expiry, channel pinning, key revocation — REQ-HAZARD-UPDATE-ROLLBACK) · **consent gating** (default-gated, delivered to the most-recently-active live session via the new `info.json` `last_active_ms` recency stamp; opt-in full-auto — REQ-UPD-4) · **adapter ripple-update** (self-then-adapters via each manifest `[update]` avenue, with adapter content signing against the adapter's own key for `file_pull` / a `self_verifies` attest for opaque `delegated` updaters — REQ-UPD-5, REQ-SEAM-UPDATE). **Peer-propagation transport (REQ-UPD-1) defers to M4** — the engine ships with self-fetch/out-of-band delivery into the same seam. All M3c reqs activated + green. Task breakdown: [`M3c-PLAN.md`](./M3c-PLAN.md).

- **M3 COMPLETE** (M3a + M3b + M3c all ✅, 2026-06-03): the real ADR-0004 architecture is live — native PTY hosting, the consolidated broker/brain daemon, and seamless signed self-update. Next milestone: M4.

- **M4 — Networking + instances. ✅ delivered (2026-06-04).** The eighth crate `spt-net` (Iroh WAN endpoint bound to the node's own Ed25519 identity, mDNS LAN discovery, TOTP-SPAKE2 pairing over a dedicated pre-trust ALPN with transcript binding / rate limiting / seed rotation+transfer, subnet registry + NDJSON replication under the per-node epoch lease) · the multi-instance model (registry rows + bare-id resolution refuse-and-qualify + visibility/sync-membership gates + rename/collision + **resting-state machine** dormant/suspended with measured warm/cold policy [`docs/DORMANCY-BUDGET.md`, red-team #9 closed] + **resource advertisement** REQ-INST-14 + **immutable home subnet & `spt fork`** REQ-INST-15, ADR-0010) · **cross-node Psyche sync** (git-native bundles over broker QUIC streams, two-tier scope gates, vector-precedence merge with conflicts surfaced + Psyche-reconciled, ADR-0013; **the gh-repo interim is retired**) · WAN messaging + remote-drive attach + chunked file transfer + per-endpoint access whitelists · the subnet **notification primitive** (replicated spool, dismiss-replication, boundary resurfacing; update consent + escalations refactored as producers) · the **update peer-propagation transport** into the M3c engine seam (offer-then-fetch, verify-before-stage per node, consent-notified — REQ-UPD-1) · production trigger loops (peer pump + inbound dispatcher — the subnet self-drives). **The real proof ran on the real rig: `[twohost]` CI run `26958175812` — pair → register → cross-node message → remote-drive (A typed into B's live PTY and read the echo) → Psyche sync both directions → update self-heal → notif dismiss-replication, HFENDULEAM (Windows) ↔ gravity-linux, whole ladder ~8 s** (`docs/TWO-HOST-RUNBOOK.md` + evidence appendix; `tests/twohost.rs` carries the nine activated `int` stages). Task breakdowns: [`M4-PLAN.md`](./M4-PLAN.md) + per-task JIT plans (D1–D9).

-- **M5 — Shells, presence, deferred capabilities** (NEXT). Shell adapters + sleep/wake, PresenceChannel, and the consent-gated capabilities (remote-exec, instantiate-anywhere) landing into the v1 consent seam. Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, remote fork arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.

+- **M5 — Shells, presence, deferred capabilities** (NEXT — scope locked with user 2026-06-04, see [`M5-PLAN.md`](./M5-PLAN.md)). Shell hosting machinery + sleep/wake proven by the mock shell adapter **plus one real shell** (the OS-notification dogfood shell); adapter registration lifecycle (`spt adapter add/remove`); **presence resolution only** (presence-datum gossip + a first-class most-recently-active resolution API — the PresenceChannel *endpoint* stays deferred past v1); the **consent framework seam only** (grant store + interactive escalation + pre-consent flags — remote-exec, instantiate-anywhere, and with them the remote-fork arm and cross-node shell *spawn*, stay deferred; cross-node shell *link* is in). Carried-in seams from M4: REQ-INST-6 (deferred-message resting gate), remote `spt suspend id@node` arm, relay rendezvous routing (D2f Q1), notif-ack → `apply_brain_only` apply orchestration, cross-runner CI two-host job, KH 7.4 binds at per-agent-runtime fan-out.

## De-risk spikes (do early — these can invalidate the architecture)


Now DEFERRED.md rows:


| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |

| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04) | Once Shells exist and the subnet registry is stable |


The file C:\Users\decid\Documents\projects\spt-core\docs\DEFERRED.md has been updated successfully. (file state is current in your context — no need to Read it back)

| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04) | Once Shells exist and the subnet registry is stable |

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |
| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates — grant store + interactive escalation); v1 ships the framework seam, this capability lands into it later. (The `shell_wake_spawn_anywhere` flag is a pre-consent variant.) | After remote-drive ships; capability drops into the v1 consent seam |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |

-| Concrete Shell types (GameRobot, OS-notification, in-session-inject) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |

-| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1 | Once Shells exist and the subnet registry is stable |

-| Presence gossip (`last_active_node/endpoint/timestamp` across subnet) | networking v1 | Needs the multi-instance registry first; agent-interaction heartbeat (7a-B) is the chosen signal | When PresenceChannel work begins |

+| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |

+| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04) | Once Shells exist and the subnet registry is stable |


| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates — grant store + interactive escalation); v1 ships the framework seam, this capability lands into it later. (The `shell_wake_spawn_anywhere` flag is a pre-consent variant.) | After remote-drive ships; capability drops into the v1 consent seam |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |

| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |


| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |

# Deferred Features

Intended features explicitly cut from v1 scope but committed for later. Not backlog noise — these are decided directions parked for sequencing reasons. Keep this list short and real.

| Feature | Cut from | Why deferred | Trigger to revisit |
|---|---|---|---|
| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any "scroll back further than the buffer" user need |
| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |
| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |
| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |
| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04) | Once Shells exist and the subnet registry is stable |
| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |
| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates — grant store + interactive escalation); v1 ships the framework seam, this capability lands into it later. (The `shell_wake_spawn_anywhere` flag is a pre-consent variant.) | After remote-drive ships; capability drops into the v1 consent seam |
| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |
| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |
| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |

-| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates — grant store + interactive escalation); v1 ships the framework seam, this capability lands into it later. (The `shell_wake_spawn_anywhere` flag is a pre-consent variant.) | After remote-drive ships; capability drops into the v1 consent seam |

-| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly) | Designed alongside instantiate-anywhere's consent gate |

+| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant shape ships M5). | Revisit-trigger met (remote-drive shipped M4); held back by user decision — capability drops into the M5 consent framework when picked up |

+| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |


Now D0b — register new reqs. Check toml tail for placement pattern:


PowerShell

Get-Content traceable-reqs.toml -Tail 30

Show end of traceable-reqs.toml


id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]
id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
[[requirements]]
id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
required_stages = []  # rule 5: activate when the daemon multi-agent pulse fan-out is wired. Today run_pulse_loop is def+test only and each agent drives its own pulse from its own process, so blocking is naturally isolated; the invariant binds once the daemon hosts N per-agent loops. M4-D9-2-3 note: the daemon's resting-edge feed points (attach detach, registry attention-shift, spt suspend) deliberately ARM the per-endpoint echo gate (resting::arm_transition_echo) instead of hosting the bounded call — N flips in one feed are N gate files consumed by N per-agent pulse loops, so the invariant holds by construction and the binding moment stays the per-agent-runtime fan-out (same seam as the in-pump Psyche reconcile turn)
# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
[[requirements]]
id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-SEED-ROTATION"
title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-RATE-LIMIT"
title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.
# --- Added 2026-06-03 at M4-D5a start (ADR-0009 origin-node gate; rule 3: register before satisfying) ---
[[requirements]]
id = "REQ-HAZARD-WAN-ORIGIN-AUTH"
title = "WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D5a: WanMessage carries NO origin field by design (a forged one decodes as an ignored unknown field — spt-net wanmsg unit), and wan::receive_wan takes origin_node from NetStreamInfo.remote_id_hex (tests/wanmsg.rs proves the stream-table origin IS the handshake identity and a forged payload origin is inert end-to-end)


Add-Content -Path traceable-reqs.toml -Value @'

# --- Added 2026-06-04 at M5-D0 (M5-PLAN scope decisions; rule 3: register before satisfying, rule 5: activate per sub-task commit) ---
[[requirements]]
id = "REQ-CONSENT-1"
title = "Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)"
required_stages = []  # rule 5: activate at M5-D1a (grant store lands)
[[requirements]]
id = "REQ-CONSENT-2"
title = "Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)"
required_stages = []  # rule 5: activate at M5-D1b/D1c (escalation + flag authoring land)
[[requirements]]
id = "REQ-PRES-1"
title = "Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)"
required_stages = []  # rule 5: activate at M5-D6 (gossip + MRA API land)
[[requirements]]
id = "REQ-SHELL-1"
title = "Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEXT Shell model)"
required_stages = []  # rule 5: activate at M5-D3 (hosting machinery lands; REQ-EP-5 covers the instantiation-model semantics)
[[requirements]]
id = "REQ-SHELL-2"
title = "Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)"
required_stages = []  # rule 5: activate at M5-D4 (sleep/wake lands)
'@

Append M5 requirement registrations to traceable-reqs.toml


(PowerShell completed with no output)


Edit

required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15

required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


160	# ───────────────────────────── Instances ────────────────────────────────────
161	[[requirements]]
162	id = "REQ-INST-1"
163	title = "endpoint ID vs instance split (adapter-agnostic ID)"
164	required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15
165	[[requirements]]
166	id = "REQ-INST-2"
167	title = "Per-node files, synced Psyche mind"

# ───────────────────────────── Instances ────────────────────────────────────
[[requirements]]
id = "REQ-INST-1"
title = "endpoint ID vs instance split (adapter-agnostic ID)"
required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15
[[requirements]]
id = "REQ-INST-2"
title = "Per-node files, synced Psyche mind"


required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15

required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "endpoint ID vs instance split (adapter-agnostic ID)"

-required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates at M5 when instantiate-anywhere makes the split user-facing (instance minting); its concrete slices are tracked by REQ-INST-7..15

+required_stages = []  # rule 5: the data-model split shipped across M4 D3 (registry Instance rows) + D9-5 (home/adapter on the endpoint record); this umbrella req activates when instantiate-anywhere makes the split user-facing (instance minting) — which DEFERS PAST M5 (user decision 2026-06-04, M5-PLAN scope decision 2: consent framework seam only); its concrete slices are tracked by REQ-INST-7..15

title = "Per-node files, synced Psyche mind"


PowerShell

traceable-reqs check | Select-Object -First 3; echo "exit=$LASTEXITCODE"

Validate registry after D0 edits


134 requirements: 134 complete, 0 incomplete, 0 findings
  [OK] REQ-API-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       api prefix and adapter_name on every machinery invocation
exit=


Green — 134 reqs, 0 findings. Commit D0:


git add ROADMAP.md docs/DEFERRED.md traceable-reqs.toml && git commit -m @'
docs(reqs,roadmap): M5-D0 — scope alignment + requirement registry prep

D0a: ROADMAP S-M5 amended to the locked scope (presence resolution only;
consent framework seam only; mock + OS-notification shell; remote-fork
arm defers with instantiate-anywhere). DEFERRED.md rows updated
(presence-gossip absorbed into the PresenceChannel row as shipping-M5
substrate; OS-notification carve-out; reserved-but-refusing capability
ids noted).

D0b: register REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 (rule 3,
inactive until their D-task per rule 5); REQ-INST-1 rationale updated
(defers past M5 with instantiate-anywhere).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@ && git push

Commit and push M5-D0 slice


warning: in the working copy of 'ROADMAP.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 2a59e7d] docs(reqs,roadmap): M5-D0 — scope alignment + requirement registry prep
 3 files changed, 28 insertions(+), 7 deletions(-)
To https://github.com/SaberMage/spt-core.git
   a6f5cab..2a59e7d  dev-freeform -> dev-freeform


Watch the D0 sha too:


$sha = git rev-parse 2a59e7d; for ($i=0; $i -lt 40; $i++) { $r = gh run list --commit $sha --json status,conclusion,workflowName | ConvertFrom-Json; if ($r.Count -gt 0 -and ($r | Where-Object { $_.status -ne "completed" }).Count -eq 0) { $r | ForEach-Object { "$($_.workflowName): $($_.conclusion)" }; break }; Start-Sleep -Seconds 20 }

Watch CI for D0 sha until FINAL conclusion


Command running in background with ID: b1pm15u8y. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b1pm15u8y.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bi8iravrn</task-id>
<tool-use-id>toolu_01Hi4wSJi4uNFMnm8pRNTydi</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bi8iravrn.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI runs for pushed sha until FINAL conclusion" completed (exit code 0)</summary>
</task-notification>


While CI runs — commune to Psyche (M5 started, scope locked):


Write

<live-context>
Generation 17 delta: M5 STARTED 2026-06-04. M5-PLAN.md authored (a6f5cab) + D0 scope alignment complete (2a59e7d); both pushed, CI watch pending FINAL.

## M5 scope LOCKED with user (three forks resolved 2026-06-04)
1. **Presence: resolution only** — presence-datum gossip + first-class most-recently-active API (REQ-PRES-1). PresenceChannel *endpoint* (dispatch/bind/thread) stays deferred past v1.
2. **Consent: framework seam only** — grant store (REQ-CONSENT-1) + interactive escalation + pre-consent flags (REQ-CONSENT-2). remote-exec + instantiate-anywhere stay deferred (ids reserved-but-refusing); **remote-fork arm + cross-node shell SPAWN defer with them**; cross-node shell LINK is in. REQ-INST-1 rationale updated (defers past M5).
3. **Shells: machinery + mock + ONE real shell** — OS-notification shell (Windows toast / Linux notify-send) as dogfood proof. GameRobot-class stays deferred.
4. Process: plan authored directly (no grill); surface further forks per-task.

## M5-PLAN task layer (D0–D9)
- D0 ✅ scope alignment (ROADMAP §M5 + DEFERRED.md amended) + registry prep (REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 registered inactive; 134 reqs, check green).
- D1 consent framework (grant store + escalation + pre-consent flags) — floor for D3 gates.
- D2 adapter registration lifecycle (REQ-INSTALL-4: spt adapter add/--github/remove, install-is-first-update).
- D3 shell hosting machinery (REQ-EP-5 + REQ-SHELL-1; KH 7.4 + STALE-INDEX-LOCK activate here; STDIN-SESSION-ID re-assess).
- D4 shell sleep/wake (REQ-SHELL-2: wake-watcher, exit-opcode, state-keyed resolution, owner cascade).
- D5 resting arms (REQ-INST-6 deferred-message gate via spt-daemon::resting seam; remote suspend/wake; remote fork documented-deferred).
- D6 presence resolution (REQ-PRES-1; swap MRA precursors: notif first-fire, update consent, escalation, wake resolution).
- D7 carried infra (relay rendezvous D2f-Q1; notif-ack→apply_brain_only production caller; cross-runner CI two-host job).
- D8 OS-notification shell + notif_command integration + cross-node relink.
- D9 rig int legs + sweep + closeout.
- Sequencing: D1+D2 floor → D3 → D4; D5 independent; D6 late enough to swap call sites once; D7c early pays off; D8 needs D2+D3+D4+D6.

## Immediate next steps
1. Confirm CI FINAL green on a6f5cab + 2a59e7d (sha-pinned, gotcha #7/#13).
2. Begin M5-D1a: grant store (capability × agent × target-node, target-enforced, security-material replication near trust store — REQ-SEC-1 plumbing precedent).

## Carry from prior commune: gotchas 1–17, flake ledger (docs/FLAKE-LEDGER.md), CI topology (hfenduleam detached runner — re-launch via Start-Process C:\actions-runner\run.cmd; gravity systemd), workflow constraints (no cargo fmt; evidence-only tags; check from repo root; clippy -D warnings; push slice → watch FINAL sha-pinned; commit trailer).
## No blockers.
</live-context>
<project-context>
name=spt-core

M5 IN PROGRESS (started 2026-06-04, M4 delivered same day): shells, presence, deferred capabilities. Scope locked with user: presence RESOLUTION only (channel endpoint deferred); consent FRAMEWORK seam only (remote-exec/instantiate-anywhere/remote-fork deferred, ids reserved); shell machinery + mock + OS-notification dogfood shell. M5-PLAN.md = task layer D0–D9; D0 complete (scope docs + 5 new reqs registered). Next: D1 consent grant store. Conventions hold: no cargo fmt; evidence-only tags; traceable-reqs check repo root; clippy -D warnings; push slice → watch FINAL sha-pinned; commit trailer Claude Opus 4.8 (1M context).
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

<live-context>
Generation 17 delta: M5 STARTED 2026-06-04. M5-PLAN.md authored (a6f5cab) + D0 scope alignment complete (2a59e7d); both pushed, CI watch pending FINAL.

## M5 scope LOCKED with user (three forks resolved 2026-06-04)
1. **Presence: resolution only** — presence-datum gossip + first-class most-recently-active API (REQ-PRES-1). PresenceChannel *endpoint* (dispatch/bind/thread) stays deferred past v1.
2. **Consent: framework seam only** — grant store (REQ-CONSENT-1) + interactive escalation + pre-consent flags (REQ-CONSENT-2). remote-exec + instantiate-anywhere stay deferred (ids reserved-but-refusing); **remote-fork arm + cross-node shell SPAWN defer with them**; cross-node shell LINK is in. REQ-INST-1 rationale updated (defers past M5).
3. **Shells: machinery + mock + ONE real shell** — OS-notification shell (Windows toast / Linux notify-send) as dogfood proof. GameRobot-class stays deferred.
4. Process: plan authored directly (no grill); surface further forks per-task.

## M5-PLAN task layer (D0–D9)
- D0 ✅ scope alignment (ROADMAP §M5 + DEFERRED.md amended) + registry prep (REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 registered inactive; 134 reqs, check green).
- D1 consent framework (grant store + escalation + pre-consent flags) — floor for D3 gates.
- D2 adapter registration lifecycle (REQ-INSTALL-4: spt adapter add/--github/remove, install-is-first-update).
- D3 shell hosting machinery (REQ-EP-5 + REQ-SHELL-1; KH 7.4 + STALE-INDEX-LOCK activate here; STDIN-SESSION-ID re-assess).
- D4 shell sleep/wake (REQ-SHELL-2: wake-watcher, exit-opcode, state-keyed resolution, owner cascade).
- D5 resting arms (REQ-INST-6 deferred-message gate via spt-daemon::resting seam; remote suspend/wake; remote fork documented-deferred).
- D6 presence resolution (REQ-PRES-1; swap MRA precursors: notif first-fire, update consent, escalation, wake resolution).
- D7 carried infra (relay rendezvous D2f-Q1; notif-ack→apply_brain_only production caller; cross-runner CI two-host job).
- D8 OS-notification shell + notif_command integration + cross-node relink.
- D9 rig int legs + sweep + closeout.
- Sequencing: D1+D2 floor → D3 → D4; D5 independent; D6 late enough to swap call sites once; D7c early pays off; D8 needs D2+D3+D4+D6.

## Immediate next steps
1. Confirm CI FINAL green on a6f5cab + 2a59e7d (sha-pinned, gotcha #7/#13).
2. Begin M5-D1a: grant store (capability × agent × target-node, target-enforced, security-material replication near trust store — REQ-SEC-1 plumbing precedent).

## Carry from prior commune: gotchas 1–17, flake ledger (docs/FLAKE-LEDGER.md), CI topology (hfenduleam detached runner — re-launch via Start-Process C:\actions-runner\run.cmd; gravity systemd), workflow constraints (no cargo fmt; evidence-only tags; check from repo root; clippy -D warnings; push slice → watch FINAL sha-pinned; commit trailer).
## No blockers.
</live-context>
<project-context>
name=spt-core

M5 IN PROGRESS (started 2026-06-04, M4 delivered same day): shells, presence, deferred capabilities. Scope locked with user: presence RESOLUTION only (channel endpoint deferred); consent FRAMEWORK seam only (remote-exec/instantiate-anywhere/remote-fork deferred, ids reserved); shell machinery + mock + OS-notification dogfood shell. M5-PLAN.md = task layer D0–D9; D0 complete (scope docs + 5 new reqs registered). Next: D1 consent grant store. Conventions hold: no cargo fmt; evidence-only tags; traceable-reqs check repo root; clippy -D warnings; push slice → watch FINAL sha-pinned; commit trailer Claude Opus 4.8 (1M context).
</project-context>


Prompt:
<task-notification>
<task-id>bi8iravrn</task-id>
<tool-use-id>toolu_01Hi4wSJi4uNFMnm8pRNTydi</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bi8iravrn.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI runs for pushed sha until FINAL conclusion" completed (exit code 0)</summary>
</task-notification>


1	ci: cancelled
2

ci: cancelled


Found 6 files
crates\spt\src\cli.rs
crates\spt-store\src\perch.rs
crates\spt-daemon\tests\access.rs
crates\spt-daemon\src\access.rs
crates\spt-store\src\access.rs
crates\spt-net\src\lib.rs


1	//! `trust/access.json` — the per-endpoint access whitelist (ADR-0009 middle
2	//! gate, M4-D5d, REQ-SEC-1) + the recent-outbound correlation log its
3	//! stateful-firewall exemption reads.
4	//!
5	//! Security material lives **next to the trust store** ([`crate::trust`]):
6	//! node-local, durable, never git-tracked, never synced over the context repo.
7	//! The store answers one question for the daemon's net-inbound gate
8	//! ([`spt-daemon::access`]): *may an interaction originating from node N
9	//! remotely reach endpoint E?*
10	//!
11	//! ## Polarity (the trust store's opposite)
12	//!
13	//! The trust store is an allowlist that starts empty (nothing trusted until
14	//! paired). The access whitelist starts **absent = open** (ADR-0009): a paired
15	//! node in your subnet may reach your endpoints until you restrict one.
16	//! Setting any entry for an endpoint flips it to restrict: only listed nodes
17	//! may send *unsolicited* inbound. An entry whose `nodes` is empty is a valid
18	//! locked-down state (refuses all unsolicited remote inbound); deleting the
19	//! entry ([`AccessStore::open`]) returns the endpoint to default-open.
20	//!
21	//! The subject is always the **origin node** — the QUIC handshake-proven
22	//! remote node id (Ed25519 pubkey hex), never a payload field
23	//! (REQ-HAZARD-WAN-ORIGIN-AUTH) and never a sender *endpoint* identity.
24	//! `users` is the inert ADR-0006 cross-user schema reservation.
25	//!
26	//! ## Recent-outbound correlation (`trust/recent-outbound.json`)
27	//!
28	//! The whitelist gates the **unsolicited** class only: an inbound correlated
29	//! to the endpoint's own recent outbound (`__REPLY_TO__` traffic) passes from
30	//! any visible node — a stateful firewall, not a blanket block. The
31	//! [`RecentOutbound`] log is the correlation state: the daemon records
32	//! `(endpoint, dest_node)` when an endpoint sends off-node, and the gate's
33	//! classifier asks whether `(endpoint, origin_node)` appears within the reply
34	//! window. Durable (the sending CLI and the receiving daemon brain are
35	//! different processes; a brain restart must not flip replies to unsolicited),
36	//! bounded (windowed prune + a hard cap), atomic writes.
37
38	use serde::{Deserialize, Serialize};
39	use std::path::Path;
40
41	use crate::atomic::atomic_write_string;
42	use crate::perch::{access_file, recent_outbound_file};
43
44	/// One endpoint's whitelist entry. Present ⇒ the endpoint is restricted.
45	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46	pub struct AccessEntry {
47	    /// The endpoint this entry restricts.
48	    pub endpoint: String,
49	    /// Origin nodes (Ed25519 pubkey hex) allowed to send unsolicited inbound.
50	    /// Empty = locked down (all unsolicited remote inbound refused).
51	    #[serde(default)]
52	    pub nodes: Vec<String>,
53	    /// Allowed users — the inert ADR-0006 cross-user reservation. Schema only;
54	    /// nothing reads it until the cross-user model lands.
55	    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56	    pub users: Vec<String>,
57	}
58
59	/// The whole `access.json` document.
60	#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61	pub struct AccessStore {
62	    #[serde(default)]
63	    pub entries: Vec<AccessEntry>,
64	}
65
66	impl AccessStore {
67	    /// Load from the canonical [`access_file`]. Absent ⇒ empty (every endpoint
68	    /// default-open) — never an error.
69	    pub fn load() -> AccessStore {
70	        Self::load_from(&access_file())
71	    }
72
73	    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
74	    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
75	    /// a damaged file must not brick the node. Note the failure direction is
76	    /// **open** (ADR-0009's default), not closed — the whitelist is a
77	    /// same-subnet convenience boundary layered *inside* pairing trust, not
78	    /// the system's outer wall.
79	    pub fn load_from(path: &Path) -> AccessStore {
80	        match std::fs::read_to_string(path) {

//! `trust/access.json` — the per-endpoint access whitelist (ADR-0009 middle
//! gate, M4-D5d, REQ-SEC-1) + the recent-outbound correlation log its
//! stateful-firewall exemption reads.
//!
//! Security material lives **next to the trust store** ([`crate::trust`]):
//! node-local, durable, never git-tracked, never synced over the context repo.
//! The store answers one question for the daemon's net-inbound gate
//! ([`spt-daemon::access`]): *may an interaction originating from node N
//! remotely reach endpoint E?*
//!
//! ## Polarity (the trust store's opposite)
//!
//! The trust store is an allowlist that starts empty (nothing trusted until
//! paired). The access whitelist starts **absent = open** (ADR-0009): a paired
//! node in your subnet may reach your endpoints until you restrict one.
//! Setting any entry for an endpoint flips it to restrict: only listed nodes
//! may send *unsolicited* inbound. An entry whose `nodes` is empty is a valid
//! locked-down state (refuses all unsolicited remote inbound); deleting the
//! entry ([`AccessStore::open`]) returns the endpoint to default-open.
//!
//! The subject is always the **origin node** — the QUIC handshake-proven
//! remote node id (Ed25519 pubkey hex), never a payload field
//! (REQ-HAZARD-WAN-ORIGIN-AUTH) and never a sender *endpoint* identity.
//! `users` is the inert ADR-0006 cross-user schema reservation.
//!
//! ## Recent-outbound correlation (`trust/recent-outbound.json`)
//!
//! The whitelist gates the **unsolicited** class only: an inbound correlated
//! to the endpoint's own recent outbound (`__REPLY_TO__` traffic) passes from
//! any visible node — a stateful firewall, not a blanket block. The
//! [`RecentOutbound`] log is the correlation state: the daemon records
//! `(endpoint, dest_node)` when an endpoint sends off-node, and the gate's
//! classifier asks whether `(endpoint, origin_node)` appears within the reply
//! window. Durable (the sending CLI and the receiving daemon brain are
//! different processes; a brain restart must not flip replies to unsolicited),
//! bounded (windowed prune + a hard cap), atomic writes.

use serde::{Deserialize, Serialize};
use std::path::Path;

use crate::atomic::atomic_write_string;
use crate::perch::{access_file, recent_outbound_file};

/// One endpoint's whitelist entry. Present ⇒ the endpoint is restricted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccessEntry {
    /// The endpoint this entry restricts.
    pub endpoint: String,
    /// Origin nodes (Ed25519 pubkey hex) allowed to send unsolicited inbound.
    /// Empty = locked down (all unsolicited remote inbound refused).
    #[serde(default)]
    pub nodes: Vec<String>,
    /// Allowed users — the inert ADR-0006 cross-user reservation. Schema only;
    /// nothing reads it until the cross-user model lands.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub users: Vec<String>,
}

/// The whole `access.json` document.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AccessStore {
    #[serde(default)]
    pub entries: Vec<AccessEntry>,
}

impl AccessStore {
    /// Load from the canonical [`access_file`]. Absent ⇒ empty (every endpoint
    /// default-open) — never an error.
    pub fn load() -> AccessStore {
        Self::load_from(&access_file())
    }

    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* pairing trust, not
    /// the system's outer wall.
    pub fn load_from(path: &Path) -> AccessStore {
        match std::fs::read_to_string(path) {


1://! Endpoint access control — the ADR-0009 middle gate (M4-D5d, REQ-SEC-1).
3://! ADR-0009 models access as **three orthogonal, nested gates**, evaluated in
6://! **endpoint access whitelist** (may *your node* remotely reach it? — THIS
32://! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —
34://! an `spt access` edit takes effect on the next inbound without a daemon

[Showing results with pagination = limit: 5]


No matches found


49:pub enum InboundClass {
59:pub enum AccessDecision {
70:fn local_node_hex() -> Option<String> {
86:pub fn access_check(endpoint: &str, origin_node: &str, class: InboundClass) -> AccessDecision {
115:pub fn classify_inbound(endpoint: &str, origin_node: &str, now_ms: u64) -> InboundClass {
129:pub fn note_outbound(endpoint: &str, node: &str, now_ms: u64) {
144:    fn gate_decision_table() {
190:    fn reply_passes_where_unsolicited_refused() {
223:    fn same_node_always_passes_without_minting() {


D1 recon done. Grant store pattern clear: sibling of `spt-store::access` (`trust/grants.json`, security material near trust store, atomic writes, degrade stance) — but **opposite polarity** (default-deny per capability vs access default-open). M4 gap noted: access whitelist "subnet-settable" shipped node-local-edit only; grant propagation path = D1a design point (likely typed WAN op → target node writes locally, since enforcement is target-side anyway).

**Status:**
- M5-PLAN.md authored + pushed (`a6f5cab`) — scope locked via your 4 fork answers
- M5-D0 complete + pushed (`2a59e7d`): ROADMAP §M5 + DEFERRED.md amended; REQ-CONSENT-1/2, REQ-PRES-1, REQ-SHELL-1/2 registered (134 reqs, check green); REQ-INST-1 defers past M5
- Commune dropped to Psyche (generation 17)
- `a6f5cab` CI run cancelled — superseded by D0 push (concurrency auto-cancel, normal). Watching `2a59e7d` for FINAL.

One consequence worth flagging: your "framework seam only" choice pulls the **remote fork arm** out of M5 (it rides instantiate-anywhere) — ROADMAP carried-seam list amended accordingly.

Next after CI green: **D1a grant store**.