let members use chat
The owner authorized this explicitly. Two refusals removed together, because
they were always one guard in two places: the wholesale isSuperAdmin middleware
in api/chat/chat.ts, and the chat socket's 403 in server.tsx.
They were right for the day they stood. A turn spawned claude as the OWNER and
every transcript path resolved through the owner's home, so a granted member
would have read the owner's sessions and run an agent as them.
What replaced them, rather than what deleted them:
the turn runs as the member spawnClaudeAsMember through sudo setpriv,
proven against a real account by reading file
ownership rather than trusting the process
the credential is theirs --reset-env plus an allowlist, so the owner's
proxy variables cannot cross
the transcripts are theirs ChatIdentity carries a home from resolveHomeDir
and claude-sessions cannot invent one
the sessions are theirs every session records its owner and all six
sidecar commands refuse a mismatch
Also adds the precondition host asked for in 10: a member whose claude is not
signed in gets the instruction rather than a turn that dies on an auth error and
reads as a broken agent. Not installed and not signed in are separate messages
because they need different actions.
registry.ts and registry.test.ts now describe chat as confined in fact rather
than ahead of its implementation. The comments at both former guards say what
had to exist first, and that a revert should go back to a refusal rather than to
a narrower one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -177,10 +177,10 @@ describe('kinds', () => {
|
||||
// cannot be asserted from the registry, so this pins the inverse: nothing becomes confined without a
|
||||
// deliberate edit here, and the list stays short enough to audit by eye.
|
||||
//
|
||||
// `chat` is on it ahead of its implementation, deliberately and temporarily: the capability is grantable so
|
||||
// the route resolves, while api/chat/chat.ts and the chat socket both refuse a non-owner because a turn
|
||||
// would still spawn the agent as the owner. If you are here because this test failed, check that those two
|
||||
// guards moved together with whatever you changed.
|
||||
// `chat` was on this list ahead of its implementation for a day, with `api/chat/chat.ts` and the chat socket
|
||||
// both refusing non-owners because a turn still spawned the agent as the owner. Both refusals were removed
|
||||
// on 2026-08-12, together, once the turn ran under `runAs` with the member's own home, credential,
|
||||
// transcripts and session ownership. `chat` is now confined in fact and not only in the registry.
|
||||
test('confined is a short, deliberate list', () => {
|
||||
expect(CAPABILITIES.filter((c) => c.kind === 'confined').map((c) => c.key)).toEqual(['terminal', 'chat', 'files']);
|
||||
});
|
||||
|
||||
@@ -272,11 +272,10 @@ export const CAPABILITIES: Capability[] = [
|
||||
ws: ['terminal'],
|
||||
routes: ['/terminal'],
|
||||
},
|
||||
// Confined so the owner can grant it and the route resolves — but the agent underneath still runs as the
|
||||
// OWNER, so `api/chat/chat.ts` refuses a non-owner outright and the chat socket is refused in server.tsx.
|
||||
// A deliberate, temporary gap: the permission exists, the functionality follows when a turn can be spawned
|
||||
// under `runAs` with the member's own HOME. Until then this grant buys a route and a refusal, and the
|
||||
// comments at both guards say so.
|
||||
// Confined, and true since 2026-08-12: a member's turn spawns their own `claude` as their own Linux account
|
||||
// through `sudo setpriv`, with their own `~/.claude` credential, their own transcripts and sessions that
|
||||
// record whose they are. The two owner-only refusals that held this open — `api/chat/chat.ts` and the socket
|
||||
// in `server.tsx` — were removed together once each of those was in place.
|
||||
{
|
||||
key: 'chat',
|
||||
label: 'Chat',
|
||||
|
||||
Reference in New Issue
Block a user