give a member a chat cwd they can actually enter
host captured the first live member turn: uid 1001, nine env vars, zero ANTHROPIC_*, zero POSTGRES_URL, zero JWT_SECRET. The privilege drop and the allowlist both held. One defect. The default chat cwd was DATA_PATH/<email>/general_chat_sessions — a sibling of the member's home, which confineUserTree deliberately makes the platform's at 0700 because the other siblings are attachments and email_accounts. So the turn ran in a directory the member cannot enter, and every Bash call failed on its own cwd. The agent reported its shell as broken, which was true. A member's default is now ~member/general_chat_sessions, created as them through runAs. mkdir -p, so it is idempotent per turn and needs no reprovision. The owner's path does not change, and the sibling stays 0700 — loosening it would trade a broken shell for an open directory holding attachments and mail. 29 said this path is email-derived and therefore stays email-derived. True, and it did not follow that it is usable: an email-derived path under DATA_PATH is precisely the set a member is locked out of. Splitting identity from filesystem path was right; assuming the identity side was inert was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +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 { claudeLoginState, ensureMemberChatCwd } from '@@/os-user-claude';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -138,9 +138,16 @@ async function resolveChatCwd(
|
||||
email: string,
|
||||
userId: number,
|
||||
home: string,
|
||||
member?: { osUser: string; home: string },
|
||||
): Promise<string> {
|
||||
if (msg.context === 'email') return resolveEmailCwd(userId, email, msg.contextId);
|
||||
if (msg.context === 'chat') return msg.cwd?.trim() ? resolveCwd(home, msg.cwd) : ensureGeneralChatSessionsCwd(email);
|
||||
if (msg.context === 'chat') {
|
||||
if (msg.cwd?.trim()) return resolveCwd(home, msg.cwd);
|
||||
// The default chat directory. The owner's is a sibling of their home; a member's is INSIDE theirs,
|
||||
// because that sibling is platform-owned at 0700 by design and a member cannot enter it — which is
|
||||
// exactly how the first live member turn failed, with every Bash call dying on its own cwd.
|
||||
return member ? ensureMemberChatCwd(member) : ensureGeneralChatSessionsCwd(email);
|
||||
}
|
||||
return resolveCwd(home, msg.cwd);
|
||||
}
|
||||
|
||||
@@ -395,7 +402,7 @@ async function handleClaudeCodeChat(
|
||||
|
||||
const home = identity.kind === 'member' ? identity.run.home : getOwnerHomeDir(email);
|
||||
|
||||
const cwd = await resolveChatCwd(msg, email, userId, home);
|
||||
const cwd = await resolveChatCwd(msg, email, userId, home, identity.kind === 'member' ? identity.run : undefined);
|
||||
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user