import { existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; // Resolve paths for sandbox const BUN_DIR = (() => { const result = Bun.spawnSync({ cmd: ['which', 'bun'], stdout: 'pipe', stderr: 'ignore' }); const binDir = dirname(result.stdout.toString().trim()); return dirname(binDir); // e.g. /home/pastilhas/.bun })(); const PROJECT_ROOT = resolve(import.meta.dir, '../../..'); const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); const HOST_HOME = process.env.HOME!; // Resolve the OS username for runuser to drop privileges inside the sandbox const OS_USERNAME = (() => { const result = Bun.spawnSync({ cmd: ['id', '-un'], stdout: 'pipe', stderr: 'ignore' }); return result.stdout.toString().trim() || 'pastilhas'; })(); // Sandbox mount points export const SANDBOX_DATA = '/data'; export const SANDBOX_HOME = `${SANDBOX_DATA}/home`; export const SANDBOX_GLOBAL_ROOT = '/officer'; export const SANDBOX_GLOBAL_SKILLS = `${SANDBOX_GLOBAL_ROOT}/skills`; export const SANDBOX_GLOBAL_EXTENSIONS = `${SANDBOX_GLOBAL_ROOT}/extensions`; export const SANDBOX_GLOBAL_TOOLS = `${SANDBOX_GLOBAL_ROOT}/tools`; // Build bwrap sandbox prefix for a given user email. // Returns args up to (but not including) the `-- runuser` suffix. // Callers can append extra `--setenv` args before calling `buildRunuserSuffix()`. export function buildSandboxPrefix(email: string): string[] { const userDataDir = join(DATA_PATH, email); const globalSkillsDir = join(DATA_PATH, 'skills'); const globalToolsDir = join(DATA_PATH, 'tools'); const globalExtensionsDir = join(DATA_PATH, 'extensions'); const args = [ 'sudo', 'bwrap', '--share-net', '--die-with-parent', '--proc', '/proc', '--dev', '/dev', '--perms', '1777', '--tmpfs', '/tmp', // System (read-only) '--ro-bind', '/usr', '/usr', '--ro-bind', '/lib', '/lib', '--ro-bind', '/bin', '/bin', '--ro-bind', '/etc', '/etc', // /run is needed for systemd-resolved DNS (resolv.conf symlink target) '--ro-bind', '/run', '/run', ]; // Optional system paths if (existsSync('/lib64')) args.push('--ro-bind', '/lib64', '/lib64'); if (existsSync('/sbin')) args.push('--ro-bind', '/sbin', '/sbin'); // Ensure intermediate dirs under HOME are traversable after runuser drops privileges // (bwrap auto-creates them as root-owned drwx------) const homeDir = HOST_HOME; args.push('--perms', '0755', '--dir', homeDir); // Bun runtime (e.g. /home/pastilhas/.bun) args.push('--ro-bind', BUN_DIR, BUN_DIR); // User-local installs (~/.local) — claude binary, pi npm packages, etc. const localDir = join(homeDir, '.local'); if (existsSync(localDir)) { args.push('--ro-bind', localDir, localDir); } // Project source (for MCP server) args.push('--ro-bind', PROJECT_ROOT, PROJECT_ROOT); // Ensure DATA_PATH intermediate dirs are traversable (same issue as HOME) args.push('--perms', '0755', '--dir', DATA_PATH); // Global content mounted at original paths for existing host-path references if (existsSync(globalSkillsDir)) args.push('--ro-bind', globalSkillsDir, globalSkillsDir); if (existsSync(globalToolsDir)) args.push('--ro-bind', globalToolsDir, globalToolsDir); if (existsSync(globalExtensionsDir)) args.push('--ro-bind', globalExtensionsDir, globalExtensionsDir); // Ensure sandbox-local global root is traversable before mounting nested paths under it. args.push('--perms', '0755', '--dir', SANDBOX_GLOBAL_ROOT); // Global content also mounted at short sandbox-local paths so nested imports do not // depend on traversing host-specific parent directories created by bwrap. if (existsSync(globalSkillsDir)) args.push('--ro-bind', globalSkillsDir, SANDBOX_GLOBAL_SKILLS); if (existsSync(globalToolsDir)) args.push('--ro-bind', globalToolsDir, SANDBOX_GLOBAL_TOOLS); if (existsSync(globalExtensionsDir)) args.push('--ro-bind', globalExtensionsDir, SANDBOX_GLOBAL_EXTENSIONS); // User data (read-write, mounted at /data to avoid intermediate dir permission issues) args.push('--bind', userDataDir, SANDBOX_DATA); // Common env vars inside the sandbox (sudo strips the environment) args.push('--setenv', 'HOME', SANDBOX_HOME); args.push('--setenv', 'PATH', process.env.PATH ?? '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'); // Set working directory inside the sandbox args.push('--chdir', SANDBOX_HOME); return args; } // Build the runuser suffix that drops privileges to the OS user. // Append this after any extra --setenv args. export function buildRunuserSuffix(): string[] { return ['--', 'runuser', '--preserve-environment', '-u', OS_USERNAME, '--']; }