Files
platform/src/servers/sidecar/claude/spawn-as-member.test.ts
T
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

44 lines
2.2 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { PERMITTED_ENV, assertEnvSafe } from './spawn-as-member';
// Pins the two guards standing between a member's agent turn and the owner's credential.
//
// Both of them shipped dead before this test existed, and in both cases the reason was the same: they were
// written inside the spawn closure, where the only way to reach them is to spawn, and the passing path spawns
// `sudo`. So nothing ever demonstrated them firing, and "it looks right" carried the weight. That is what
// these tests are for — not the happy path, which is obvious, but the two edits a future reader will actually
// make.
const OWNER_CRED = 'ANTHROPIC_API_KEY';
describe('assertEnvSafe', () => {
test('the shipping lists are clean', () => {
// The regression this exists to catch: somebody adds a credential to ALLOWED_ENV.
expect(() => assertEnvSafe(PERMITTED_ENV, { HOME: '/home/x' }, 'x')).not.toThrow();
});
test('throws when a credential is in the allowlist — guards the constant', () => {
// The realistic dangerous edit. Note the credential is NOT in childEnv: the point is that the *list*
// permits it, so the next call that inherits one would pass it through silently.
const poisoned = new Set([...PERMITTED_ENV, OWNER_CRED]);
expect(() => assertEnvSafe(poisoned, { HOME: '/home/x' }, 'green')).toThrow(/is in the allowlist/);
});
test('throws when the built env carries an unvetted key — guards the construction', () => {
expect(() => assertEnvSafe(PERMITTED_ENV, { HOME: '/home/x', POSTGRES_URL: 'postgres://…' }, 'green')).toThrow(
/unvetted env/,
);
});
test('names the offending variable, so the error is actionable', () => {
expect(() => assertEnvSafe(PERMITTED_ENV, { JWT_SECRET: 'x' }, 'green')).toThrow(/JWT_SECRET/);
});
test('every NEVER_ENV name would be caught if it were permitted', () => {
// Guards against the list being quietly narrowed as well as the allowlist being widened.
for (const name of ['ANTHROPIC_BASE_URL', 'CLAUDE_CODE_OAUTH_TOKEN', 'POSTGRES_URL', 'JWT_SECRET']) {
expect(() => assertEnvSafe(new Set([...PERMITTED_ENV, name]), {}, 'green')).toThrow(name);
}
});
});