Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions docs/cli/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,10 @@
"model": "gemini-3.1-flash-lite"
}
},
"gemini-3.5-flash": {

Check warning on line 675 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
"extends": "chat-base-3",
"modelConfig": {
"model": "gemini-3.5-flash"

Check warning on line 678 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
}
},
"gemma-4-31b-it": {
Expand All @@ -702,10 +702,10 @@
"model": "gemini-3-flash-preview"
}
},
"gemini-3.5-flash-base": {

Check warning on line 705 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
"extends": "base",
"modelConfig": {
"model": "gemini-3.5-flash"

Check warning on line 708 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
}
},
"classifier": {
Expand Down Expand Up @@ -954,7 +954,7 @@
"multimodalToolUse": true
}
},
"gemini-3.5-flash": {

Check warning on line 957 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
"tier": "flash",
"family": "gemini-3",
"isPreview": false,
Expand Down Expand Up @@ -1120,7 +1120,7 @@
"hasAccessToPreview": false,
"useGemini3_5Flash": true
},
"target": "gemini-3.5-flash"

Check warning on line 1123 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
},
{
"condition": {
Expand All @@ -1131,8 +1131,8 @@
}
]
},
"gemini-3.5-flash": {

Check warning on line 1134 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
"default": "gemini-3.5-flash",

Check warning on line 1135 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
"contexts": [
{
"condition": {
Expand All @@ -1156,7 +1156,7 @@
"condition": {
"useGemini3_5Flash": true
},
"target": "gemini-3.5-flash"

Check warning on line 1159 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
}
]
},
Expand Down Expand Up @@ -1242,7 +1242,7 @@
"condition": {
"useGemini3_5Flash": true
},
"target": "gemini-3.5-flash"

Check warning on line 1245 in docs/reference/configuration.md

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3.5". Please make sure this change is appropriate to submit.
},
{
"condition": {
Expand Down Expand Up @@ -2736,10 +2736,10 @@
- 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.
Expand Down
117 changes: 113 additions & 4 deletions packages/cli/src/utils/sandbox-macos-permissive-open.sb
Original file line number Diff line number Diff line change
@@ -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"))
Expand All @@ -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)
120 changes: 111 additions & 9 deletions packages/cli/src/utils/sandbox-macos-permissive-proxied.sb
Original file line number Diff line number Diff line change
@@ -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"))
Expand All @@ -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=<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 "*:*"))
78 changes: 78 additions & 0 deletions packages/cli/src/utils/sandbox-macos-profiles.test.ts
Original file line number Diff line number Diff line change
@@ -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\)/);
});
});
Loading