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:
2026-08-12 01:12:00 +00:00
co-authored by Claude Opus 5
parent ba64a412f2
commit c59df4f866
6 changed files with 94 additions and 34 deletions
+25
View File
@@ -19,6 +19,7 @@ import { join } from 'path';
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
import { getUserSettings, getEmailAccounts, getChatEventsSince, appendChatEvent, getUserById } from 'officerdb';
import { resolveHomeDir } from '@@/user-home';
import { claudeLoginState } from '@@/os-user-claude';
import { mkdirSync } from 'node:fs';
import { logger } from './logger';
@@ -368,6 +369,30 @@ async function handleClaudeCodeChat(
return;
}
// A member has to sign `claude` in themselves, once, with their own Anthropic account — the platform cannot
// do it for them without lending them the owner's credential, which is the thing this whole feature exists
// to avoid. Without this check their turn spawns, `claude` exits on an auth error, and it surfaces as "the
// agent is broken" — the exact confusion `/agent-status` was built to prevent, arriving through a different
// door. The refusal carries the instruction so the answer is the same whether the UI asked or not.
if (identity.kind === 'member') {
const state = await claudeLoginState({ email, osUser: identity.run.osUser });
if (!state.installed) {
sendToClient(ws, {
type: 'error',
message: 'Claude is not installed in your home yet — ask the server owner to reprovision your account.',
});
return;
}
if (!state.loggedIn) {
sendToClient(ws, {
type: 'error',
message:
'Open a terminal and run `claude` once to sign in with your own Anthropic account. It stays signed in.',
});
return;
}
}
const home = identity.kind === 'member' ? identity.run.home : getOwnerHomeDir(email);
const cwd = await resolveChatCwd(msg, email, userId, home);