Skip to content

Commit e07a814

Browse files
committed
fix(sbx): preserve pi identity through codex sandbox
1 parent 24218f4 commit e07a814

1 file changed

Lines changed: 162 additions & 0 deletions

File tree

.local/bin/sbx

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env bash
2+
# Run a command with Codex's native sandbox and a generated permissions profile.
3+
set -euo pipefail
4+
5+
usage() {
6+
cat <<'EOF'
7+
Usage: sbx [options] -- command [arguments...]
8+
9+
Run a command under `codex sandbox`. The command may read the filesystem, but
10+
may write only to the current workspace, /tmp, ~/.pi, ~/.config, XDG state and
11+
cache directories, pnpm data, Android, Java, Kotlin, and Gradle state, Codex's
12+
temporary directories, and any additional directories named with --write.
13+
14+
Options:
15+
-w, --write DIR Add a writable directory (repeatable)
16+
--print Print the generated Codex permissions profile and exit
17+
-h, --help Show this help
18+
19+
Examples:
20+
sbx -- pi
21+
sbx -w ../shared -- claude
22+
sbx -- npm test
23+
24+
Codex uses macOS sandbox-exec/Seatbelt behind this command on Apple platforms.
25+
EOF
26+
}
27+
28+
die() {
29+
printf 'sbx: %s\n' "$*" >&2
30+
exit 1
31+
}
32+
33+
resolve_dir() {
34+
local path=$1 resolved
35+
36+
[[ -d $path ]] || die "directory does not exist: $path"
37+
resolved=$(cd -P -- "$path" && pwd)
38+
if [[ $resolved == *'"'* || $resolved =~ [[:cntrl:]] ]]; then
39+
die "Codex cannot use a writable path containing quotes or control characters: $resolved"
40+
fi
41+
printf '%s\n' "$resolved"
42+
}
43+
44+
append_unique_root() {
45+
local root=$1 existing
46+
47+
for existing in "${writable_roots[@]}"; do
48+
[[ $root != "$existing" ]] || return 0
49+
done
50+
writable_roots+=("$root")
51+
}
52+
53+
append_existing_root() {
54+
local root=$1
55+
56+
[[ ! -d $root ]] || append_unique_root "$(resolve_dir "$root")"
57+
}
58+
59+
toml_quote() {
60+
local value=$1
61+
62+
value=${value//\\/\\\\}
63+
value=${value//\"/\\\"}
64+
value=${value//$'\b'/\\b}
65+
value=${value//$'\t'/\\t}
66+
value=${value//$'\n'/\\n}
67+
value=${value//$'\f'/\\f}
68+
value=${value//$'\r'/\\r}
69+
printf '"%s"' "$value"
70+
}
71+
72+
print_profile=false
73+
writable_roots=()
74+
state=${XDG_STATE_HOME:-$HOME/.local/state}
75+
cache=${XDG_CACHE_HOME:-$HOME/.cache}
76+
pnpm_home=${PNPM_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}/pnpm}
77+
append_unique_root "$(resolve_dir /tmp)"
78+
append_unique_root "$(resolve_dir "$HOME/.pi")"
79+
append_unique_root "$(resolve_dir "$HOME/.config")"
80+
append_existing_root "$PWD/.git"
81+
append_existing_root "$state"
82+
append_existing_root "$cache"
83+
append_existing_root "$pnpm_home"
84+
append_existing_root "$HOME/.cache"
85+
append_existing_root "$HOME/Library/Caches"
86+
append_existing_root "$HOME/.gradle"
87+
append_existing_root "${ANDROID_USER_HOME:-$HOME/.android}"
88+
append_existing_root "${ANDROID_AVD_HOME:-}"
89+
append_existing_root "$HOME/Library/Application Support/kotlin"
90+
append_existing_root "${JAVA_HOME:-}"
91+
if [[ -x /usr/libexec/java_home ]]; then
92+
append_existing_root "$(/usr/libexec/java_home 2>/dev/null || true)"
93+
fi
94+
for java_home in \
95+
/opt/homebrew/opt/openjdk*/libexec/*.jdk/Contents/Home \
96+
/usr/local/opt/openjdk*/libexec/*.jdk/Contents/Home; do
97+
append_existing_root "$java_home"
98+
done
99+
100+
while (($#)); do
101+
case $1 in
102+
-w | --write)
103+
(($# >= 2)) || die "$1 requires a directory"
104+
resolved_root=$(resolve_dir "$2") || exit
105+
append_unique_root "$resolved_root"
106+
shift 2
107+
;;
108+
--print)
109+
print_profile=true
110+
shift
111+
;;
112+
-h | --help)
113+
usage
114+
exit 0
115+
;;
116+
--)
117+
shift
118+
break
119+
;;
120+
-*)
121+
die "unknown option: $1"
122+
;;
123+
*)
124+
die "expected -- before the command"
125+
;;
126+
esac
127+
done
128+
129+
command -v codex >/dev/null || die "codex is required"
130+
if ! "$print_profile" && (($# == 0)); then
131+
die "no command specified"
132+
fi
133+
134+
# Codex is the visible foreground process when it wraps an agent, so preserve
135+
# the actual agent identity for Herdr's process detection.
136+
if (($# > 0)) && [[ ${1##*/} == pi ]]; then
137+
export HERDR_AGENT=pi
138+
fi
139+
140+
# Extend Codex's built-in workspace profile instead of maintaining a Seatbelt
141+
# policy here. Codex supplies the workspace and temporary write roots, protects
142+
# sensitive workspace metadata, and selects the platform sandbox implementation.
143+
profile='{ extends = ":workspace"'
144+
if ((${#writable_roots[@]})); then
145+
profile+=', workspace_roots = {'
146+
for path in "${writable_roots[@]}"; do
147+
profile+=" $(toml_quote "$path") = true,"
148+
done
149+
profile+=' }'
150+
fi
151+
profile+=', network = { enabled = true } }'
152+
153+
if "$print_profile"; then
154+
printf 'permissions.sbx = %s\n' "$profile"
155+
exit 0
156+
fi
157+
158+
XDG_STATE_HOME="$state" XDG_CACHE_HOME="$cache" exec codex sandbox \
159+
-C "$PWD" \
160+
-c "permissions.sbx=$profile" \
161+
-P sbx \
162+
-- "$@"

0 commit comments

Comments
 (0)