resolve transcripts and cwd against the caller's home, not the owner's

The history layer, and the last change that could be made without a live member.

claude-sessions.ts had `claudeHome = process.env.HOME_DIR ?? join(DATA_PATH,
email, 'home')`, which discards its argument whenever HOME_DIR is set — always,
on a real install. Every transcript read therefore resolved to the OWNER'S
~/.claude no matter who asked, and the comment above it asserted "single-user
platform" as though that were a property rather than an assumption. A member
reaching these functions would have been handed the owner's conversation list.

Now every read takes a ChatIdentity {email, home} with the home resolved from
resolveHomeDir(userId), and this file has no way to invent one. Both fields
travel together because they are genuinely different: general_chat_sessions
lives under DATA_PATH/<email>, not under a home. Collapsing them would be the
same class of mistake as undefined meaning "the owner".

websocket.ts's resolveCwd takes a home, so `~` expands against the caller's own.
Identity is resolved BEFORE the cwd — expanding `~` before knowing whose home it
is would be exactly the bug being removed — which also let a duplicate
resolveTurnIdentity call from 6aeb304 be deleted.

chat.ts resolves per request and throws FORBIDDEN rather than falling back, same
posture as resolveTurnIdentity. agent-runner passes the owner's home explicitly
rather than inheriting it, since that path really is owner-only.

Made at 01:00 after saying it should not be. Three things I am least sure of are
listed in COMMS 29 rather than left for the reviewer to find: chat routes now
have a failure mode they did not have, resolveBaseCwd's exported parameter
changed meaning rather than shape, and the bare-email rewrite in chat.ts was
mechanical with hand repair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 00:48:09 +00:00
co-authored by Claude Opus 5
parent 519109a342
commit 95951fbe5a
5 changed files with 191 additions and 87 deletions
+9 -3
View File
@@ -87,7 +87,7 @@ export function buildAgentPrompt(agent: AgentRecord, inputs: Record<string, unkn
* which is fine: while a run is live you find it as the newest entry in the agent's project group.
*/
function titleRun(
email: string,
who: { email: string; home: string },
cwd: string,
claudeSessionId: string,
agentName: string,
@@ -99,7 +99,7 @@ function titleRun(
const title = [agentName, subject, when].filter(Boolean).join(' · ');
try {
if (!renameClaudeSession(email, cwd, claudeSessionId, title)) {
if (!renameClaudeSession(who, cwd, claudeSessionId, title)) {
logger.warn('Could not title agent run — transcript not found', { claudeSessionId, cwd });
}
} catch (err) {
@@ -171,7 +171,13 @@ export async function startAgentRun(params: StartAgentRunParams): Promise<StartA
run.finishedAt = Date.now();
if (msg.claudeSessionId) {
run.claudeSessionId = msg.claudeSessionId;
titleRun(params.user.email, cwd, msg.claudeSessionId, agent.name || agent.dirName, inputs);
titleRun(
{ email: params.user.email, home: homeDir },
cwd,
msg.claudeSessionId,
agent.name || agent.dirName,
inputs,
);
}
} else if (msg.type === 'error') {
run.status = 'failed';