restricted networking for all sandboxing methods, new seatbelt profiles, updated docs, fixes to sandbox build, debugging through sandbox (#891)
This commit is contained in:
parent
895c1f132f
commit
e38d2078cc
|
@ -215,7 +215,7 @@ To debug the CLI's React-based UI, you can use React DevTools. Ink, the library
|
||||||
|
|
||||||
### MacOS Seatbelt
|
### MacOS Seatbelt
|
||||||
|
|
||||||
On MacOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `minimal` profile (see `packages/cli/src/utils/sandbox-macos-minimal.sb`) that restricts writes to the project folder but otherwise allows all other operations by default. You can switch to a `strict` profile (see `.../sandbox-macos-strict.sb`) that declines operations by default by setting `SEATBELT_PROFILE=strict` in your environment or `.env` file. You can also switch to a custom profile `SEATBELT_PROFILE=<profile>` if you also create a file `.gemini/sandbox-macos-<profile>.sb` under your project settings directory `.gemini`.
|
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 `restrictive-closed` profile (see `.../sandbox-macos-strict.sb`) that declines all operations and outbound network traffic ("closed") by default by setting `SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file. Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}` (see below for proxied networking). You can also switch to a custom profile `SEATBELT_PROFILE=<profile>` if you also create a file `.gemini/sandbox-macos-<profile>.sb` under your project settings directory `.gemini`.
|
||||||
|
|
||||||
### Container-based Sandboxing (All Platforms)
|
### Container-based Sandboxing (All Platforms)
|
||||||
|
|
||||||
|
@ -223,6 +223,10 @@ For stronger container-based sandboxing on MacOS or other platforms, you can set
|
||||||
|
|
||||||
Container-based sandboxing mounts the project directory (and system temp directory) with read-write access and is started/stopped/removed automatically as you start/stop Gemini CLI. Files created within the sandbox should be automatically mapped to your user/group on host machine. You can easily specify additional mounts, ports, or environment variables by setting `SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox for your projects by creating the files `.gemini/sandbox.Dockerfile` and/or `.gemini/sandbox.bashrc` under your project settings directory (`.gemini`) and running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom sandbox.
|
Container-based sandboxing mounts the project directory (and system temp directory) with read-write access and is started/stopped/removed automatically as you start/stop Gemini CLI. Files created within the sandbox should be automatically mapped to your user/group on host machine. You can easily specify additional mounts, ports, or environment variables by setting `SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox for your projects by creating the files `.gemini/sandbox.Dockerfile` and/or `.gemini/sandbox.bashrc` under your project settings directory (`.gemini`) and running `gemini` with `BUILD_SANDBOX=1` to trigger building of your custom sandbox.
|
||||||
|
|
||||||
|
#### Proxied Networking
|
||||||
|
|
||||||
|
All sandboxing methods, including MacOS Seatbelt using `*-proxied` profiles, support restricting outbound network traffic through a custom proxy server that can be specified as `GEMINI_SANDBOX_PROXY_COMMAND=<command>`, where `<command>` must start a proxy server that listens on `0.0.0.0:8877` for relevant requests. See `scripts/example-proxy.js` for a minimal proxy that only allows `HTTPS` connections to `example.com:443` (e.g. `curl https://example.com`) and declines all other requests. The proxy is started and stopped automatically alongside the sandbox.
|
||||||
|
|
||||||
## Manual Publish
|
## Manual Publish
|
||||||
|
|
||||||
We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands:
|
We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands:
|
||||||
|
|
|
@ -23,6 +23,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
psmisc \
|
psmisc \
|
||||||
lsof \
|
lsof \
|
||||||
socat \
|
socat \
|
||||||
|
ca-certificates \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||||
- **`SEATBELT_PROFILE`** (macOS specific):
|
- **`SEATBELT_PROFILE`** (macOS specific):
|
||||||
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
- Switches the Seatbelt (`sandbox-exec`) profile on macOS.
|
||||||
- `minimal`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-minimal.sb`) but allows other operations.
|
- `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.
|
||||||
- `strict`: Uses a strict profile that declines operations by default.
|
- `strict`: Uses a strict profile that declines operations by default.
|
||||||
- `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
- `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/` directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
|
||||||
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
|
- **`DEBUG` or `DEBUG_MODE`** (often used by underlying libraries or the CLI itself):
|
||||||
|
|
|
@ -9403,6 +9403,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
|
||||||
|
"integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
@ -10754,7 +10763,8 @@
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
"strip-ansi": "^7.1.0"
|
"strip-ansi": "^7.1.0",
|
||||||
|
"undici": "^7.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
(version 1)
|
||||||
|
|
||||||
|
;; allow everything by default
|
||||||
|
(allow default)
|
||||||
|
|
||||||
|
;; deny all writes EXCEPT under specific paths
|
||||||
|
(deny file-write*)
|
||||||
|
(allow file-write*
|
||||||
|
(subpath (param "TARGET_DIR"))
|
||||||
|
(subpath (param "TMP_DIR"))
|
||||||
|
(subpath (param "CACHE_DIR"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gemini"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||||
|
(literal "/dev/stdout")
|
||||||
|
(literal "/dev/stderr")
|
||||||
|
(literal "/dev/null")
|
||||||
|
)
|
||||||
|
|
||||||
|
;; deny all inbound network traffic EXCEPT on debugger port
|
||||||
|
(deny network-inbound)
|
||||||
|
(allow network-inbound (local ip "localhost:9229"))
|
||||||
|
|
||||||
|
;; deny all outbound network traffic
|
||||||
|
(deny network-outbound)
|
|
@ -0,0 +1,31 @@
|
||||||
|
(version 1)
|
||||||
|
|
||||||
|
;; allow everything by default
|
||||||
|
(allow default)
|
||||||
|
|
||||||
|
;; deny all writes EXCEPT under specific paths
|
||||||
|
(deny file-write*)
|
||||||
|
(allow file-write*
|
||||||
|
(subpath (param "TARGET_DIR"))
|
||||||
|
(subpath (param "TMP_DIR"))
|
||||||
|
(subpath (param "CACHE_DIR"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gemini"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||||
|
(literal "/dev/stdout")
|
||||||
|
(literal "/dev/stderr")
|
||||||
|
(literal "/dev/null")
|
||||||
|
)
|
||||||
|
|
||||||
|
;; deny all inbound network traffic EXCEPT on debugger port
|
||||||
|
(deny network-inbound)
|
||||||
|
(allow network-inbound (local ip "localhost:9229"))
|
||||||
|
|
||||||
|
;; 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 0.0.0.0:8877 (see scripts/example-proxy.js)
|
||||||
|
(deny network-outbound)
|
||||||
|
(allow network-outbound (remote tcp "localhost:8877"))
|
||||||
|
|
||||||
|
(allow network-bind (local ip "*:*"))
|
|
@ -0,0 +1,87 @@
|
||||||
|
(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 specific paths
|
||||||
|
(allow file-write*
|
||||||
|
(subpath (param "TARGET_DIR"))
|
||||||
|
(subpath (param "TMP_DIR"))
|
||||||
|
(subpath (param "CACHE_DIR"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gemini"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||||
|
(literal "/dev/stdout")
|
||||||
|
(literal "/dev/stderr")
|
||||||
|
(literal "/dev/null")
|
||||||
|
)
|
||||||
|
|
||||||
|
;; 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.*"))
|
||||||
|
|
||||||
|
;; allow inbound network traffic on debugger port
|
||||||
|
(allow network-inbound (local ip "localhost:9229"))
|
|
@ -76,15 +76,15 @@
|
||||||
(literal "/dev/null")
|
(literal "/dev/null")
|
||||||
)
|
)
|
||||||
|
|
||||||
;; allow outbound network connections
|
|
||||||
(allow network-outbound)
|
|
||||||
|
|
||||||
;; allow inbound network connections to debugging port
|
|
||||||
(allow network-inbound (local ip (string-append "*:" "9229")))
|
|
||||||
|
|
||||||
;; allow communication with sysmond for process listing (e.g. for pgrep)
|
;; allow communication with sysmond for process listing (e.g. for pgrep)
|
||||||
(allow mach-lookup (global-name "com.apple.sysmond"))
|
(allow mach-lookup (global-name "com.apple.sysmond"))
|
||||||
|
|
||||||
;; enable terminal access required by ink
|
;; enable terminal access required by ink
|
||||||
;; fixes setRawMode EPERM failure (at node:tty:81:24)
|
;; fixes setRawMode EPERM failure (at node:tty:81:24)
|
||||||
(allow file-ioctl (regex #"^/dev/tty.*"))
|
(allow file-ioctl (regex #"^/dev/tty.*"))
|
||||||
|
|
||||||
|
;; allow inbound network traffic on debugger port
|
||||||
|
(allow network-inbound (local ip "localhost:9229"))
|
||||||
|
|
||||||
|
;; allow all outbound network traffic
|
||||||
|
(allow network-outbound)
|
|
@ -0,0 +1,92 @@
|
||||||
|
(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 specific paths
|
||||||
|
(allow file-write*
|
||||||
|
(subpath (param "TARGET_DIR"))
|
||||||
|
(subpath (param "TMP_DIR"))
|
||||||
|
(subpath (param "CACHE_DIR"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gemini"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||||
|
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||||
|
(literal "/dev/stdout")
|
||||||
|
(literal "/dev/stderr")
|
||||||
|
(literal "/dev/null")
|
||||||
|
)
|
||||||
|
|
||||||
|
;; 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.*"))
|
||||||
|
|
||||||
|
;; allow inbound network traffic on debugger port
|
||||||
|
(allow network-inbound (local ip "localhost:9229"))
|
||||||
|
|
||||||
|
;; allow outbound network traffic through proxy on localhost:8877
|
||||||
|
;; set `GEMINI_SANDBOX_PROXY_COMMAND=<command>` to run proxy alongside sandbox
|
||||||
|
;; proxy must listen on 0.0.0.0:8877 (see scripts/example-proxy.js)
|
||||||
|
(allow network-outbound (remote tcp "localhost:8877"))
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync, spawnSync, spawn } from 'node:child_process';
|
import { execSync, spawn, type ChildProcess } from 'node:child_process';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
@ -30,6 +30,16 @@ function getContainerPath(hostPath: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'gemini-cli-sandbox';
|
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'gemini-cli-sandbox';
|
||||||
|
const SANDBOX_NETWORK_NAME = 'gemini-cli-sandbox';
|
||||||
|
const SANDBOX_PROXY_NAME = 'gemini-cli-sandbox-proxy';
|
||||||
|
const BUILTIN_SEATBELT_PROFILES = [
|
||||||
|
'permissive-open',
|
||||||
|
'permissive-closed',
|
||||||
|
'permissive-proxied',
|
||||||
|
'restrictive-open',
|
||||||
|
'restrictive-closed',
|
||||||
|
'restrictive-proxied',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the sandbox container should be run with the current user's UID and GID.
|
* Determines whether the sandbox container should be run with the current user's UID and GID.
|
||||||
|
@ -230,14 +240,14 @@ export async function start_sandbox(sandbox: string) {
|
||||||
if (sandbox === 'sandbox-exec') {
|
if (sandbox === 'sandbox-exec') {
|
||||||
// disallow BUILD_SANDBOX
|
// disallow BUILD_SANDBOX
|
||||||
if (process.env.BUILD_SANDBOX) {
|
if (process.env.BUILD_SANDBOX) {
|
||||||
console.error('ERROR: cannot BUILD_SANDBOX when using MacOC Seatbelt');
|
console.error('ERROR: cannot BUILD_SANDBOX when using MacOS Seatbelt');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const profile = (process.env.SEATBELT_PROFILE ??= 'minimal');
|
const profile = (process.env.SEATBELT_PROFILE ??= 'permissive-open');
|
||||||
let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
|
let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
|
||||||
.pathname;
|
.pathname;
|
||||||
// if profile is anything other than 'minimal' or 'strict', then look for the profile file under the project settings directory
|
// if profile name is not recognized, then look for file under project settings directory
|
||||||
if (profile !== 'minimal' && profile !== 'strict') {
|
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
|
||||||
profileFile = path.join(
|
profileFile = path.join(
|
||||||
SETTINGS_DIRECTORY_NAME,
|
SETTINGS_DIRECTORY_NAME,
|
||||||
`sandbox-macos-${profile}.sb`,
|
`sandbox-macos-${profile}.sb`,
|
||||||
|
@ -251,10 +261,6 @@ export async function start_sandbox(sandbox: string) {
|
||||||
}
|
}
|
||||||
console.error(`using macos seatbelt (profile: ${profile}) ...`);
|
console.error(`using macos seatbelt (profile: ${profile}) ...`);
|
||||||
// if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
|
// if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
|
||||||
if (process.env.DEBUG) {
|
|
||||||
process.env.NODE_OPTIONS ??= '';
|
|
||||||
process.env.NODE_OPTIONS += ` --inspect-brk`;
|
|
||||||
}
|
|
||||||
const args = [
|
const args = [
|
||||||
'-D',
|
'-D',
|
||||||
`TARGET_DIR=${fs.realpathSync(process.cwd())}`,
|
`TARGET_DIR=${fs.realpathSync(process.cwd())}`,
|
||||||
|
@ -270,11 +276,67 @@ export async function start_sandbox(sandbox: string) {
|
||||||
'-c',
|
'-c',
|
||||||
[
|
[
|
||||||
`SANDBOX=sandbox-exec`,
|
`SANDBOX=sandbox-exec`,
|
||||||
`NODE_OPTIONS="${process.env.NODE_OPTIONS}"`,
|
`NODE_OPTIONS="${process.env.DEBUG ? `--inspect-brk` : ''}"`,
|
||||||
...process.argv.map((arg) => quote([arg])),
|
...process.argv.map((arg) => quote([arg])),
|
||||||
].join(' '),
|
].join(' '),
|
||||||
];
|
];
|
||||||
spawnSync(sandbox, args, { stdio: 'inherit' });
|
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
|
||||||
|
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
|
||||||
|
let proxyProcess: ChildProcess | undefined;
|
||||||
|
const sandboxEnv = { ...process.env };
|
||||||
|
if (proxyCommand) {
|
||||||
|
const proxy =
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy ||
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
'http://localhost:8877';
|
||||||
|
sandboxEnv['HTTPS_PROXY'] = proxy;
|
||||||
|
sandboxEnv['https_proxy'] = proxy; // lower-case can be required, e.g. for curl
|
||||||
|
sandboxEnv['HTTP_PROXY'] = proxy;
|
||||||
|
sandboxEnv['http_proxy'] = proxy;
|
||||||
|
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
||||||
|
if (noProxy) {
|
||||||
|
sandboxEnv['NO_PROXY'] = noProxy;
|
||||||
|
sandboxEnv['no_proxy'] = noProxy;
|
||||||
|
}
|
||||||
|
proxyProcess = spawn(proxyCommand, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
shell: true,
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
// commented out as it disrupts ink rendering
|
||||||
|
// proxyProcess.stdout?.on('data', (data) => {
|
||||||
|
// console.info(data.toString());
|
||||||
|
// });
|
||||||
|
proxyProcess.stderr?.on('data', (data) => {
|
||||||
|
console.error(data.toString());
|
||||||
|
});
|
||||||
|
console.log('waiting for proxy to start ...');
|
||||||
|
execSync(`until lsof -i :8877 | grep -q "LISTEN"; do sleep 0.1; done`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// spawn child and let it inherit stdio
|
||||||
|
const child = spawn(sandbox, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: sandboxEnv,
|
||||||
|
});
|
||||||
|
if (proxyProcess) {
|
||||||
|
proxyProcess.on('close', (code, signal) => {
|
||||||
|
console.error(
|
||||||
|
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
|
||||||
|
);
|
||||||
|
if (child.pid) {
|
||||||
|
process.kill(-child.pid, 'SIGTERM');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => child.on('close', resolve));
|
||||||
|
} finally {
|
||||||
|
if (proxyProcess?.pid) {
|
||||||
|
process.kill(-proxyProcess.pid, 'SIGTERM');
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,6 +470,45 @@ export async function start_sandbox(sandbox: string) {
|
||||||
args.push(`--publish`, `${debugPort}:${debugPort}`);
|
args.push(`--publish`, `${debugPort}:${debugPort}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy proxy environment variables, replacing localhost with SANDBOX_PROXY_NAME
|
||||||
|
// copy as both upper-case and lower-case as is required by some utilities
|
||||||
|
// GEMINI_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set
|
||||||
|
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
|
||||||
|
let proxy =
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy ||
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy ||
|
||||||
|
'http://localhost:8877';
|
||||||
|
proxy = proxy.replace('localhost', SANDBOX_PROXY_NAME);
|
||||||
|
if (proxy) {
|
||||||
|
args.push('--env', `HTTPS_PROXY=${proxy}`);
|
||||||
|
args.push('--env', `https_proxy=${proxy}`); // lower-case can be required, e.g. for curl
|
||||||
|
args.push('--env', `HTTP_PROXY=${proxy}`);
|
||||||
|
args.push('--env', `http_proxy=${proxy}`);
|
||||||
|
}
|
||||||
|
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
||||||
|
if (noProxy) {
|
||||||
|
args.push('--env', `NO_PROXY=${noProxy}`);
|
||||||
|
args.push('--env', `no_proxy=${noProxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if using proxy, switch to internal networking through proxy
|
||||||
|
if (proxy) {
|
||||||
|
execSync(
|
||||||
|
`${sandbox} network exists ${SANDBOX_NETWORK_NAME} || ${sandbox} network create --internal ${SANDBOX_NETWORK_NAME}`,
|
||||||
|
);
|
||||||
|
args.push('--network', SANDBOX_NETWORK_NAME);
|
||||||
|
// if proxy command is set, create a separate network w/ host access (i.e. non-internal)
|
||||||
|
// we will run proxy in its own container connected to both host network and internal network
|
||||||
|
// this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
|
||||||
|
if (proxyCommand) {
|
||||||
|
execSync(
|
||||||
|
`${sandbox} network exists ${SANDBOX_PROXY_NAME} || ${sandbox} network create ${SANDBOX_PROXY_NAME}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// name container after image, plus numeric suffix to avoid conflicts
|
// name container after image, plus numeric suffix to avoid conflicts
|
||||||
const imageName = parseImageName(image);
|
const imageName = parseImageName(image);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
@ -510,28 +611,52 @@ export async function start_sandbox(sandbox: string) {
|
||||||
// push container entrypoint (including args)
|
// push container entrypoint (including args)
|
||||||
args.push(...entrypoint(workdir));
|
args.push(...entrypoint(workdir));
|
||||||
|
|
||||||
// spawn child and let it inherit stdio
|
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
|
||||||
const child = spawn(sandbox, args, {
|
let proxyProcess: ChildProcess | undefined;
|
||||||
stdio: 'inherit',
|
if (proxyCommand) {
|
||||||
detached: os.platform() !== 'win32',
|
// run proxyCommand in its own container
|
||||||
});
|
const proxyContainerCommand = `${sandbox} run --rm --init --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} --network ${SANDBOX_NETWORK_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
|
||||||
|
proxyProcess = spawn(proxyContainerCommand, {
|
||||||
child.on('error', (err) => {
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
console.error('Sandbox process error:', err);
|
shell: true,
|
||||||
});
|
detached: true,
|
||||||
|
|
||||||
// uncomment this line (and comment the await on following line) to let parent exit
|
|
||||||
// child.unref();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
child.on('close', (code, signal) => {
|
|
||||||
if (code !== 0) {
|
|
||||||
console.log(
|
|
||||||
`Sandbox process exited with code: ${code}, signal: ${signal}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
});
|
// commented out as it disrupts ink rendering
|
||||||
|
// proxyProcess.stdout?.on('data', (data) => {
|
||||||
|
// console.info(data.toString());
|
||||||
|
// });
|
||||||
|
proxyProcess.stderr?.on('data', (data) => {
|
||||||
|
console.error(data.toString().trim());
|
||||||
|
});
|
||||||
|
console.log('waiting for proxy to start ...');
|
||||||
|
execSync(`until lsof -i :8877 | grep -q "LISTEN"; do sleep 0.1; done`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// spawn child and let it inherit stdio
|
||||||
|
const child = spawn(sandbox, args, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.error('Sandbox process error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.log(
|
||||||
|
`Sandbox process exited with code: ${code}, signal: ${signal}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (proxyProcess?.pid) {
|
||||||
|
process.kill(-proxyProcess.pid, 'SIGTERM');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions to ensure sandbox image is present
|
// Helper functions to ensure sandbox image is present
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
"strip-ansi": "^7.1.0"
|
"strip-ansi": "^7.1.0",
|
||||||
|
"undici": "^7.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
|
|
|
@ -38,6 +38,18 @@ import {
|
||||||
createContentGenerator,
|
createContentGenerator,
|
||||||
} from './contentGenerator.js';
|
} from './contentGenerator.js';
|
||||||
|
|
||||||
|
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||||
|
|
||||||
|
const proxy =
|
||||||
|
process.env.HTTPS_PROXY ||
|
||||||
|
process.env.https_proxy ||
|
||||||
|
process.env.HTTP_PROXY ||
|
||||||
|
process.env.http_proxy;
|
||||||
|
|
||||||
|
if (proxy) {
|
||||||
|
setGlobalDispatcher(new ProxyAgent(proxy));
|
||||||
|
}
|
||||||
|
|
||||||
export class GeminiClient {
|
export class GeminiClient {
|
||||||
private chat: Promise<GeminiChat>;
|
private chat: Promise<GeminiChat>;
|
||||||
private contentGenerator: ContentGenerator;
|
private contentGenerator: ContentGenerator;
|
||||||
|
|
|
@ -111,7 +111,7 @@ function buildImage(imageName, dockerfile) {
|
||||||
|
|
||||||
execSync(
|
execSync(
|
||||||
`${buildCommand} ${process.env.BUILD_SANDBOX_FLAGS || ''} -f "${dockerfile}" -t "${imageName}" .`,
|
`${buildCommand} ${process.env.BUILD_SANDBOX_FLAGS || ''} -f "${dockerfile}" -t "${imageName}" .`,
|
||||||
{ stdio: buildStdout },
|
{ stdio: buildStdout, shell: '/bin/bash' },
|
||||||
);
|
);
|
||||||
console.log(`built ${imageName}`);
|
console.log(`built ${imageName}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example proxy server that listens on 0.0.0.0:8877 and only allows HTTPS connections to example.com.
|
||||||
|
// Set `GEMINI_SANDBOX_PROXY_COMMAND=scripts/example-proxy.js` to run proxy alongside sandbox
|
||||||
|
// Test via `curl https://example.com` inside sandbox (in shell mode or via shell tool)
|
||||||
|
|
||||||
|
import http from 'http';
|
||||||
|
import net from 'net';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import console from 'console';
|
||||||
|
|
||||||
|
const PROXY_PORT = 8877;
|
||||||
|
const ALLOWED_DOMAINS = ['example.com', 'googleapis.com'];
|
||||||
|
const ALLOWED_PORT = '443';
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
// Deny all requests other than CONNECT for HTTPS
|
||||||
|
console.log(
|
||||||
|
`[PROXY] Denying non-CONNECT request for: ${req.method} ${req.url}`,
|
||||||
|
);
|
||||||
|
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('Method Not Allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('connect', (req, clientSocket, head) => {
|
||||||
|
// req.url will be in the format "hostname:port" for a CONNECT request.
|
||||||
|
const { port, hostname } = new URL(`http://${req.url}`);
|
||||||
|
|
||||||
|
console.log(`[PROXY] Intercepted CONNECT request for: ${hostname}:${port}`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
ALLOWED_DOMAINS.some(
|
||||||
|
(domain) => hostname == domain || hostname.endsWith(`.${domain}`),
|
||||||
|
) &&
|
||||||
|
port === ALLOWED_PORT
|
||||||
|
) {
|
||||||
|
console.log(`[PROXY] Allowing connection to ${hostname}:${port}`);
|
||||||
|
|
||||||
|
// Establish a TCP connection to the original destination.
|
||||||
|
const serverSocket = net.connect(port, hostname, () => {
|
||||||
|
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||||
|
// Create a tunnel by piping data between the client and the destination server.
|
||||||
|
serverSocket.write(head);
|
||||||
|
serverSocket.pipe(clientSocket);
|
||||||
|
clientSocket.pipe(serverSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
serverSocket.on('error', (err) => {
|
||||||
|
console.error(`[PROXY] Error connecting to destination: ${err.message}`);
|
||||||
|
clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`[PROXY] Denying connection to ${hostname}:${port}`);
|
||||||
|
clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSocket.on('error', (err) => {
|
||||||
|
// This can happen if the client hangs up.
|
||||||
|
console.error(`[PROXY] Client socket error: ${err.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PROXY_PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`[PROXY] Proxy listening on 0.0.0.0:${PROXY_PORT}`);
|
||||||
|
console.log(
|
||||||
|
`[PROXY] Allowing HTTPS connections to domains: ${ALLOWED_DOMAINS.join(', ')}`,
|
||||||
|
);
|
||||||
|
});
|
|
@ -33,11 +33,12 @@ execSync('node ./scripts/check-build-status.js', {
|
||||||
// inside sandbox SANDBOX should be set and sandbox_command.js should fail
|
// inside sandbox SANDBOX should be set and sandbox_command.js should fail
|
||||||
const nodeArgs = [];
|
const nodeArgs = [];
|
||||||
try {
|
try {
|
||||||
execSync('node scripts/sandbox_command.js -q', {
|
const sandboxCommand = execSync('node scripts/sandbox_command.js', {
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: root,
|
cwd: root,
|
||||||
});
|
})
|
||||||
if (process.env.DEBUG) {
|
.toString()
|
||||||
|
.trim();
|
||||||
|
if (process.env.DEBUG && !sandboxCommand) {
|
||||||
if (process.env.SANDBOX) {
|
if (process.env.SANDBOX) {
|
||||||
const port = process.env.DEBUG_PORT || '9229';
|
const port = process.env.DEBUG_PORT || '9229';
|
||||||
nodeArgs.push(`--inspect-brk=0.0.0.0:${port}`);
|
nodeArgs.push(`--inspect-brk=0.0.0.0:${port}`);
|
||||||
|
|
Loading…
Reference in New Issue