126 lines
4.7 KiB
TypeScript
126 lines
4.7 KiB
TypeScript
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, '--'];
|
|
}
|