Commit Graph
2 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 c5522700ff compare the binary as a string, and prove the uid by file ownership
host ran the live test against green. Two results.

SETPRIV WORKS. The privilege drop lands on the member and the SDK spawn survives
it, so the design does not change shape and everything layered on the hook
stays. That was the last question that could have moved the architecture.

THE BINARY CHECK REFUSED A BYTE-IDENTICAL PATH. sameFile used realpathSync,
which has to stat inside a 700 home the platform is `other` to, so it threw
EACCES and the catch turned that into "not their binary" — every member turn
refused forever, the moment the gates moved. Failing closed was the right
direction and it made the feature impossible rather than unsafe.

Now resolve(command) !== claudeBinIn(run.home). Both operands are computed by
the platform from the same function, so string equality establishes exactly what
the check is for and needs no access to their home. sameFile and its tests are
deleted: a helper kept for a case that cannot arise is a trap for the next
reader. The realpath version was defending against an upstream that normalises
paths, and there is no such upstream — the platform controls both ends.

The fact that decides this is that the check runs in the PLATFORM process, not
the member's, and neither of us stated it until EACCES did.

THE TEST WATCHED THE WRONG PROCESS. child.pid is sudo, whose real uid is
legitimately the service user's until it execs down through setpriv, so
asserting on it fails on a working drop. Split: --version through the hook
proves their binary ran, and a second spawn creates a file in their home whose
owner the test stats. Nothing self-reports, and a process cannot forge the uid
that owns a file it created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:03:17 +00:00
pastilhasandClaude Opus 5 9bab67aca5 test the privilege drop without lifting a gate
host caught a circularity I had written twice: do not lift the chat gates until
a member turn has been watched running, but a member turn goes through chat and
chat refuses non-owners. With the gates up there is nothing to watch; with them
down the thing we wanted proven has already shipped.

spawn-as-member.live.test.ts calls spawnClaudeAsMember directly against a real
provisioned account — no gate, no chat, no SDK. The child's uid is read from
/proc/<pid>/status, so it is the kernel's answer rather than anything the child
chose to say, and it asserts >=1000 and not this process's uid: a failed
privilege drop cannot pass by running as the service user. It also asserts the
binary exited 0 having printed a version, which proves their install ran rather
than merely being spawned, plus a negative that /bin/sh through the same hook
throws.

Opt-in via OFFICER_TEST_MEMBER and OFFICER_TEST_MEMBER_HOME, because it needs a
provisioned member with claude installed — which exists on the production host
and on no developer machine. A run without them skips loudly rather than
reporting an empty file as a pass.

Also adopted host's NO REPLY NEEDED terminator: "reply to everything" had no
exit condition and cost the owner two agents being polite at each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:57:18 +00:00