fix super admin claude: host cwd, host mcp config paths

Capture HOST_HOME before user-instance overrides process.env.HOME so
Super Admin spawns claude in /home/pastilhas. Generate separate MCP
configs for sandbox (sandbox paths) and host (real filesystem paths),
pick based on role at spawn time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 01:29:14 +00:00
co-authored by Claude Opus 4.6
parent 29229c8cb7
commit 8264e7b995
3 changed files with 50 additions and 25 deletions
-1
View File
@@ -225,7 +225,6 @@ async function ensureClaudeSidecar(email: string): Promise<RegisteredSidecar> {
} }
async function spawnAndWaitForRegistration(email: string, name: string): Promise<RegisteredSidecar> { async function spawnAndWaitForRegistration(email: string, name: string): Promise<RegisteredSidecar> {
// Get proxy secret for auth
const proxySecret = await getProxySecret(); const proxySecret = await getProxySecret();
const proxyPort = process.env.ANTHROPIC_PROXY_PORT ?? '5051'; const proxyPort = process.env.ANTHROPIC_PROXY_PORT ?? '5051';
+17 -11
View File
@@ -12,6 +12,8 @@ const SEND_TIMEOUT_MS = 5 * 60 * 1000;
// The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude. // The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude.
const CLAUDE_BIN = '/usr/local/bin/claude'; const CLAUDE_BIN = '/usr/local/bin/claude';
// Capture original HOME before user-instance overrides it
const HOST_HOME = process.env.HOME!;
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
// Build full bwrap sandbox args for Claude (prefix + Anthropic env + runuser suffix) // Build full bwrap sandbox args for Claude (prefix + Anthropic env + runuser suffix)
@@ -28,11 +30,13 @@ function buildSandboxArgs(email: string): string[] {
// Active streaming processes // Active streaming processes
const activeProcs = new Map<string, Subprocess>(); const activeProcs = new Map<string, Subprocess>();
// MCP config path, set by user-instance at startup // MCP config paths, set by user-instance at startup
let mcpConfigPath: string | undefined; let mcpSandboxPath: string | undefined; // path inside bwrap sandbox (/data/...)
let mcpHostPath: string | undefined; // path on the host filesystem
export function setMcpConfigPath(path: string): void { export function setMcpConfigPath(sandboxPath: string, hostPath: string): void {
mcpConfigPath = path; mcpSandboxPath = sandboxPath;
mcpHostPath = hostPath;
} }
// ── Blocking send ── // ── Blocking send ──
@@ -60,7 +64,9 @@ export async function spawnClaude(params: ClaudeSpawnParams): Promise<ClaudeCode
'json', 'json',
]; ];
if (mcpConfigPath) claudeArgs.push('--mcp-config', mcpConfigPath); const isSuperAdmin = params.role === 'Super Admin';
const mcpConfig = isSuperAdmin ? mcpHostPath : mcpSandboxPath;
if (mcpConfig) claudeArgs.push('--mcp-config', mcpConfig);
const subModel = params.model?.split('/')[1]; const subModel = params.model?.split('/')[1];
if (subModel) claudeArgs.push('--model', subModel); if (subModel) claudeArgs.push('--model', subModel);
@@ -69,9 +75,8 @@ export async function spawnClaude(params: ClaudeSpawnParams): Promise<ClaudeCode
claudeArgs.push('--resume', existingSession); claudeArgs.push('--resume', existingSession);
} }
const isSuperAdmin = params.role === 'Super Admin';
const spawnCmd = isSuperAdmin ? claudeArgs : [...buildSandboxArgs(email), ...claudeArgs]; const spawnCmd = isSuperAdmin ? claudeArgs : [...buildSandboxArgs(email), ...claudeArgs];
const spawnCwd = isSuperAdmin ? (process.env.HOME ?? join(DATA_PATH, email, 'home')) : undefined; const spawnCwd = isSuperAdmin ? HOST_HOME : undefined;
const proc = Bun.spawn(spawnCmd, { const proc = Bun.spawn(spawnCmd, {
stdin: 'pipe', stdin: 'pipe',
@@ -155,7 +160,10 @@ export async function spawnClaudeStreaming(
'--include-partial-messages', '--include-partial-messages',
]; ];
if (mcpConfigPath) claudeArgs.push('--mcp-config', mcpConfigPath); const { CLAUDECODE: _, ...cleanEnv } = process.env;
const isSuperAdmin = params.role === 'Super Admin';
const mcpConfig = isSuperAdmin ? mcpHostPath : mcpSandboxPath;
if (mcpConfig) claudeArgs.push('--mcp-config', mcpConfig);
const subModel = params.model?.split('/')[1]; const subModel = params.model?.split('/')[1];
if (subModel) claudeArgs.push('--model', subModel); if (subModel) claudeArgs.push('--model', subModel);
@@ -164,10 +172,8 @@ export async function spawnClaudeStreaming(
claudeArgs.push('--resume', existingSession); claudeArgs.push('--resume', existingSession);
} }
const { CLAUDECODE: _, ...cleanEnv } = process.env;
const isSuperAdmin = params.role === 'Super Admin';
const spawnCmd = isSuperAdmin ? claudeArgs : [...buildSandboxArgs(email), ...claudeArgs]; const spawnCmd = isSuperAdmin ? claudeArgs : [...buildSandboxArgs(email), ...claudeArgs];
const spawnCwd = isSuperAdmin ? (process.env.HOME ?? join(DATA_PATH, email, 'home')) : undefined; const spawnCwd = isSuperAdmin ? HOST_HOME : undefined;
const proc = Bun.spawn(spawnCmd, { const proc = Bun.spawn(spawnCmd, {
stdin: 'ignore', stdin: 'ignore',
+33 -13
View File
@@ -53,34 +53,54 @@ function refreshClaudeMd(): void {
// ── MCP config ── // ── MCP config ──
function generateMcpConfig(): string { type McpPaths = { sandboxPath: string; hostPath: string };
function generateMcpConfig(): McpPaths {
const contextDir = join(DATA_PATH, email!, '.container-context'); const contextDir = join(DATA_PATH, email!, '.container-context');
mkdirSync(contextDir, { recursive: true }); mkdirSync(contextDir, { recursive: true });
const configPath = join(contextDir, 'mcp.json');
// Inside the sandbox, user data is mounted at SANDBOX_DATA (/data) const userRoot = join(DATA_PATH, email!);
// Global tools stay at their original paths (mounted read-only at same path)
const sandboxUserToolsDir = `${SANDBOX_DATA}/tools`;
const toolsDirs = [globalToolsDir, ...(existsSync(userToolsDir) ? [sandboxUserToolsDir] : [])].join(':');
const config = { // Sandbox config (paths relative to /data mount)
const sandboxToolsDirs = [globalToolsDir, ...(existsSync(userToolsDir) ? [`${SANDBOX_DATA}/tools`] : [])].join(':');
const sandboxConfig = {
mcpServers: { mcpServers: {
'officer-tools': { 'officer-tools': {
type: 'stdio', type: 'stdio',
command: 'bun', command: 'bun',
args: ['run', MCP_SERVER_SCRIPT], args: ['run', MCP_SERVER_SCRIPT],
env: { env: {
PI_TOOLS_DIRS: toolsDirs, PI_TOOLS_DIRS: sandboxToolsDirs,
OFFICER_EMAIL_DB: `${SANDBOX_DATA}/emails.db`, OFFICER_EMAIL_DB: `${SANDBOX_DATA}/emails.db`,
MCP_TOOLS_LOG: `${SANDBOX_DATA}/logs/mcp-tools.log`, MCP_TOOLS_LOG: `${SANDBOX_DATA}/logs/mcp-tools.log`,
}, },
}, },
}, },
}; };
writeFileSync(join(contextDir, 'mcp.json'), JSON.stringify(sandboxConfig));
writeFileSync(configPath, JSON.stringify(config)); // Host config (real filesystem paths, for Super Admin)
// Return the sandbox path (config file is inside user data, mounted at /data) const hostToolsDirs = [globalToolsDir, ...(existsSync(userToolsDir) ? [userToolsDir] : [])].join(':');
return `${SANDBOX_DATA}/.container-context/mcp.json`; const hostConfig = {
mcpServers: {
'officer-tools': {
type: 'stdio',
command: 'bun',
args: ['run', MCP_SERVER_SCRIPT],
env: {
PI_TOOLS_DIRS: hostToolsDirs,
OFFICER_EMAIL_DB: join(userRoot, 'emails.db'),
MCP_TOOLS_LOG: join(userRoot, 'logs', 'mcp-tools.log'),
},
},
},
};
writeFileSync(join(contextDir, 'mcp-host.json'), JSON.stringify(hostConfig));
return {
sandboxPath: `${SANDBOX_DATA}/.container-context/mcp.json`,
hostPath: join(contextDir, 'mcp-host.json'),
};
} }
// ── Startup ── // ── Startup ──
@@ -91,8 +111,8 @@ try {
console.error(`[claude:${email}] failed to refresh CLAUDE.md:`, err instanceof Error ? err.message : err); console.error(`[claude:${email}] failed to refresh CLAUDE.md:`, err instanceof Error ? err.message : err);
} }
const mcpPath = generateMcpConfig(); const mcpPaths = generateMcpConfig();
setMcpConfigPath(mcpPath); setMcpConfigPath(mcpPaths.sandboxPath, mcpPaths.hostPath);
console.log(`[claude:${email}] started (HOME=${homeDir})`); console.log(`[claude:${email}] started (HOME=${homeDir})`);