diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index b23d5b16..71a8823e 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -45,10 +45,14 @@ export const Footer: React.FC = ({ justifyContent="center" display="flex" > - {process.env.SANDBOX ? ( + {process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? ( {process.env.SANDBOX} + ) : process.env.SANDBOX === 'sandbox-exec' ? ( + + using macos seatbelt ({process.env.SANDBOX_EXEC_PROFILE}) + ) : ( - WARNING: OUTSIDE SANDBOX + WARNING: SANDBOX NOT ENABLED )} diff --git a/packages/cli/src/utils/sandbox-macos-minimal.sb b/packages/cli/src/utils/sandbox-macos-minimal.sb new file mode 100644 index 00000000..c304b838 --- /dev/null +++ b/packages/cli/src/utils/sandbox-macos-minimal.sb @@ -0,0 +1,15 @@ +(version 1) + +;; allow everything by default +(allow default) + +;; deny all writes EXCEPT under project directory, temp directory, stdout/stderr and /dev/null +(deny file-write*) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) + diff --git a/packages/cli/src/utils/sandbox-macos-strict.sb b/packages/cli/src/utils/sandbox-macos-strict.sb new file mode 100644 index 00000000..4c7c2df0 --- /dev/null +++ b/packages/cli/src/utils/sandbox-macos-strict.sb @@ -0,0 +1,82 @@ +(version 1) + +;; deny everything by default +(deny default) + +;; allow reading files from anywhere on host +(allow file-read*) + +;; allow exec/fork (children inherit policy) +(allow process-exec) +(allow process-fork) + +;; allow signals to self, e.g. SIGPIPE on write to closed pipe +(allow signal (target self)) + +;; allow read access to specific information about system +;; from https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd +(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_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_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (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.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) + +;; allow writes to project directory, temp directory, stdout/stderr and /dev/null +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) + +;; allow outbound network connections +(allow network-outbound) + +;; allow communication with sysmond for process listing (e.g. for pgrep) +(allow mach-lookup (global-name "com.apple.sysmond")) + +;; enable terminal access required by ink +;; fixes setRawMode EPERM failure (at node:tty:81:24) +(allow file-ioctl (regex #"^/dev/tty.*")) diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 508ce368..697ff7e3 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -45,6 +45,14 @@ export function sandbox_command(sandbox?: string | boolean): string { process.exit(1); } } else { + // if we are on macOS (Darwin) and sandbox-exec is available, use that for minimal sandboxing + if ( + os.platform() === 'darwin' && + execSync('command -v sandbox-exec || true').toString().trim() + ) { + return 'sandbox-exec'; + } + return ''; // no sandbox } } @@ -133,6 +141,27 @@ function entrypoint(workdir: string): string[] { } export async function start_sandbox(sandbox: string) { + if (sandbox === 'sandbox-exec') { + process.env.SANDBOX_EXEC_PROFILE ??= 'minimal'; + const args = [ + '-D', + `TARGET_DIR=${process.cwd()}`, + '-D', + `TMP_DIR=${fs.realpathSync(os.tmpdir())}`, + '-f', + new URL( + `sandbox-macos-${process.env.SANDBOX_EXEC_PROFILE}.sb`, + import.meta.url, + ).pathname, + 'bash', + '-c', + 'SANDBOX=sandbox-exec ' + + process.argv.map((arg) => quote([arg])).join(' '), + ]; + spawnSync(sandbox, args, { stdio: 'inherit' }); + return; + } + // determine full path for gemini-code to distinguish linked vs installed setting const gcPath = execSync(`realpath $(which gemini-code)`).toString().trim(); diff --git a/scripts/build_package.sh b/scripts/build_package.sh index b536723f..28053a89 100755 --- a/scripts/build_package.sh +++ b/scripts/build_package.sh @@ -27,7 +27,7 @@ fi tsc --build # copy .{md,json} files (replace -q with -i to see itemized changes) -rsync -aq --delete --include='*.md' --include='*.json' --include='*/' --exclude='*' ./src/ ./dist/src/ +rsync -aq --delete --include='*.md' --include='*.json' --include='*.sb' --include='*/' --exclude='*' ./src/ ./dist/src/ # touch dist/.last_build touch dist/.last_build