refactor(cli): align macOS permissive Seatbelt profiles with deny-default model (#28424)

This commit is contained in:
Om Patel
2026-07-17 14:06:01 -04:00
committed by GitHub
parent aea9e5e8ed
commit 69e0c2659e
6 changed files with 312 additions and 22 deletions
+3 -3
View File
@@ -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.
+3 -2
View File
@@ -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):
+4 -4
View File
@@ -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.
@@ -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)
@@ -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]*$")
)
;; deny all inbound network traffic EXCEPT on debugger port
(deny network-inbound)
;; 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")
)
;; 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=<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 "*:*"))
@@ -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\)/);
});
});