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:
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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})`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user