{
  "summary": "# Core-security RCA and recommendation\n\n## Verdict\n\n**No spt-core production-code change is warranted for this incident.** The observed `SEEDED:<dead-pid>` followed deterministically by `STALE_SEED:<dead-pid>` is the current, intentional contract:\n\n1. `api seed` acknowledges that the daemon accepted ephemeral startup material; it does **not** certify the PID or session identity.\n2. `api listen` takes that consume-once seed, checks the anchor PID at the point where a bind would mutate a perch, and fails closed when the PID is dead.\n3. A supplied `--session-id` is a fallback only when there is **no seed**. It deliberately does not rescue or override an existing stale seed.\n4. A nonempty explicit session ID paired with a live PID can bind because core has no independent Claude Code session-ID oracle. This is an expressly documented local bearer-credential/override surface, not freshness validation.\n\nSilently replacing either explicit value in core would weaken the ownership boundary: it could convert a stale or malformed adapter launch into a successful bind, potentially under a bearer session ID belonging to a prior seat. Core should continue to honor explicit caller input and fail closed where it has authoritative evidence (dead PID, empty SID, live-owner conflict), rather than guess a different harness process or session.\n\n## Exact seed → listen behavior\n\n- `crates/spt/src/api/startup.rs:37-64`: `cmd_seed(pid, session_id, cwd)` rejects only empty/whitespace SIDs, builds `Seed`, calls daemon `put_seed`, and prints `SEEDED:{pid}`. There is deliberately no PID liveness probe here.\n- `crates/spt-daemon/src/seedmap.rs:1-18`: seeds are in-memory, ephemeral, keyed by parent PID, and consume-once; daemon restart drops them.\n- `crates/spt-daemon/src/seedmap.rs:73-92`: `SeedRegistry` is a `Mutex<HashMap<u32, Seed>>`; `put` uses `insert`, so a later seed for the same PID refreshes/replaces the earlier value.\n- `crates/spt-daemon/src/seedmap.rs:130-148`: daemon `Take` removes the PID-keyed seed before returning it.\n- `crates/spt-daemon/src/seedmap.rs:221-252`: public `put_seed`/`take_seed` are thin IPC clients; no hidden validation is performed.\n- `crates/spt/src/api/startup.rs:134-152`: `bind_from_seed` calls `take_seed_from_daemon`; `NoSeed` is returned only when the map has no entry.\n- `crates/spt/src/api/startup.rs:154-162`: `seed_restorable` explicitly classifies `StaleSeed` and `EmptySession` as spent/non-restorable; pre-bind errors such as conflict/home refusal restore the seed for a corrected retry.\n- `crates/spt/src/api/startup.rs:164-193`: `bind_taken_seed` checks empty SID, then checks `proc::is_process_alive(seed.parent_pid)`, and returns `BindError::StaleSeed` before `establish_perch` if dead.\n- `crates/spt/src/api/startup.rs:560-561`: listen PID precedence is `parent_pid_override.or_else(proc::parent_pid)`. Thus an explicit stale `--parent-pid` intentionally wins over core's direct-parent discovery.\n- `crates/spt/src/api/startup.rs:602-625`: session-ID fallback is entered only for `Err(BindError::NoSeed(_))`. `StaleSeed`, `EmptySession`, conflicts, and every other seed-path result are reported directly; `--session-id` does not paper over them.\n- `crates/spt/src/api/startup.rs:796-822`: diagnostics are loud and differentiated: `NO_SEED`, `STALE_SEED`, `EMPTY_SESSION`, `CONFLICT`, etc.\n\nConsequently, the exact repro has these semantics:\n\n- first command: the seed daemon stores `{dead_pid, sid}` and returns `SEEDED`;\n- listen: takes the record, proves the PID is dead at bind time, returns `STALE_SEED`, and does not write a perch;\n- another listen without reseeding: returns `NO_SEED`, because a stale seed is diagnostic evidence that is intentionally spent rather than restored;\n- reseeding the same PID recreates/replaces the map entry and permits the sequence to repeat deterministically.\n\n## Why validation belongs at bind, not seed\n\nA seed-time PID check would not establish a stronger invariant: the process can exit immediately after the check, and PID recycling can occur between seed and bind. The mutation boundary is the correct required check. The current implementation probes again immediately before establishing the perch. Adding an early seed-time diagnostic could only be an additional UX hint; it would not replace the bind check and is not required for correctness. It could also reject legitimate timing arrangements in which seed custody and listener startup race differently across harnesses.\n\nOn Windows, `crates/spt-store/src/proc.rs:16-66` implements liveness through `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` and immediate `CloseHandle`; PID 0 is always dead. This proves process existence, not Claude-session identity or process birth time. Core's local API does not claim a cryptographic same-user/PID-recycle boundary. The startup docs explicitly classify `--parent-pid` as a local override and session IDs as bearer strings.\n\n## Explicit session-ID behavior and security boundary\n\n- `crates/spt/src/api/startup.rs:211-239`: the SID fallback contract says a seed is a consume-once capability but `--session-id` is a **bearer string**; anyone who knows a live SID can present it. It records the accepted local-security posture rather than treating SID as OS-verifiable identity.\n- `crates/spt/src/api/startup.rs:230-286`: `bind_from_session_id` validates nonempty SID and live parent PID, then passes through the same `establish_perch` conflict/custody gates. There is no harness-specific SID-freshness oracle.\n- `crates/spt/src/api/startup.rs:261-300` and `crates/spt/src/api/auth.rs:82-148`: `establish_perch`/auth prevent takeover of a perch whose recorded owner is alive under a different SID and protect psyche custody. These are authoritative local state checks; they do not reinterpret the caller's SID.\n- `docs-site/src/harness-contract/api.md:127-138`: published documentation states that a seed is a consume-once capability, SID is a bearer string, SIDs must be treated as secrets, and the local surface already trusts the `--parent-pid` override.\n- `docs-site/src/harness-contract/api.md:140-151`: documents spend-versus-restore behavior: stale PID and empty SID spend the seed; recoverable pre-bind refusals restore it.\n\nTherefore, “explicit current PID + SID binds” is expected. Core can determine that the PID exists and that the target perch is not held by a conflicting live owner; it cannot determine whether a Claude-provided string is the newest session ID. A generic core self-resolution or stale-SID override would be both harness-specific and security-negative.\n\nResidual security qualification: a malicious same-user local process that knows a SID and can choose a live PID is within the explicitly accepted local override surface. Core is fail-closed against demonstrably dead anchors and conflicting live owners, not a sandbox against hostile processes running as the same OS user. Any stronger model would require a new authenticated hook/host attestation protocol, not a heuristic ancestry walk or silent replacement of explicit values.\n\n## Manifest hook fill ownership\n\nThe manifest does not make core the executor or identity resolver for inbound SessionStart hooks:\n\n- `crates/spt-runtime/src/manifest.rs:171-183`: `Hook` is parsed as opaque `fires`, declared `reads`, and `can_inject`; the schema stores the harness's outbound command string.\n- `crates/spt-runtime/src/manifest.rs:1079-1238`: manifest validation checks executable runtime role templates; it does not execute or fill `hooks.*.fires`.\n- `crates/spt-runtime/src/runtime.rs:79-138`: the generic substitution-key catalog contains `parent_pid` and session keys for core-executed adapter role templates, but catalog membership is not hook execution ownership.\n- `crates/spt-runtime/src/runtime.rs:290-470`: fill primitives operate when core executes runtime templates; there is no SessionStart-hook dispatcher here.\n- `docs-site/src/harness-contract/integration-checklist.md:140-144`: authoritative integration guidance states that `[hooks.<Event>]` remains purely outbound—the harness fires it, core never runs a hook handler—and hook logic must live in the adapter binary or static plugin.\n\nThere is a documentation ambiguity worth correcting separately: `docs-site/src/harness-contract/manifest.md:30-39` broadly says placeholders in `fires` must resolve to values core supplies, while the integration checklist and implementation establish that the harness/adapter dispatch wrapper performs the hook call. This is a **docs-only clarification opportunity**, not a reason to add core hook execution or PID discovery.\n\n## Existing test coverage\n\nUnit coverage in `crates/spt/src/api/startup.rs`:\n\n- `:941-958` — valid current-PID seed binds and records the seed SID.\n- `:961-971` — dead anchor returns `StaleSeed` and creates no perch.\n- `:974-1004` — missing seed returns `NoSeed`; empty seed SID is refused without a perch.\n- `:1007-1017` — empty SID refusal.\n- `:1020-1043` — live perch under another SID conflicts, covering PID-recycle/id-collision protection at the held-perch boundary.\n- `:1411-1458` — recoverable pre-bind refusal restores the seed and corrected retry succeeds.\n- `:1465-1482` — successful bind consumes the seed exactly once.\n- `:1489-1505` — SID fallback refuses empty SID and dead parent.\n- `:1512-1525` — live-parent direct SID fallback succeeds and records the SID.\n- `:1535-1555` — SID fallback runs only for `NoSeed`, not for an existing seed-path refusal.\n- `:1594-1623` — stale/dead-PID seed is spent: first bind is `StaleSeed`, retry is `NoSeed`.\n- `:1626-1650` — empty-SID seed is likewise spent.\n- `:1653-1683` — SID fallback cannot use another psyche's custody SID to squat a new perch.\n\nSeed-daemon unit coverage in `crates/spt-daemon/src/seedmap.rs:529-568` proves absent take, put→take→absent consume-once behavior, and same-PID put refresh/replacement.\n\nCLI/integration coverage:\n\n- `crates/spt/tests/contract_e2e.rs:181-232` exercises real `api seed` then `api listen` with the current live PID and successful bind.\n- `crates/spt/tests/listen_seed_retry_e2e.rs:65-162` exercises real daemon/CLI seed restoration after a recoverable refusal and corrected retry.\n- `crates/spt/tests/listen_seed_retry_e2e.rs:166-224` exercises real CLI `--session-id` fallback when no seed exists.\n\nThe only useful coverage addition would be a focused CLI E2E spelling out the exact dead-PID `SEEDED → STALE_SEED → NO_SEED` output sequence. Unit tests already defend all underlying observable contracts, including “SID fallback does not override a seed-path refusal.” This is a **test-only hardening opportunity**, not evidence of missing production behavior. Per the assignment, no tests were run and no files were edited.\n\n## Version/commit distinction\n\nCore's relevant F-034 seed/listen behavior is already released:\n\n- initial behavior commit `32b134ef314c84cc737124085c615cf8a2ab606d` (`feat(msg-identity): W2 F-034 - listen bringup-bind legs`);\n- test/rework commit `53b1d59e1e0bcdd3c5e3aa501015e879fceba179`;\n- included by core tag `v0.31.0` at `c9586d1734ef2bed887d5725265940cf567228ba`;\n- therefore also included by field core `v0.32.0` tag `6f9df02d3ef85a8bb1077a74690880dacf f1e404` (repository ref renders the hash without the displayed spacing: `6f9df02d3ef85a8bb1077a74690880dacff1e404`).\n\nThe Windows real-Claude-PID defect is adapter-owned and already fixed after the field adapter version:\n\n- field adapter `v0.22.0` tag: `b61c03144080d12130eea0a8d785b878032eada1`;\n- fix behavior commit: `ddc1334be8b41c42e9aa3165fd9701860f8dfa5e` (`feat: v0.24.0 — wake emit flip, win32 live anchor...`);\n- adapter `v0.24.0` tag: `58110a4b5a2269555386abcd96ac352972177593`;\n- `BigscreenVR/claude-spt-bs/src/hook.rs:921-922` seeds with the adapter's `host_anchor` result rather than trusting inherited dispatch identity;\n- `src/hook.rs:999-1004` exports the resolved PID as `SPT_HOST_PID`;\n- `src/hook.rs:1890-1952` implements the Windows Toolhelp process snapshot;\n- `src/hook.rs:2161-2169` performs the hook-process ancestry walk;\n- `BigscreenVR/claude-spt-bs/manifest.toml:509-522` records that v0.24 heals both `api seed --pid` and `SPT_HOST_PID` with the real Claude Windows PID.\n\nThat makes the stale/dead PID symptom on adapter 0.22.0 an **already-fixed adapter defect**, not a new core defect. The clean repair is adapter upgrade to at least v0.24.0 plus a Claude/hook bounce so the running hook binary and exported environment are actually replaced. No spt-core workaround is warranted.\n\nOWL session identity has different ownership:\n\n- adapter `src/hook.rs:829` reads SID directly from the Claude hook payload;\n- `src/hook.rs:987` exports that payload SID as `OWL_SESSION_ID`;\n- v0.24 PID self-resolution does **not** add a new SID-freshness resolver.\n\nThus, if `OWL_SESSION_ID` remains stale after confirming the current adapter binary and a fresh SessionStart execution, it is a separate payload/env-file lifecycle defect to trace at the Claude hook/adapter projection boundary. Core cannot manufacture the current Claude SID and should not silently substitute one. This distinction is important: stale PID on field v0.22 is already fixed in adapter v0.24; stale SID on a genuinely current hook execution is not fixed by that PID patch, but it is also not core-owned.\n\n## Final recommendation\n\n- **spt-core code change: NO.** Preserve explicit-value precedence, consume-once custody, bind-time liveness, and `NoSeed`-only SID fallback.\n- **spt-core fail-loud addition: NO requirement.** `listen` already emits the authoritative `STALE_SEED`; seed-time liveness would be race-prone duplicate validation.\n- **spt-core self-resolution addition: NO.** Generic direct-parent resolution is appropriate only when no override exists; real-Claude ancestry across Windows elevation/wrappers is adapter-specific and already implemented in adapter v0.24.\n- **Clean incident fix:** update/bounce the field adapter from 0.22.0 to current; verify the running hook path/version and freshly generated `SPT_HOST_PID`. If SID remains stale, capture the fresh SessionStart payload and adapter env-file output and fix that source/projection lifecycle—do not override it in core.\n- **Optional non-behavior follow-ups:** clarify hook-fill ownership in manifest docs; add the exact dead-PID CLI diagnostic sequence as regression coverage.",
  "files": [
    {
      "path": "crates/spt/src/api/startup.rs",
      "description": "Primary seed/listen contract: seed ACK, take-before-bind custody, stale/empty spend policy, explicit PID precedence, NoSeed-only SID fallback, bind gates, diagnostics, and comprehensive unit tests."
    },
    {
      "path": "crates/spt-daemon/src/seedmap.rs",
      "description": "In-memory PID-keyed seed registry and IPC: replacement on put, removal on take, consume-once tests."
    },
    {
      "path": "crates/spt-store/src/seed.rs",
      "description": "Shared `Seed` wire/data contract: parent PID, SID, optional cwd, creation timestamp; documents ephemeral daemon custody."
    },
    {
      "path": "crates/spt-store/src/proc.rs",
      "description": "Cross-platform PID liveness and parent/process ancestry primitives; Windows liveness is `OpenProcess`, while generic listen fallback uses the current process's direct parent."
    },
    {
      "path": "crates/spt/src/api/auth.rs",
      "description": "Local API bearer/capability security model and live-owner/custody refusal gates."
    },
    {
      "path": "crates/spt-runtime/src/manifest.rs",
      "description": "Manifest schema: hooks are opaque outbound `fires` declarations with `reads`/`can_inject`; identity metadata is parsed but does not create an inbound hook executor."
    },
    {
      "path": "crates/spt-runtime/src/runtime.rs",
      "description": "Core-executed runtime template substitution catalog/fill primitives; no SessionStart hook dispatcher."
    },
    {
      "path": "docs-site/src/harness-contract/api.md",
      "description": "Published seed lifetime, stale/empty spend policy, SID bearer-string warning, and local `--parent-pid` override posture."
    },
    {
      "path": "docs-site/src/harness-contract/integration-checklist.md",
      "description": "Explicit ownership statement that harness/adapter executes hooks and core never runs hook handlers."
    },
    {
      "path": "docs-site/src/harness-contract/manifest.md",
      "description": "Manifest reference with a broad `fires` substitution sentence that should be clarified against actual harness-owned dispatch."
    },
    {
      "path": "crates/spt/tests/contract_e2e.rs",
      "description": "Current-live-PID real CLI seed→listen happy-path coverage."
    },
    {
      "path": "crates/spt/tests/listen_seed_retry_e2e.rs",
      "description": "Real CLI coverage for recoverable seed restoration and NoSeed session-ID fallback."
    },
    {
      "path": "BigscreenVR/claude-spt-bs/src/hook.rs",
      "description": "Private adapter ownership: Claude hook payload SID projection and v0.24 Windows real-host-PID ancestry resolution used for both seeding and `SPT_HOST_PID`."
    },
    {
      "path": "BigscreenVR/claude-spt-bs/manifest.toml",
      "description": "Adapter release history documenting the v0.24 Windows host-anchor correction."
    }
  ],
  "architecture": "Harness-hosted startup is deliberately split across ownership boundaries: Claude/adapter SessionStart obtains the harness payload and resolves the real host anchor, then calls `spt api seed(pid, sid)`; the always-on spt daemon keeps that record only in an in-memory PID-keyed map; later `spt api listen` uses an explicit parent PID if supplied (otherwise only core's generic direct PPID), atomically takes the seed, validates nonempty SID and live anchor, resolves the adapter by the live host executable when no adapter override was supplied, and establishes the perch under live-owner/custody conflict gates. Recoverable pre-bind configuration refusals restore the seed; identity-invalid evidence (dead PID/empty SID) spends it. Direct SID binding is a separate NoSeed-only local bearer escape hatch. Manifest hook tables describe adapter/harness outbound invocation; core parses them but does not execute SessionStart or resolve Claude-specific hook identities. Therefore the Windows elevation/wrapper ancestry problem belongs in the Claude adapter (fixed in v0.24), while core owns fail-closed bind-time validation and must not reinterpret explicit stale identity."
}