59 lines
4.7 KiB
JavaScript

import { existsSync } from 'node:fs';
import { writeFile, realpath } from 'node:fs/promises';
import { join, dirname } from 'node:path';
const seatbelt = value => JSON.stringify(value);
export async function sandboxCommand(options) {
const { platform = process.platform, runtimeDir, root, token, brokerUrl, uid } = options;
const home = join(root, 'home'); const workspace = join(root, 'workspace');
const temp = join(root, 'tmp'); const node = options.nodePath;
const common = { LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', NO_COLOR: '1',
CANVAS_AGENT_RUN_TOKEN: token, NODE_OPTIONS: '--max-old-space-size=512' };
if (platform === 'linux') {
if (process.getuid?.() !== 0 || !Number.isSafeInteger(uid) || uid < 200000) throw new Error('Linux Agent 沙箱需要受控的独立运行 UID');
for (const file of ['/usr/bin/bwrap', '/usr/bin/setpriv', '/usr/bin/systemd-run', '/usr/bin/prlimit'])
if (!existsSync(file)) throw new Error('缺少 Linux Agent 沙箱组件,禁止宿主直接执行');
const args = ['--quiet', '--scope', '--collect', `--unit=yedir-canvas-${options.runId}`,
'--property=MemoryMax=1073741824', '--property=TasksMax=128', '--property=CPUQuota=200%',
'/usr/bin/prlimit', '--core=0:0', '--', '/usr/bin/setpriv', `--reuid=${uid}`, `--regid=${uid}`, '--clear-groups',
'/usr/bin/bwrap', '--unshare-all', '--new-session', '--die-with-parent', '--cap-drop', 'ALL', '--clearenv',
'--ro-bind', '/usr', '/usr', '--ro-bind', '/lib64', '/lib64', '--symlink', 'usr/bin', '/bin', '--symlink', 'usr/lib', '/lib',
'--ro-bind', runtimeDir, '/runtime', '--bind', home, '/home/agent', '--bind', workspace, '/workspace',
'--tmpfs', '/tmp', '--proc', '/proc', '--dev', '/dev', '--dir', '/run',
'--ro-bind', join(root, 'broker.sock'), '/run/broker.sock', '--dir', '/etc',
'--ro-bind', join(root, 'passwd'), '/etc/passwd', '--ro-bind', join(root, 'group'), '/etc/group',
'--chdir', '/workspace'];
const env = { ...common, PATH: '/runtime/bin:/usr/bin:/bin', HOME: '/home/agent', USER: 'agent', LOGNAME: 'agent',
TMPDIR: '/tmp', ZCODE_DATA_BASE_DIR: '/home/agent', ZCODE_STORAGE_DIR: '/home/agent/.zcode/cli' };
for (const [key, value] of Object.entries(env)) args.push('--setenv', key, value);
args.push('--', '/runtime/bin/node', '/runtime/bridge.mjs', 'launch', '/runtime/zcode.cjs', ...options.cliArgs);
return { command: '/usr/bin/systemd-run', args, cwd: root,
env: { PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' }, kind: 'linux-bwrap', workspace, home };
}
if (platform === 'darwin') {
if (!existsSync('/usr/bin/sandbox-exec')) throw new Error('macOS 沙箱不可用,禁止宿主直接执行');
if (!brokerUrl?.startsWith('http://127.0.0.1:')) throw new Error('macOS 沙箱缺少私有模型网关');
const port = new URL(brokerUrl).port;
const readPaths = await Promise.all([runtimeDir, dirname(dirname(node))].map(path => realpath(path)));
const writePaths = await Promise.all([home, workspace, temp].map(path => realpath(path)));
const profile = ['(version 1)', '(deny default)', '(allow process-exec process-fork)',
'(allow process-info* (target same-sandbox))', '(allow signal (target same-sandbox))',
'(allow mach-priv-task-port (target same-sandbox))', '(allow sysctl-read)',
'(allow sysctl-write (sysctl-name "kern.tcsm_enable"))',
'(allow mach-lookup (global-name "com.apple.system.logger") (global-name "com.apple.logd") (global-name "com.apple.system.opendirectoryd.libinfo"))',
'(allow file-read-metadata)', '(allow file-read* (literal "/"))',
...['/System', '/usr', '/bin', '/sbin', '/dev', '/private/etc', '/private/var/db/dyld', '/Library/Apple', '/Library/Developer/CommandLineTools', ...readPaths].map(path => `(allow file-read* (subpath ${seatbelt(path)}))`),
...writePaths.map(path => `(allow file-read* file-write* (subpath ${seatbelt(path)}))`),
'(allow file-write* (literal "/dev/null"))',
'(allow file-ioctl (literal "/dev/null"))',
`(allow network-outbound (remote ip "localhost:${port}"))`,
].join('\n');
const profilePath = join(root, 'sandbox.sb'); await writeFile(profilePath, profile, { mode: 0o600 });
return { command: '/usr/bin/sandbox-exec', args: ['-f', profilePath, node, join(runtimeDir, 'zcode.cjs'), ...options.cliArgs],
cwd: workspace, env: { ...common, PATH: `${join(runtimeDir, 'bin')}:/usr/bin:/bin`, HOME: home, USERPROFILE: home,
TMPDIR: temp, ZCODE_DATA_BASE_DIR: home, ZCODE_STORAGE_DIR: join(home, '.zcode/cli'),
CANVAS_AGENT_BROKER_URL: brokerUrl }, kind: 'macos-seatbelt', workspace, home };
}
throw new Error('当前平台没有配置独立 Agent 沙箱,禁止宿主直接执行');
}