# #300 direction C — source-grounded design packet

**Status: DESIGN ONLY. No implementation, no REQ activation, no acceptance claim, no milestone change.**
Subject: `origin/main` @ `24a7ba0a` (read-only). Separate from frozen v9 (`a9510e1e`), which this does not touch.

**Direction, with provenance.** Reavo confirmed direction C for #300; the confirmation reached me
RELAYED — operator to lia to doyle to me (doyle's dispatch, 2026-09-12 09:42Z). It supersedes the
A/B fork doyle filed earlier on #300. I have not spoken to the operator about it and I am not the
author of the ruling; what follows is design against it.

C as stated: "If an endpoint session's now signal can report the controller attached state, then the
now signal's USER_INPUT event can detect the attached controller's node ID. ... IF the remote node
needs a certified means of communicating the event back to the attached controller's node, consider
something like a handshake code which the USER_INPUT event would be able to know and use."

## 1. C's premise is TRUE in source, and its first clause alone is NOT SOUND

The attached-controller datum exists and is reported:
- `crates/spt-daemon/src/attachment.rs:118-125` — `Attachment { controlled: bool, controller_node: Option<String>, viewers, viewer_nodes }`.
- `crates/spt-daemon/src/broker.rs:3621` populates `controller_node` from `controller_by()` (`:1603`), i.e. the controller seat's `by`.
- The now-signal surface carries both (`controlled`, `controller-node`), asserted at `crates/spt/tests/attach_link_push_e2e.rs:392-421`.

Three source facts refute using that datum as the identity of a given USER_INPUT:

1. **It can be absent for a real controller.** `attachment.rs:113-117`: "a local controller presents
   no node identity, so 'somebody is driving' is knowable when 'which node is driving' is not."
2. **It LAGS the decision it describes.** `broker.rs:1614-1620`: the perch info stamp "lags the seat
   decision (KH 7.15 / info.rs terminal_normalize)", and `SessionInfo.controller_by == None` is
   "ambiguous between empty and local". A value that lags cannot date an event.
3. **Sampling it at event time is the handoff counterexample already on record** (my earlier #300
   packet `.spt/preserved/todlando-304-input-contract.txt` section 2, and doyle's "current-controller
   inference stays rejected"): A submits, the harness queues it, B takes the seat, the hook fires,
   the sample says B. Reading the now-signal instead of `driven_by` changes nothing about that.

So C is deliverable only through its SECOND clause. The handshake code is not an optional extra for
a "remote node that needs certification" — it is the whole mechanism.

## 2. The trusted identity path that already exists

`crates/spt-daemon/src/attach.rs:419` — `origin_node` MUST be the handshake-proven remote id from
the broker's handshake; `:348` and `:454` take it as a parameter rather than deriving it, and
`access_check` (`:32`) sees that proven origin. **A cryptographically proven controller node id
therefore exists at ATTACH time, on the receiving node, before any input flows.** That is the datum
C wants. What is missing is not proof of origin; it is a CARRIED ASSOCIATION between that proof and
a particular semantic submission.

The authorization arm that would consume it already exists and is reusable unchanged:
`crates/spt-daemon/src/wan.rs:1289-1345` `request_quoted_paths` — requires a parsed envelope, refuses
anything whose type is not `user-msg`, extracts quoted absolute paths
(`spt_store::helperline::quoted_path_candidates`, capped at `MAX_QUOTED_PATHS`), then asks the
PROVEN origin node to serve each path with `audience = msg.target`, `HELPER_ENTRY_TTL_MS`, and the
message id as the entry's `origin`. Every refusal emits one `HELPER_SERVE_FOR ... outcome=declined`
line. Its three authorization inputs are exactly: proven origin node, committed user text, audience.

## 3. What the event side does NOT carry, and the constraint that must not be broken

- `crates/spt/src/api/delivery.rs:115-134` `state_io_kind` — USER_INPUT is `api state busy` WITH a
  payload and not mid-turn. The function is deliberately PURE and its own comment forbids the
  shortcut C's first clause implies: "it still cannot consult the edge, the sentinel or the stamp."
  **The kind decision must stay free of attachment state.** Provenance must ride as DATA on the
  event, never as a condition on its classification.
- `delivery.rs:141-152` `publish_state_io` builds `IoEvent::new(id, kind, payload).with_mid(mid)`:
  IoEvent has no origin, no receipt, no submission id. The funnel is also explicitly best-effort and
  "structurally unable to affect the caller" — so a provenance check may never change an `api state`
  exit code, and a grant decision may not live inside the funnel.
- `crates/spt/src/rc.rs:210-216` — the controller reserved-row identity marker (`REQ-RC-IDENTITY`)
  is SHIPPED OFF (`STATUS_ROW_ENABLED = false`, bug #14, `REQ-RC-IDMARKER-DISABLE`). There is
  currently no in-band surface painting controller identity into the session, so no design may
  assume one.

## 4. The missing contract, stated as the smallest thing that closes C

**A seat-bound input-provenance code.**

- **Mint:** when a Control attach is admitted with a handshake-proven origin, the broker mints an
  opaque code bound to (session id, endpoint incarnation, controller SEAT GENERATION, proven origin
  node). A handoff retires the old code and mints a new one; a viewer gets none.
- **Read:** the session's own adapter, on the target node, can read the CURRENT code through an
  existing authenticated read verb. No new transport.
- **Report:** the adapter passes the code it captured AT THE MOMENT IT ACCEPTED THE SUBMISSION
  alongside the `api state busy` payload. The code is data on the event; classification is untouched.
- **Verify and authorize:** core accepts a grant only if the code verifies, is unretired, and names a
  seat generation with one continuous occupancy since mint. The grant then runs the existing
  `request_quoted_paths` arm with origin = the code's proven node, audience = the receiving endpoint,
  TTL = `HELPER_ENTRY_TTL_MS`, once per input/path. A missing, retired, or unverifiable code REFUSES
  BY NAME on the existing declined line. No code, no grant — refusal is the default arm.

**Why this is smaller than the (A) capability I previously specified:** (A) required a
receipt-bearing input TRANSPORT. This needs no transport change, because the code is read out of
band and reported with the payload the adapter already sends.

**The residual hole, named rather than buried.** Capture-at-acceptance is the adapter's act, and core
cannot verify when the capture happened. In the queue case (A submits, harness queues, B takes the
seat, the hook fires and reads the code THEN), the event carries B's code. The path then resolves on
B's machine: usually absent, so a 404/decline and no wrongful grant. But if B happens to hold that
path, B's file is exposed to the audience under B's code without B typing it. That is a narrow
misauthorization and it is the ONE thing C cannot close from the core side alone. It closes only if
the adapter contract REQUIRES capture at acceptance and core may refuse when the seat generation
changed between mint and report — which only the adapter can witness. This must be an explicit
clause, not an assumption.

## 5. Acceptance criteria (proposed; not granted)

1. **Real remote rc input, not an envelope.** `spt rc` from node A to an endpoint on node B, with a
   quoted absolute path in the typed input. The existing WAN-envelope test is not coverage.
2. **USER_INPUT carries a verifying code** whose bound origin equals A's handshake-proven node id.
3. **One scoped registration on A**: the quoted path, audience = the receiving endpoint, TTL 24h
   (`HELPER_ENTRY_TTL_MS`), `origin` naming the input — and the FILE_ACCESS_HELPER frame emitted once
   per input/path, not per keystroke and not per repeat.
4. **Cross-node fetch 200 with byte equality**; an edit on A visible through the reference; a delete
   yields 404. The #300 body's manual-serving 200/16898 is explicitly NOT this.
5. **Refusal arms, each with its own positive control:** no code declines by name; a retired
   (post-handoff) code declines by name; a viewer attach has no code and declines; a code from
   another session or incarnation declines. A refusal suite with no passing arm proves nothing.
6. **No raw-keystroke path.** Typing a path that never reaches USER_INPUT must grant nothing — core
   never parses terminal keys into grants.
7. **Replay cannot extend.** Re-reporting the same code and payload re-registers nothing and does not
   move the TTL.

## 6. What I am NOT doing in this packet

No code, no REQ minted or activated, no test authored, no milestone membership change, no build, no
field action. The residual in section 4 is unresolved and is the first thing needing a ruling.
