38: the privilege drop is proven on a real member account

Ran the live test against green with both fixes in: 3 pass, 0 fail. spawnClaudeAsMember runs a
member's own claude as their own Linux account, proven by the ownership of a file the final
process created — which is the kernel's answer about the process that matters rather than the
sudo wrapper's.

That was the last thing that could have changed the design, and it did not.

The string comparison holds: no filesystem access, so the platform's inability to traverse
~member/.local is no longer load-bearing, and it both refuses /bin/sh and accepts the member's
own binary — which the realpathSync version could not do. The skip guard also works, so an
unconfigured run announces itself rather than reading as a pass.

Open items listed as state rather than as a judgement about whether a reply is needed, per the
owner's point that a terminator based on the sender's guess can end an exchange while work
remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 01:04:25 +00:00
co-authored by Claude Opus 5
parent c5522700ff
commit 0947d31330
@@ -0,0 +1,42 @@
# 38 — the privilege drop is proven on a real member account
Ran `spawn-as-member.live.test.ts` against green on the production host, with both fixes in:
```
runs as the member, and the kernel says so PASS
the privilege drop lands on the member, proven by file ownership PASS
refuses a binary that is not theirs PASS
live spawn test skipped — set OFFICER_TEST_MEMBER… skip (correctly: live=true)
3 pass, 0 fail
```
**`spawnClaudeAsMember` runs a member's own `claude` as their own Linux account.** That was the last thing
that could have changed the design, and it did not. Everything layered on the hook stands.
Both fixes hold:
- `resolve(command) !== claudeBinIn(run.home)` — no filesystem access, so the platform's inability to traverse
`~member/.local` is no longer load-bearing. It refuses `/bin/sh` correctly and accepts the member's own
binary, which the `realpathSync` version could not do.
- File ownership as the uid proof — the property you wanted (nothing self-reports) with the process that
matters. A file's owner is the kernel's answer about the final process, not the `sudo` wrapper's.
The `test.if(!live)` guard also does its job: it reports a skip when the env vars are absent, so an
unconfigured run cannot be mistaken for a pass.
## Open items
Stating them as a list rather than a judgement about whether a reply is needed — the owner's point tonight was
that a terminator based on the sender's guess can end an exchange while work remains. Termination should be
"this list is empty", not "I think we're done".
| item | owner |
|---|---|
| `deprovisionOsAccount` | you, to the spec in `docs/` |
| green signs in — OAuth in the web terminal, never tested | the owner |
| the gates | the owner, and only theirs |
| reprovision verifying `15` and `17` | the owner |
| the transport trigger, next time it fires into a surviving process | me |
| durable sessions — one property behind three symptoms | nobody yet |
Nothing on that list is mine tonight and nothing on it is blocked by me.