# Boundary rotation credential: the departed-session catch-22 and the parent_pid anchor

<!-- [doc->REQ-BOUNDARY-ROTATION-CREDENTIAL] -->

## Status

accepted (2026-07-02; ruled by `doyle` with operator during the F-024D field diagnosis, against a clean-room repro on two nodes). Companion to REQ-HAZARD-SESSION-PIN-WEDGE (the wedge this credential design prevents) and the harness-contract `api boundary` documentation this ADR made honest. Field history: `spt-claude-code` docs/SPT-CORE-FINDINGS.md F-024/F-024D.

## Context

`api boundary <clear|compact> <id> --to-session-id <sid>` rebinds a perch to the session id a harness rotates to on `/clear`/`/compact` (CONTEXT.md "Inbound `api` surface": the report *carries the new session_id*; the endpoint identity and **`parent_pid` anchor hold** across the rotation).

The local-api auth hardening (REQ-HAZARD-LOCAL-API-AUTH, codex #13) later wrapped every mutating `api` verb in one uniform gate: prove association with `--token` or a `--session-id` **matching the perch record**. For every other verb the caller naturally holds that sid — it *is* the current session. `boundary` is the one verb where it is not: the matching sid belongs to the session being **departed**, and by the time the harness fires the rotation hook, that session's context (env, per-session state) is gone while the hook payload carries only the **new** sid. A caller with no prior-sid bookkeeping is refused — and a refused (or silently skipped) rotation latches REQ-HAZARD-SESSION-PIN-WEDGE: the perch stays pinned to a dead sid, every subsequent id-scoped hook call refuses (including retry-boundary, which presents the new sid), and the endpoint strands with zero delivery until a full relaunch. Two independent nodes hit exactly this in the field (F-024D clean-room: `/clear` wedges a *healthy* session).

Two incoherences make this a design defect, not just a docs gap:

1. **It contradicts the documented design shape.** CONTEXT.md designs rotation around the *stable* anchors — endpoint identity + `parent_pid` — yet the implementation gates rotation on the *rotating* credential (the departed sid), the one thing the design says does NOT survive the boundary.
2. **It is out of proportion with the local trust posture.** The token is "a local capability, not a network secret" (same-user-readable file); `spt endpoint purge` — strictly more destructive — is *deliberately ungated* (CONTEXT.md). Demanding departed-session proof for a rotation report is stronger auth than the surface's own teardown.

Legacy parity note: the sister project's boundary ledger had no rotation credential at all; the uniform gate is a rebuild-era addition that never got a boundary-shaped design pass.

## Decision

Three layers, additive — no existing proof path changes:

### 1. Documented adapter pattern (immediate; the contract as it stands)

The harness-contract documents the real requirement: **persist the current session id at every SessionStart in adapter-owned state keyed by the endpoint id; present it as `--session-id <prior>` at the rotation call.** `--to-session-id` is payload, never proof. A refused or unresolvable rotation must be LOUD (stderr + exit code surfaced) — a silent skip is a stranded endpoint. Reference implementation: `claude-spt` v0.10.4 (`{adapter_dir}/state/session/<endpoint_id>.sid`). Per-session env files are explicitly the WRONG persistence surface (they die with the session — that is the catch-22 itself).

### 2. Dead-owner re-pin (crashed sessions; ships with the F-023 delivery wave)

`authenticate()` gains an additive branch: sid mismatch AND no token presented AND the perch's recorded pid is **dead** → accept and re-pin (loud `SESSION_REPIN`). Rescues a perch stranded by a *crashed* session. Deliberately does **NOT** fire for a live owner — `/clear` keeps the harness pid alive, so live rotation still requires proof (squat protection unchanged).

### 3. parent_pid-anchor ancestry proof (the design-true credential; core, follow-up wave)

`api boundary` additionally accepts an **OS-verified ancestry proof**: the calling `spt` process's real ancestor chain contains the perch's recorded harness pid (`parent_pid`/`pid` anchor). A rotation hook *is* a descendant of the harness being rotated — the anchor CONTEXT.md says holds across the boundary becomes the credential, verified from the process table (OS truth), never caller-claimed. This makes adapter sid-state optional (the layer-1 pattern remains valid and documented). Scope: `boundary` only — other verbs keep the uniform gate (their callers hold the current sid naturally).

## Consequences

- Adapter authors get an honest contract now (layer 1) and a bookkeeping-free path later (layer 3); the `.sid` pattern stays harmless once redundant.
- The wedge class becomes unreachable three ways: documented proof for live rotation, re-pin for dead sessions, anchor proof once layer 3 lands.
- Windows caveat recorded for layer 3: parent-pid spoofing via `PROC_THREAD_ATTRIBUTE_PARENT_PROCESS` exists; it is inside the same-user local threat model the token already accepts (CONTEXT.md trust posture: the subnet membership boundary is the trust boundary; local same-user adversaries are out of scope). The ancestry walk must tolerate reparenting-to-1 on unix (dead intermediate parents) by matching against the recorded anchor pid anywhere in the available chain, and must fail CLOSED to the layer-1 proof paths.

## Considered and rejected

- **Ungate `boundary` entirely** (parity with ungated `purge`): rejected — purge refuses live endpoints, boundary targets them by nature; an unauthenticated live-perch rotation is a real accident/squat surface (any local script naming an id could hijack hook routing).
- **Uniform gate + adapter bookkeeping as the PERMANENT design** (status quo, documented): rejected as end-state — it makes every adapter carry state to satisfy a credential the design never intended; kept only as the immediate documented pattern (layer 1).
- **Expose the perch token via a published accessor** for hooks to read: rejected — same-user file reads make it a no-op security-wise, but it adds a second bookkeeping surface (token path resolution) without removing the first; the ancestry proof removes bookkeeping entirely.
