diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84e6bc483e5..de43641f673 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -421,9 +421,9 @@ To debug the CLI's React-based UI, you can use React DevTools. On macOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open` profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that -restricts writes to the project folder but otherwise allows all other operations -and outbound network traffic ("open") by default. You can switch to a -`strict-open` profile (see +denies operations by default, confining writes to the project folder while +allowing broad file reads and outbound network traffic ("open") by default. You +can switch to a `strict-open` profile (see `packages/cli/src/utils/sandbox-macos-strict-open.sb`) that restricts both reads and writes to the working directory while allowing outbound network traffic by setting `SEATBELT_PROFILE=strict-open` in your environment or `.env` file. diff --git a/docs/cli/sandbox.md b/docs/cli/sandbox.md index 0eea995cbcd..5beb243aa15 100644 --- a/docs/cli/sandbox.md +++ b/docs/cli/sandbox.md @@ -87,8 +87,9 @@ preferred container solution. Lightweight, built-in sandboxing using `sandbox-exec`. -**Default profile**: `permissive-open` - restricts writes outside project -directory but allows most other operations. +**Default profile**: `permissive-open` - denies operations by default; confines +writes to the project directory while allowing broad file reads and network +access. Built-in profiles (set via `SEATBELT_PROFILE` env var): diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 293d99313c7..66785882cea 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -2736,10 +2736,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file. - Run the CLI once with this set to generate the file. - **`SEATBELT_PROFILE`** (macOS specific): - Switches the Seatbelt (`sandbox-exec`) profile on macOS. - - `permissive-open`: (Default) Restricts writes to the project folder (and a - few other folders, see - `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other - operations. + - `permissive-open`: (Default) Denies operations by default, confining writes + to the project folder (and a few other folders, see + `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) while allowing + broad file reads and network access. - `restrictive-open`: Declines operations by default, allows network. - `strict-open`: Restricts both reads and writes to the working directory, allows network. diff --git a/packages/cli/src/utils/sandbox-macos-permissive-open.sb b/packages/cli/src/utils/sandbox-macos-permissive-open.sb index 73b70212ee2..062634b1238 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-open.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-open.sb @@ -1,10 +1,74 @@ (version 1) -;; allow everything by default -(allow default) +;; permissive-open: uses (deny default) and explicitly allows the operations the +;; CLI needs, matching the restrictive-* / strict-* profiles. Keep the allow-list +;; minimal and reviewed; do not switch to (allow default). +;; +;; Keep the non-network rules in sync with sandbox-macos-permissive-proxied.sb: +;; the two profiles are intentionally identical except for their network rules +;; ("open" allows broad outbound; "proxied" routes outbound through the proxy). +(deny default) -;; deny all writes EXCEPT under specific paths -(deny file-write*) +;; allow reading files from anywhere on host +(allow file-read*) + +;; allow exec/fork (children inherit this policy, so they stay sandboxed) +(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 only to specific paths (deny default already blocks the rest) (allow file-write* (subpath (param "TARGET_DIR")) (subpath (param "TMP_DIR")) @@ -24,3 +88,48 @@ (literal "/dev/ptmx") (regex #"^/dev/ttys[0-9]*$") ) + +;; allow the mach services normal workflows need under deny-default: sysmond for +;; process listing (pgrep), plus DNS resolution (mDNSResponder), directory +;; services (opendirectoryd), and certificate validation (trustd/ocspd). +;; This set mirrors the deny-default profile in +;; packages/core/src/sandbox/macos/baseProfile.ts (its NETWORK_SEATBELT_PROFILE), +;; which restrictive-open reaches only implicitly via (allow network-outbound). +;; Keep this allow-list minimal and reviewed. +(allow mach-lookup + (global-name "com.apple.sysmond") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.mDNSResponder") + (global-name "com.apple.mDNSResponderHelper") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd") +) + +;; AF_SYSTEM socket used by the network stack (from baseProfile.ts) +(allow system-socket + (require-all + (socket-domain AF_SYSTEM) + (socket-protocol 2) + ) +) + +;; enable terminal access required by ink +;; fixes setRawMode EPERM failure (at node:tty:81:24) +(allow file-ioctl (regex #"^/dev/tty.*")) + +;; allow inbound network traffic (local dev/test servers, the debugger on :9229, +;; OAuth localhost callbacks) +(allow network-inbound (local ip "*:*")) + +;; allow binding local ports (dev/test servers, OAuth localhost listeners) +(allow network-bind (local ip "*:*")) + +;; allow all outbound network traffic +(allow network-outbound) diff --git a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb index 61e09d4e173..ef0157f4628 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb @@ -1,10 +1,76 @@ (version 1) -;; allow everything by default -(allow default) +;; permissive-proxied: uses (deny default) and explicitly allows the operations +;; the CLI needs, matching the restrictive-* / strict-* profiles. Keep the +;; allow-list minimal and reviewed; do not switch to (allow default). +;; +;; Keep the non-network rules in sync with sandbox-macos-permissive-open.sb: +;; the two profiles are intentionally identical except for their network rules +;; ("open" allows broad outbound; "proxied" routes outbound through the proxy). +(deny default) -;; deny all writes EXCEPT under specific paths -(deny file-write*) +;; allow reading files from anywhere on host +(allow file-read*) + +;; allow exec/fork (children inherit this policy, so they stay sandboxed) +(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 only to specific paths (deny default already blocks the rest). +;; Mirrors permissive-open, including /dev/ptmx and the /dev/ttys regex needed +;; for PTY support under deny default. (allow file-write* (subpath (param "TARGET_DIR")) (subpath (param "TMP_DIR")) @@ -21,16 +87,52 @@ (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") + (literal "/dev/ptmx") + (regex #"^/dev/ttys[0-9]*$") +) + +;; allow the mach services normal workflows need under deny-default: sysmond for +;; process listing (pgrep), plus DNS resolution (mDNSResponder), directory +;; services (opendirectoryd), and certificate validation (trustd/ocspd). +;; This set mirrors the deny-default profile in +;; packages/core/src/sandbox/macos/baseProfile.ts (its NETWORK_SEATBELT_PROFILE), +;; which restrictive-proxied reaches only implicitly via (allow network-outbound). +;; Keep this allow-list minimal and reviewed. +(allow mach-lookup + (global-name "com.apple.sysmond") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.mDNSResponder") + (global-name "com.apple.mDNSResponderHelper") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd") ) -;; deny all inbound network traffic EXCEPT on debugger port -(deny network-inbound) +;; AF_SYSTEM socket used by the network stack (from baseProfile.ts) +(allow system-socket + (require-all + (socket-domain AF_SYSTEM) + (socket-protocol 2) + ) +) + +;; enable terminal access required by ink +;; fixes setRawMode EPERM failure (at node:tty:81:24) +(allow file-ioctl (regex #"^/dev/tty.*")) + +;; allow inbound network traffic on debugger port (allow network-inbound (local ip "localhost:9229")) +;; allow binding local ports (dev/test servers, OAuth localhost listeners) +(allow network-bind (local ip "*:*")) + ;; deny all outbound network traffic EXCEPT through proxy on localhost:8877 ;; set `GEMINI_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox ;; proxy must listen on :::8877 (see docs/examples/proxy-script.md) -(deny network-outbound) (allow network-outbound (remote tcp "localhost:8877")) - -(allow network-bind (local ip "*:*")) diff --git a/packages/cli/src/utils/sandbox-macos-profiles.test.ts b/packages/cli/src/utils/sandbox-macos-profiles.test.ts new file mode 100644 index 00000000000..9c447eae33b --- /dev/null +++ b/packages/cli/src/utils/sandbox-macos-profiles.test.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const utilsDir = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Strip SBPL comments (`; ...` to end of line) so assertions run against the + * actual sandbox rules rather than any keywords that happen to appear in the + * explanatory comments. + */ +function readRules(profile: string): string { + return readFileSync(path.join(utilsDir, profile), 'utf8') + .split('\n') + .map((line) => { + const commentStart = line.indexOf(';'); + return commentStart === -1 ? line : line.slice(0, commentStart); + }) + .join('\n'); +} + +const PERMISSIVE_PROFILES = [ + 'sandbox-macos-permissive-open.sb', + 'sandbox-macos-permissive-proxied.sb', +]; + +// These two profiles are the default macOS Seatbelt profiles, so the invariants +// below must never silently regress. Keep them deny-default and confirm the +// reviewed allow-list stays in place. +describe('macOS permissive Seatbelt profiles', () => { + describe.each(PERMISSIVE_PROFILES)('%s', (profile) => { + const rules = readRules(profile); + + it('uses a deny-default foundation', () => { + expect(rules).toContain('(deny default)'); + }); + + it('does not use an allow-default foundation', () => { + expect(rules).not.toContain('(allow default)'); + }); + + it('does not permit filesystem (un)mounts', () => { + expect(rules).not.toMatch(/file-mount/); + expect(rules).not.toMatch(/file-unmount/); + }); + + it('does not grant broad service lookups', () => { + expect(rules).not.toMatch(/launchd/); + expect(rules).not.toMatch(/launchservices/i); + }); + + it('allows binding local ports for dev/test servers', () => { + expect(rules).toContain('(allow network-bind (local ip "*:*"))'); + }); + }); + + it('permissive-open keeps broad inbound and outbound network', () => { + const rules = readRules('sandbox-macos-permissive-open.sb'); + expect(rules).toContain('(allow network-inbound (local ip "*:*"))'); + expect(rules).toMatch(/\(allow network-outbound\)/); + }); + + it('permissive-proxied confines outbound to the proxy', () => { + const rules = readRules('sandbox-macos-permissive-proxied.sb'); + expect(rules).toContain( + '(allow network-outbound (remote tcp "localhost:8877"))', + ); + // Proxied mode must never grant unrestricted outbound network. + expect(rules).not.toMatch(/\(allow network-outbound\)/); + }); +});