mirror of
https://github.com/tvytlx/ai-agent-deep-dive.git
synced 2026-04-13 04:24:49 +08:00
Add extracted source directory and README navigation
This commit is contained in:
630
extracted-source/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js
generated
vendored
Normal file
630
extracted-source/node_modules/@anthropic-ai/sandbox-runtime/dist/sandbox/macos-sandbox-utils.js
generated
vendored
Normal file
@@ -0,0 +1,630 @@
|
||||
import shellquote from 'shell-quote';
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { whichSync } from '../utils/which.js';
|
||||
import { normalizePathForSandbox, generateProxyEnvVars, encodeSandboxedCommand, decodeSandboxedCommand, containsGlobChars, globToRegex, DANGEROUS_FILES, getDangerousDirectories, } from './sandbox-utils.js';
|
||||
/**
|
||||
* Get mandatory deny patterns as glob patterns (no filesystem scanning).
|
||||
* macOS sandbox profile supports regex/glob matching directly via globToRegex().
|
||||
*/
|
||||
export function macGetMandatoryDenyPatterns(allowGitConfig = false) {
|
||||
const cwd = process.cwd();
|
||||
const denyPaths = [];
|
||||
// Dangerous files - static paths in CWD + glob patterns for subtree
|
||||
for (const fileName of DANGEROUS_FILES) {
|
||||
denyPaths.push(path.resolve(cwd, fileName));
|
||||
denyPaths.push(`**/${fileName}`);
|
||||
}
|
||||
// Dangerous directories
|
||||
for (const dirName of getDangerousDirectories()) {
|
||||
denyPaths.push(path.resolve(cwd, dirName));
|
||||
denyPaths.push(`**/${dirName}/**`);
|
||||
}
|
||||
// Git hooks are always blocked for security
|
||||
denyPaths.push(path.resolve(cwd, '.git/hooks'));
|
||||
denyPaths.push('**/.git/hooks/**');
|
||||
// Git config - conditionally blocked based on allowGitConfig setting
|
||||
if (!allowGitConfig) {
|
||||
denyPaths.push(path.resolve(cwd, '.git/config'));
|
||||
denyPaths.push('**/.git/config');
|
||||
}
|
||||
return [...new Set(denyPaths)];
|
||||
}
|
||||
const sessionSuffix = `_${Math.random().toString(36).slice(2, 11)}_SBX`;
|
||||
/**
|
||||
* Generate a unique log tag for sandbox monitoring
|
||||
* @param command - The command being executed (will be base64 encoded)
|
||||
*/
|
||||
function generateLogTag(command) {
|
||||
const encodedCommand = encodeSandboxedCommand(command);
|
||||
return `CMD64_${encodedCommand}_END_${sessionSuffix}`;
|
||||
}
|
||||
/**
|
||||
* Get all ancestor directories for a path, up to (but not including) root
|
||||
* Example: /private/tmp/test/file.txt -> ["/private/tmp/test", "/private/tmp", "/private"]
|
||||
*/
|
||||
function getAncestorDirectories(pathStr) {
|
||||
const ancestors = [];
|
||||
let currentPath = path.dirname(pathStr);
|
||||
// Walk up the directory tree until we reach root
|
||||
while (currentPath !== '/' && currentPath !== '.') {
|
||||
ancestors.push(currentPath);
|
||||
const parentPath = path.dirname(currentPath);
|
||||
// Break if we've reached the top (path.dirname returns the same path for root)
|
||||
if (parentPath === currentPath) {
|
||||
break;
|
||||
}
|
||||
currentPath = parentPath;
|
||||
}
|
||||
return ancestors;
|
||||
}
|
||||
/**
|
||||
* Generate deny rules for file movement (file-write-unlink) to protect paths
|
||||
* This prevents bypassing read or write restrictions by moving files/directories
|
||||
*
|
||||
* @param pathPatterns - Array of path patterns to protect (can include globs)
|
||||
* @param logTag - Log tag for sandbox violations
|
||||
* @returns Array of sandbox profile rule lines
|
||||
*/
|
||||
function generateMoveBlockingRules(pathPatterns, logTag) {
|
||||
const rules = [];
|
||||
for (const pathPattern of pathPatterns) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern);
|
||||
if (containsGlobChars(normalizedPath)) {
|
||||
// Use regex matching for glob patterns
|
||||
const regexPattern = globToRegex(normalizedPath);
|
||||
// Block moving/renaming files matching this pattern
|
||||
rules.push(`(deny file-write-unlink`, ` (regex ${escapePath(regexPattern)})`, ` (with message "${logTag}"))`);
|
||||
// For glob patterns, extract the static prefix and block ancestor moves
|
||||
// Remove glob characters to get the directory prefix
|
||||
const staticPrefix = normalizedPath.split(/[*?[\]]/)[0];
|
||||
if (staticPrefix && staticPrefix !== '/') {
|
||||
// Get the directory containing the glob pattern
|
||||
const baseDir = staticPrefix.endsWith('/')
|
||||
? staticPrefix.slice(0, -1)
|
||||
: path.dirname(staticPrefix);
|
||||
// Block moves of the base directory itself
|
||||
rules.push(`(deny file-write-unlink`, ` (literal ${escapePath(baseDir)})`, ` (with message "${logTag}"))`);
|
||||
// Block moves of ancestor directories
|
||||
for (const ancestorDir of getAncestorDirectories(baseDir)) {
|
||||
rules.push(`(deny file-write-unlink`, ` (literal ${escapePath(ancestorDir)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use subpath matching for literal paths
|
||||
// Block moving/renaming the denied path itself
|
||||
rules.push(`(deny file-write-unlink`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
// Block moves of ancestor directories
|
||||
for (const ancestorDir of getAncestorDirectories(normalizedPath)) {
|
||||
rules.push(`(deny file-write-unlink`, ` (literal ${escapePath(ancestorDir)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
/**
|
||||
* Generate filesystem read rules for sandbox profile
|
||||
*
|
||||
* Supports two layers:
|
||||
* 1. denyOnly: deny reads from these paths (broad regions like /Users)
|
||||
* 2. allowWithinDeny: re-allow reads within denied regions (like CWD)
|
||||
* allowWithinDeny takes precedence over denyOnly.
|
||||
*
|
||||
* In Seatbelt profiles, later rules take precedence, so we emit:
|
||||
* (allow file-read*) ← default: allow everything
|
||||
* (deny file-read* ...) ← deny broad regions
|
||||
* (allow file-read* ...) ← re-allow specific paths within denied regions
|
||||
*/
|
||||
function generateReadRules(config, logTag) {
|
||||
if (!config) {
|
||||
return [`(allow file-read*)`];
|
||||
}
|
||||
const rules = [];
|
||||
// Start by allowing everything
|
||||
rules.push(`(allow file-read*)`);
|
||||
// Then deny specific paths
|
||||
for (const pathPattern of config.denyOnly || []) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern);
|
||||
if (containsGlobChars(normalizedPath)) {
|
||||
// Use regex matching for glob patterns
|
||||
const regexPattern = globToRegex(normalizedPath);
|
||||
rules.push(`(deny file-read*`, ` (regex ${escapePath(regexPattern)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
else {
|
||||
// Use subpath matching for literal paths
|
||||
rules.push(`(deny file-read*`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
// Re-allow specific paths within denied regions (allowWithinDeny takes precedence)
|
||||
for (const pathPattern of config.allowWithinDeny || []) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern);
|
||||
if (containsGlobChars(normalizedPath)) {
|
||||
const regexPattern = globToRegex(normalizedPath);
|
||||
rules.push(`(allow file-read*`, ` (regex ${escapePath(regexPattern)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
else {
|
||||
rules.push(`(allow file-read*`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
// Allow stat/lstat on all directories so that realpath() can traverse
|
||||
// path components within denied regions. Without this, C realpath() fails
|
||||
// when resolving symlinks because it needs to lstat every intermediate
|
||||
// directory (e.g. /Users, /Users/chris) even if only a subdirectory like
|
||||
// ~/.local is in allowWithinDeny. This only allows metadata reads on
|
||||
// directories — not listing contents (readdir) or reading files.
|
||||
if ((config.denyOnly).length > 0) {
|
||||
rules.push(`(allow file-read-metadata`, ` (vnode-type DIRECTORY))`);
|
||||
}
|
||||
// Block file movement to prevent bypass via mv/rename
|
||||
rules.push(...generateMoveBlockingRules(config.denyOnly || [], logTag));
|
||||
return rules;
|
||||
}
|
||||
/**
|
||||
* Generate filesystem write rules for sandbox profile
|
||||
*/
|
||||
function generateWriteRules(config, logTag, allowGitConfig = false) {
|
||||
if (!config) {
|
||||
return [`(allow file-write*)`];
|
||||
}
|
||||
const rules = [];
|
||||
// Automatically allow TMPDIR parent on macOS when write restrictions are enabled
|
||||
const tmpdirParents = getTmpdirParentIfMacOSPattern();
|
||||
for (const tmpdirParent of tmpdirParents) {
|
||||
const normalizedPath = normalizePathForSandbox(tmpdirParent);
|
||||
rules.push(`(allow file-write*`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
// Generate allow rules
|
||||
for (const pathPattern of config.allowOnly || []) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern);
|
||||
if (containsGlobChars(normalizedPath)) {
|
||||
// Use regex matching for glob patterns
|
||||
const regexPattern = globToRegex(normalizedPath);
|
||||
rules.push(`(allow file-write*`, ` (regex ${escapePath(regexPattern)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
else {
|
||||
// Use subpath matching for literal paths
|
||||
rules.push(`(allow file-write*`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
// Combine user-specified and mandatory deny patterns (no ripgrep needed on macOS)
|
||||
const denyPaths = [
|
||||
...(config.denyWithinAllow || []),
|
||||
...macGetMandatoryDenyPatterns(allowGitConfig),
|
||||
];
|
||||
for (const pathPattern of denyPaths) {
|
||||
const normalizedPath = normalizePathForSandbox(pathPattern);
|
||||
if (containsGlobChars(normalizedPath)) {
|
||||
// Use regex matching for glob patterns
|
||||
const regexPattern = globToRegex(normalizedPath);
|
||||
rules.push(`(deny file-write*`, ` (regex ${escapePath(regexPattern)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
else {
|
||||
// Use subpath matching for literal paths
|
||||
rules.push(`(deny file-write*`, ` (subpath ${escapePath(normalizedPath)})`, ` (with message "${logTag}"))`);
|
||||
}
|
||||
}
|
||||
// Block file movement to prevent bypass via mv/rename
|
||||
rules.push(...generateMoveBlockingRules(denyPaths, logTag));
|
||||
return rules;
|
||||
}
|
||||
/**
|
||||
* Generate complete sandbox profile
|
||||
*/
|
||||
function generateSandboxProfile({ readConfig, writeConfig, httpProxyPort, socksProxyPort, needsNetworkRestriction, allowUnixSockets, allowAllUnixSockets, allowLocalBinding, allowPty, allowGitConfig = false, enableWeakerNetworkIsolation = false, logTag, }) {
|
||||
const profile = [
|
||||
'(version 1)',
|
||||
`(deny default (with message "${logTag}"))`,
|
||||
'',
|
||||
`; LogTag: ${logTag}`,
|
||||
'',
|
||||
'; Essential permissions - based on Chrome sandbox policy',
|
||||
'; Process permissions',
|
||||
'(allow process-exec)',
|
||||
'(allow process-fork)',
|
||||
'(allow process-info* (target same-sandbox))',
|
||||
'(allow signal (target same-sandbox))',
|
||||
'(allow mach-priv-task-port (target same-sandbox))',
|
||||
'',
|
||||
'; User preferences',
|
||||
'(allow user-preference-read)',
|
||||
'',
|
||||
'; Mach IPC - specific services only (no wildcard)',
|
||||
'(allow mach-lookup',
|
||||
' (global-name "com.apple.audio.systemsoundserver")',
|
||||
' (global-name "com.apple.distributed_notifications@Uv3")',
|
||||
' (global-name "com.apple.FontObjectsServer")',
|
||||
' (global-name "com.apple.fonts")',
|
||||
' (global-name "com.apple.logd")',
|
||||
' (global-name "com.apple.lsd.mapdb")',
|
||||
' (global-name "com.apple.PowerManagement.control")',
|
||||
' (global-name "com.apple.system.logger")',
|
||||
' (global-name "com.apple.system.notification_center")',
|
||||
' (global-name "com.apple.system.opendirectoryd.libinfo")',
|
||||
' (global-name "com.apple.system.opendirectoryd.membership")',
|
||||
' (global-name "com.apple.bsd.dirhelper")',
|
||||
' (global-name "com.apple.securityd.xpc")',
|
||||
' (global-name "com.apple.coreservices.launchservicesd")',
|
||||
')',
|
||||
'',
|
||||
...(enableWeakerNetworkIsolation
|
||||
? [
|
||||
'; trustd.agent - needed for Go TLS certificate verification (weaker network isolation)',
|
||||
'(allow mach-lookup (global-name "com.apple.trustd.agent"))',
|
||||
]
|
||||
: []),
|
||||
'',
|
||||
'; POSIX IPC - shared memory',
|
||||
'(allow ipc-posix-shm)',
|
||||
'',
|
||||
'; POSIX IPC - semaphores for Python multiprocessing',
|
||||
'(allow ipc-posix-sem)',
|
||||
'',
|
||||
'; IOKit - specific operations only',
|
||||
'(allow iokit-open',
|
||||
' (iokit-registry-entry-class "IOSurfaceRootUserClient")',
|
||||
' (iokit-registry-entry-class "RootDomainUserClient")',
|
||||
' (iokit-user-client-class "IOSurfaceSendRight")',
|
||||
')',
|
||||
'',
|
||||
'; IOKit properties',
|
||||
'(allow iokit-get-properties)',
|
||||
'',
|
||||
"; Specific safe system-sockets, doesn't allow network access",
|
||||
'(allow system-socket (require-all (socket-domain AF_SYSTEM) (socket-protocol 2)))',
|
||||
'',
|
||||
'; sysctl - specific sysctls only',
|
||||
'(allow sysctl-read',
|
||||
' (sysctl-name "hw.activecpu")',
|
||||
' (sysctl-name "hw.busfrequency_compat")',
|
||||
' (sysctl-name "hw.byteorder")',
|
||||
' (sysctl-name "hw.cacheconfig")',
|
||||
' (sysctl-name "hw.cachelinesize_compat")',
|
||||
' (sysctl-name "hw.cpufamily")',
|
||||
' (sysctl-name "hw.cpufrequency")',
|
||||
' (sysctl-name "hw.cpufrequency_compat")',
|
||||
' (sysctl-name "hw.cputype")',
|
||||
' (sysctl-name "hw.l1dcachesize_compat")',
|
||||
' (sysctl-name "hw.l1icachesize_compat")',
|
||||
' (sysctl-name "hw.l2cachesize_compat")',
|
||||
' (sysctl-name "hw.l3cachesize_compat")',
|
||||
' (sysctl-name "hw.logicalcpu")',
|
||||
' (sysctl-name "hw.logicalcpu_max")',
|
||||
' (sysctl-name "hw.machine")',
|
||||
' (sysctl-name "hw.memsize")',
|
||||
' (sysctl-name "hw.ncpu")',
|
||||
' (sysctl-name "hw.nperflevels")',
|
||||
' (sysctl-name "hw.packages")',
|
||||
' (sysctl-name "hw.pagesize_compat")',
|
||||
' (sysctl-name "hw.pagesize")',
|
||||
' (sysctl-name "hw.physicalcpu")',
|
||||
' (sysctl-name "hw.physicalcpu_max")',
|
||||
' (sysctl-name "hw.tbfrequency_compat")',
|
||||
' (sysctl-name "hw.vectorunit")',
|
||||
' (sysctl-name "kern.argmax")',
|
||||
' (sysctl-name "kern.bootargs")',
|
||||
' (sysctl-name "kern.hostname")',
|
||||
' (sysctl-name "kern.maxfiles")',
|
||||
' (sysctl-name "kern.maxfilesperproc")',
|
||||
' (sysctl-name "kern.maxproc")',
|
||||
' (sysctl-name "kern.ngroups")',
|
||||
' (sysctl-name "kern.osproductversion")',
|
||||
' (sysctl-name "kern.osrelease")',
|
||||
' (sysctl-name "kern.ostype")',
|
||||
' (sysctl-name "kern.osvariant_status")',
|
||||
' (sysctl-name "kern.osversion")',
|
||||
' (sysctl-name "kern.secure_kernel")',
|
||||
' (sysctl-name "kern.tcsm_available")',
|
||||
' (sysctl-name "kern.tcsm_enable")',
|
||||
' (sysctl-name "kern.usrstack64")',
|
||||
' (sysctl-name "kern.version")',
|
||||
' (sysctl-name "kern.willshutdown")',
|
||||
' (sysctl-name "machdep.cpu.brand_string")',
|
||||
' (sysctl-name "machdep.ptrauth_enabled")',
|
||||
' (sysctl-name "security.mac.lockdown_mode_state")',
|
||||
' (sysctl-name "sysctl.proc_cputype")',
|
||||
' (sysctl-name "vm.loadavg")',
|
||||
' (sysctl-name-prefix "hw.optional.arm")',
|
||||
' (sysctl-name-prefix "hw.optional.arm.")',
|
||||
' (sysctl-name-prefix "hw.optional.armv8_")',
|
||||
' (sysctl-name-prefix "hw.perflevel")',
|
||||
' (sysctl-name-prefix "kern.proc.all")',
|
||||
' (sysctl-name-prefix "kern.proc.pgrp.")',
|
||||
' (sysctl-name-prefix "kern.proc.pid.")',
|
||||
' (sysctl-name-prefix "machdep.cpu.")',
|
||||
' (sysctl-name-prefix "net.routetable.")',
|
||||
')',
|
||||
'',
|
||||
'; V8 thread calculations',
|
||||
'(allow sysctl-write',
|
||||
' (sysctl-name "kern.tcsm_enable")',
|
||||
')',
|
||||
'',
|
||||
'; Distributed notifications',
|
||||
'(allow distributed-notification-post)',
|
||||
'',
|
||||
'; Specific mach-lookup permissions for security operations',
|
||||
'(allow mach-lookup (global-name "com.apple.SecurityServer"))',
|
||||
'',
|
||||
'; File I/O on device files',
|
||||
'(allow file-ioctl (literal "/dev/null"))',
|
||||
'(allow file-ioctl (literal "/dev/zero"))',
|
||||
'(allow file-ioctl (literal "/dev/random"))',
|
||||
'(allow file-ioctl (literal "/dev/urandom"))',
|
||||
'(allow file-ioctl (literal "/dev/dtracehelper"))',
|
||||
'(allow file-ioctl (literal "/dev/tty"))',
|
||||
'',
|
||||
'(allow file-ioctl file-read-data file-write-data',
|
||||
' (require-all',
|
||||
' (literal "/dev/null")',
|
||||
' (vnode-type CHARACTER-DEVICE)',
|
||||
' )',
|
||||
')',
|
||||
'',
|
||||
];
|
||||
// Network rules
|
||||
profile.push('; Network');
|
||||
if (!needsNetworkRestriction) {
|
||||
profile.push('(allow network*)');
|
||||
}
|
||||
else {
|
||||
// Allow local binding if requested
|
||||
// Use "*:*" instead of "localhost:*" because modern runtimes (Java, etc.) create
|
||||
// IPv6 dual-stack sockets by default. When binding such a socket to 127.0.0.1,
|
||||
// the kernel represents it as ::ffff:127.0.0.1 (IPv4-mapped IPv6). Seatbelt's
|
||||
// "localhost" filter only matches 127.0.0.1 and ::1, NOT ::ffff:127.0.0.1.
|
||||
// Using (local ip "*:*") is safe because it only matches the LOCAL endpoint —
|
||||
// internet-bound connections originate from non-loopback interfaces, so they
|
||||
// remain blocked by (deny default).
|
||||
if (allowLocalBinding) {
|
||||
profile.push('(allow network-bind (local ip "*:*"))');
|
||||
profile.push('(allow network-inbound (local ip "*:*"))');
|
||||
profile.push('(allow network-outbound (local ip "*:*"))');
|
||||
}
|
||||
// Unix domain sockets for local IPC (SSH agent, Docker, Gradle, etc.)
|
||||
// Three separate operations must be allowed:
|
||||
// 1. system-socket: socket(AF_UNIX, ...) syscall — creates the socket fd (no path context)
|
||||
// 2. network-bind: bind() to a local Unix socket path
|
||||
// 3. network-outbound: connect() to a remote Unix socket path
|
||||
// Note: (subpath ...) and (path-regex ...) are path-based filters that can only match
|
||||
// bind/connect operations — socket() creation has no path, so it requires system-socket.
|
||||
if (allowAllUnixSockets) {
|
||||
// Allow creating AF_UNIX sockets and all Unix socket paths
|
||||
profile.push('(allow system-socket (socket-domain AF_UNIX))');
|
||||
profile.push('(allow network-bind (local unix-socket (path-regex #"^/")))');
|
||||
profile.push('(allow network-outbound (remote unix-socket (path-regex #"^/")))');
|
||||
}
|
||||
else if (allowUnixSockets && allowUnixSockets.length > 0) {
|
||||
// Allow creating AF_UNIX sockets (required for any Unix socket use)
|
||||
profile.push('(allow system-socket (socket-domain AF_UNIX))');
|
||||
// Allow specific Unix socket paths
|
||||
for (const socketPath of allowUnixSockets) {
|
||||
const normalizedPath = normalizePathForSandbox(socketPath);
|
||||
profile.push(`(allow network-bind (local unix-socket (subpath ${escapePath(normalizedPath)})))`);
|
||||
profile.push(`(allow network-outbound (remote unix-socket (subpath ${escapePath(normalizedPath)})))`);
|
||||
}
|
||||
}
|
||||
// If both allowAllUnixSockets and allowUnixSockets are false/undefined/empty, Unix sockets are blocked by default
|
||||
// Allow localhost TCP operations for the HTTP proxy
|
||||
if (httpProxyPort !== undefined) {
|
||||
profile.push(`(allow network-bind (local ip "localhost:${httpProxyPort}"))`);
|
||||
profile.push(`(allow network-inbound (local ip "localhost:${httpProxyPort}"))`);
|
||||
profile.push(`(allow network-outbound (remote ip "localhost:${httpProxyPort}"))`);
|
||||
}
|
||||
// Allow localhost TCP operations for the SOCKS proxy
|
||||
if (socksProxyPort !== undefined) {
|
||||
profile.push(`(allow network-bind (local ip "localhost:${socksProxyPort}"))`);
|
||||
profile.push(`(allow network-inbound (local ip "localhost:${socksProxyPort}"))`);
|
||||
profile.push(`(allow network-outbound (remote ip "localhost:${socksProxyPort}"))`);
|
||||
}
|
||||
}
|
||||
profile.push('');
|
||||
// Read rules
|
||||
profile.push('; File read');
|
||||
profile.push(...generateReadRules(readConfig, logTag));
|
||||
profile.push('');
|
||||
// Write rules
|
||||
profile.push('; File write');
|
||||
profile.push(...generateWriteRules(writeConfig, logTag, allowGitConfig));
|
||||
// Pseudo-terminal (pty) support
|
||||
if (allowPty) {
|
||||
profile.push('');
|
||||
profile.push('; Pseudo-terminal (pty) support');
|
||||
profile.push('(allow pseudo-tty)');
|
||||
profile.push('(allow file-ioctl');
|
||||
profile.push(' (literal "/dev/ptmx")');
|
||||
profile.push(' (regex #"^/dev/ttys")');
|
||||
profile.push(')');
|
||||
profile.push('(allow file-read* file-write*');
|
||||
profile.push(' (literal "/dev/ptmx")');
|
||||
profile.push(' (regex #"^/dev/ttys")');
|
||||
profile.push(')');
|
||||
}
|
||||
return profile.join('\n');
|
||||
}
|
||||
/**
|
||||
* Escape path for sandbox profile using JSON.stringify for proper escaping
|
||||
*/
|
||||
function escapePath(pathStr) {
|
||||
return JSON.stringify(pathStr);
|
||||
}
|
||||
/**
|
||||
* Get TMPDIR parent directory if it matches macOS pattern /var/folders/XX/YYY/T/
|
||||
* Returns both /var/ and /private/var/ versions since /var is a symlink
|
||||
*/
|
||||
function getTmpdirParentIfMacOSPattern() {
|
||||
const tmpdir = process.env.TMPDIR;
|
||||
if (!tmpdir)
|
||||
return [];
|
||||
const match = tmpdir.match(/^\/(private\/)?var\/folders\/[^/]{2}\/[^/]+\/T\/?$/);
|
||||
if (!match)
|
||||
return [];
|
||||
const parent = tmpdir.replace(/\/T\/?$/, '');
|
||||
// Return both /var/ and /private/var/ versions since /var is a symlink
|
||||
if (parent.startsWith('/private/var/')) {
|
||||
return [parent, parent.replace('/private', '')];
|
||||
}
|
||||
else if (parent.startsWith('/var/')) {
|
||||
return [parent, '/private' + parent];
|
||||
}
|
||||
return [parent];
|
||||
}
|
||||
/**
|
||||
* Wrap command with macOS sandbox
|
||||
*/
|
||||
export function wrapCommandWithSandboxMacOS(params) {
|
||||
const { command, needsNetworkRestriction, httpProxyPort, socksProxyPort, allowUnixSockets, allowAllUnixSockets, allowLocalBinding, readConfig, writeConfig, allowPty, allowGitConfig = false, enableWeakerNetworkIsolation = false, binShell, } = params;
|
||||
// Determine if we have restrictions to apply
|
||||
// Read: denyOnly pattern - empty array means no restrictions
|
||||
// Write: allowOnly pattern - undefined means no restrictions, any config means restrictions
|
||||
const hasReadRestrictions = readConfig && readConfig.denyOnly.length > 0;
|
||||
const hasWriteRestrictions = writeConfig !== undefined;
|
||||
// No sandboxing needed
|
||||
if (!needsNetworkRestriction &&
|
||||
!hasReadRestrictions &&
|
||||
!hasWriteRestrictions) {
|
||||
return command;
|
||||
}
|
||||
const logTag = generateLogTag(command);
|
||||
const profile = generateSandboxProfile({
|
||||
readConfig,
|
||||
writeConfig,
|
||||
httpProxyPort,
|
||||
socksProxyPort,
|
||||
needsNetworkRestriction,
|
||||
allowUnixSockets,
|
||||
allowAllUnixSockets,
|
||||
allowLocalBinding,
|
||||
allowPty,
|
||||
allowGitConfig,
|
||||
enableWeakerNetworkIsolation,
|
||||
logTag,
|
||||
});
|
||||
// Generate proxy environment variables using shared utility
|
||||
const proxyEnvArgs = generateProxyEnvVars(httpProxyPort, socksProxyPort);
|
||||
// Use the user's shell (zsh, bash, etc.) to ensure aliases/snapshots work
|
||||
// Resolve the full path to the shell binary
|
||||
const shellName = binShell || 'bash';
|
||||
const shell = whichSync(shellName);
|
||||
if (!shell) {
|
||||
throw new Error(`Shell '${shellName}' not found in PATH`);
|
||||
}
|
||||
// Use `env` command to set environment variables - each VAR=value is a separate
|
||||
// argument that shellquote handles properly, avoiding shell quoting issues
|
||||
const wrappedCommand = shellquote.quote([
|
||||
'env',
|
||||
...proxyEnvArgs,
|
||||
'sandbox-exec',
|
||||
'-p',
|
||||
profile,
|
||||
shell,
|
||||
'-c',
|
||||
command,
|
||||
]);
|
||||
logForDebugging(`[Sandbox macOS] Applied restrictions - network: ${!!(httpProxyPort || socksProxyPort)}, read: ${readConfig
|
||||
? 'allowAllExcept' in readConfig
|
||||
? 'allowAllExcept'
|
||||
: 'denyAllExcept'
|
||||
: 'none'}, write: ${writeConfig
|
||||
? 'allowAllExcept' in writeConfig
|
||||
? 'allowAllExcept'
|
||||
: 'denyAllExcept'
|
||||
: 'none'}`);
|
||||
return wrappedCommand;
|
||||
}
|
||||
/**
|
||||
* Start monitoring macOS system logs for sandbox violations
|
||||
* Look for sandbox-related kernel deny events ending in {logTag}
|
||||
*/
|
||||
export function startMacOSSandboxLogMonitor(callback, ignoreViolations) {
|
||||
// Pre-compile regex patterns for better performance
|
||||
const cmdExtractRegex = /CMD64_(.+?)_END/;
|
||||
const sandboxExtractRegex = /Sandbox:\s+(.+)$/;
|
||||
// Pre-process ignore patterns for faster lookup
|
||||
const wildcardPaths = ignoreViolations?.['*'] || [];
|
||||
const commandPatterns = ignoreViolations
|
||||
? Object.entries(ignoreViolations).filter(([pattern]) => pattern !== '*')
|
||||
: [];
|
||||
// Stream and filter kernel logs for all sandbox violations
|
||||
// We can't filter by specific logTag since it's dynamic per command
|
||||
const logProcess = spawn('log', [
|
||||
'stream',
|
||||
'--predicate',
|
||||
`(eventMessage ENDSWITH "${sessionSuffix}")`,
|
||||
'--style',
|
||||
'compact',
|
||||
]);
|
||||
logProcess.stdout?.on('data', (data) => {
|
||||
const lines = data.toString().split('\n');
|
||||
// Get violation and command lines
|
||||
const violationLine = lines.find(line => line.includes('Sandbox:') && line.includes('deny'));
|
||||
const commandLine = lines.find(line => line.startsWith('CMD64_'));
|
||||
if (!violationLine)
|
||||
return;
|
||||
// Extract violation details
|
||||
const sandboxMatch = violationLine.match(sandboxExtractRegex);
|
||||
if (!sandboxMatch?.[1])
|
||||
return;
|
||||
const violationDetails = sandboxMatch[1];
|
||||
// Try to get command
|
||||
let command;
|
||||
let encodedCommand;
|
||||
if (commandLine) {
|
||||
const cmdMatch = commandLine.match(cmdExtractRegex);
|
||||
encodedCommand = cmdMatch?.[1];
|
||||
if (encodedCommand) {
|
||||
try {
|
||||
command = decodeSandboxedCommand(encodedCommand);
|
||||
}
|
||||
catch {
|
||||
// Failed to decode, continue without command
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always filter out noisey violations
|
||||
if (violationDetails.includes('mDNSResponder') ||
|
||||
violationDetails.includes('mach-lookup com.apple.diagnosticd') ||
|
||||
violationDetails.includes('mach-lookup com.apple.analyticsd')) {
|
||||
return;
|
||||
}
|
||||
// Check if we should ignore this violation
|
||||
if (ignoreViolations && command) {
|
||||
// Check wildcard patterns first
|
||||
if (wildcardPaths.length > 0) {
|
||||
const shouldIgnore = wildcardPaths.some(path => violationDetails.includes(path));
|
||||
if (shouldIgnore)
|
||||
return;
|
||||
}
|
||||
// Check command-specific patterns
|
||||
for (const [pattern, paths] of commandPatterns) {
|
||||
if (command.includes(pattern)) {
|
||||
const shouldIgnore = paths.some(path => violationDetails.includes(path));
|
||||
if (shouldIgnore)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Not ignored - report the violation
|
||||
callback({
|
||||
line: violationDetails,
|
||||
command,
|
||||
encodedCommand,
|
||||
timestamp: new Date(), // We could parse the timestamp from the log but this feels more reliable
|
||||
});
|
||||
});
|
||||
logProcess.stderr?.on('data', (data) => {
|
||||
logForDebugging(`[Sandbox Monitor] Log stream stderr: ${data.toString()}`);
|
||||
});
|
||||
logProcess.on('error', (error) => {
|
||||
logForDebugging(`[Sandbox Monitor] Failed to start log stream: ${error.message}`);
|
||||
});
|
||||
logProcess.on('exit', (code) => {
|
||||
logForDebugging(`[Sandbox Monitor] Log stream exited with code: ${code}`);
|
||||
});
|
||||
return () => {
|
||||
logForDebugging('[Sandbox Monitor] Stopping log monitor');
|
||||
logProcess.kill('SIGTERM');
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=macos-sandbox-utils.js.map
|
||||
Reference in New Issue
Block a user