shared bwrap sandbox, skip for super admin, extend to pi and terminals

- extract buildSandboxPrefix/buildRunuserSuffix into shared sandbox.ts
- super admin bypasses bwrap for full host access (claude, pi, terminal)
- member pi processes now use bwrap instead of sudo -u
- member terminals now use bwrap instead of sudo -u
- mount /run for systemd-resolved DNS inside sandbox
- pass role through claude spawn params and channel types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 00:06:44 +00:00
co-authored by Claude Opus 4.6
parent c38d5b0ea1
commit 9578110e8b
9 changed files with 386 additions and 180 deletions
+95
View File
@@ -0,0 +1,95 @@
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');
// 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 point for user data (short path avoids intermediate dir traversal issues)
export const SANDBOX_DATA = '/data';
export const SANDBOX_HOME = `${SANDBOX_DATA}/home`;
// 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 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');
// Bun runtime (e.g. /home/pastilhas/.bun)
args.push('--ro-bind', BUN_DIR, BUN_DIR);
// Project source (for MCP server)
args.push('--ro-bind', PROJECT_ROOT, PROJECT_ROOT);
// Global tools/extensions (read-only, mounted at original paths for MCP config references)
if (existsSync(globalToolsDir)) args.push('--ro-bind', globalToolsDir, globalToolsDir);
if (existsSync(globalExtensionsDir)) args.push('--ro-bind', globalExtensionsDir, globalExtensionsDir);
// 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, '--'];
}