populate member from the authenticated socket
The last mechanical link: chat socket -> resolveMemberRun(userId) -> ClaudeSpawnStreamingParams.member -> claude-manager's branch -> spawnClaudeAsMember -> sudo setpriv. The path from a request to a privilege drop is now complete. Resolved from the authenticated socket, never from the client message — the same rule server.tsx applies to the pty sidecar, where it deletes any client-supplied osUser/home from the query string before setting its own. resolveMemberRun returns undefined rather than throwing when a home cannot be resolved, because undefined means "the owner" downstream: an account with no Linux user has nothing to confine a turn to, and falling back to the owner is the one wrong answer that must not happen by accident. A separate function with that reasoning attached rather than an inline ternary. Still inert. Both gates refuse non-owners before this line is reached, so the only path that reaches it today returns undefined via isOwner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ import { ensureGeneralChatSessionsCwd } from './claude-sessions';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { join } from 'path';
|
||||
import { getOwnerHomeDir, getEmailAccountsDir } from '../../../servers/data-path';
|
||||
import { getUserSettings, getEmailAccounts, getChatEventsSince, appendChatEvent } from 'officerdb';
|
||||
import { getUserSettings, getEmailAccounts, getChatEventsSince, appendChatEvent, getUserById } from 'officerdb';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -28,6 +29,21 @@ const DEFAULT_MODEL = 'claude-code';
|
||||
// (opencode/anthropic/openai/… — all `providerID/modelID` ids) runs through the OpenCode server.
|
||||
const isClaudeModel = (model: string): boolean => model.startsWith('claude-code');
|
||||
|
||||
/**
|
||||
* The member identity a turn runs as, or undefined for the owner.
|
||||
*
|
||||
* Returns undefined rather than throwing when the home cannot be resolved: an account with no Linux user has
|
||||
* nothing to confine a turn to, and the caller must not silently fall back to running it as the owner. Since
|
||||
* both gates still refuse non-owners, the only path that reaches this today returns undefined via `isOwner`.
|
||||
*/
|
||||
async function resolveMemberRun(userId: number): Promise<{ osUser: string; home: string } | undefined> {
|
||||
const resolved = await resolveHomeDir(userId);
|
||||
if (!resolved.ok || resolved.isOwner) return undefined;
|
||||
const row = await getUserById(userId);
|
||||
if (!row?.osUser) return undefined;
|
||||
return { osUser: row.osUser, home: resolved.home };
|
||||
}
|
||||
|
||||
async function getUserDefaultModel(userId: number): Promise<string | null> {
|
||||
try {
|
||||
const settings = await getUserSettings(userId);
|
||||
@@ -359,6 +375,15 @@ async function handleClaudeCodeChat(
|
||||
sessionKey: sessionId,
|
||||
cwd,
|
||||
model,
|
||||
// Whose Linux account this turn runs as. Resolved here from the authenticated socket, never from the
|
||||
// client message — the same rule `server.tsx` applies to the pty sidecar, where it deletes any
|
||||
// client-supplied `osUser`/`home` from the query string before setting its own.
|
||||
//
|
||||
// Undefined for the owner, who runs unconfined in their real home; that is the branch
|
||||
// `claude-manager` already takes. Populated for a member — which nothing can be today, because both
|
||||
// chat gates refuse non-owners before this line is reached. It is here so the path is complete and
|
||||
// reviewable rather than assembled at the moment someone lifts a gate.
|
||||
member: await resolveMemberRun(userId),
|
||||
resumeSessionId: msg.resumeSessionId,
|
||||
onMessage,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user