MacOS + Docker shenanigans

This commit is contained in:
Samuel Aubertin
2025-11-27 16:12:46 +01:00
parent c1e64bb4ef
commit 7630e7edba
2 changed files with 167 additions and 14 deletions

View File

@@ -9,6 +9,14 @@ sloptrap runs the OpenAI Codex CLI inside a container with a predictable and loc
> Tip: set `SLOPTRAP_CONTAINER_ENGINE=<engine>` if you need to override the default Podman requirement. > Tip: set `SLOPTRAP_CONTAINER_ENGINE=<engine>` if you need to override the default Podman requirement.
### macOS setup
sloptrap targets GNU userland. On macOS, install the GNU tools via Homebrew and the launcher will prepend their `gnubin` paths automatically:
```
brew install coreutils gnu-tar jq
```
## Quick Start ## Quick Start
1. Place `sloptrap` somewhere on your PATH/shared drive (the helper Dockerfile and Codex binary are bundled and downloaded automatically). 1. Place `sloptrap` somewhere on your PATH/shared drive (the helper Dockerfile and Codex binary are bundled and downloaded automatically).

173
sloptrap
View File

@@ -2,7 +2,57 @@
# sloptrap # sloptrap
set -euo pipefail set -euo pipefail
require_cmd() { command -v "$1" >/dev/null 2>&1 || error "'$1' is required"; } IS_MAC=false
if [[ $(uname -s 2>/dev/null) == "Darwin" ]]; then
IS_MAC=true
mac_gnu_bins=(
/opt/homebrew/opt/coreutils/libexec/gnubin
/usr/local/opt/coreutils/libexec/gnubin
/opt/homebrew/opt/gnu-tar/libexec/gnubin
/usr/local/opt/gnu-tar/libexec/gnubin
)
for bin_dir in "${mac_gnu_bins[@]}"; do
[[ -d $bin_dir ]] || continue
case ":$PATH:" in
*":$bin_dir:"*)
;;
*)
path_head=${PATH%%:*}
path_rest=""
if [[ $PATH == *:* ]]; then
path_rest=${PATH#*:}
fi
case "$path_head" in
/tmp/*|/var/folders/*|"$HOME"/*)
if [[ -n $path_rest ]]; then
PATH="$path_head:$bin_dir:$path_rest"
else
PATH="$path_head:$bin_dir"
fi
;;
*)
PATH="$bin_dir:$PATH"
;;
esac
unset -v path_head path_rest
;;
esac
done
export PATH
fi
REQUIRED_BREW_PKGS="coreutils gnu-tar jq"
require_cmd() {
if command -v "$1" >/dev/null 2>&1; then
return 0
fi
if $IS_MAC; then
error "'$1' is required; install with: brew install $REQUIRED_BREW_PKGS"
else
error "'$1' is required"
fi
}
for c in curl tar sha256sum realpath jq; do require_cmd "$c"; done for c in curl tar sha256sum realpath jq; do require_cmd "$c"; done
COLOR_TEXT=$'\033[38;5;247m' COLOR_TEXT=$'\033[38;5;247m'
@@ -147,6 +197,26 @@ trim() {
printf '%s' "$value" printf '%s' "$value"
} }
resolve_path_relaxed() {
local candidate=$1
local resolved
if resolved=$(realpath -m "$candidate" 2>/dev/null); then
printf '%s' "$resolved"
return 0
fi
return 1
}
resolve_path_strict() {
local candidate=$1
local resolved
if resolved=$(resolve_path_relaxed "$candidate"); then
printf '%s' "$resolved"
return 0
fi
error "failed to resolve path '$candidate'; install GNU coreutils for realpath support"
}
declare -A MANIFEST=() declare -A MANIFEST=()
declare -a SLOPTRAP_IGNORE_ENTRIES=() declare -a SLOPTRAP_IGNORE_ENTRIES=()
declare -a IGNORE_MOUNT_ARGS=() declare -a IGNORE_MOUNT_ARGS=()
@@ -181,11 +251,11 @@ cleanup_ignore_stub_dir() {
[[ -n $helper_root && -n $stub_base ]] || return 0 [[ -n $helper_root && -n $stub_base ]] || return 0
[[ -d $stub_base ]] || return 0 [[ -d $stub_base ]] || return 0
local resolved_helper resolved_stub local resolved_helper resolved_stub
if ! resolved_helper=$(realpath -m "$helper_root") 2>/dev/null; then if ! resolved_helper=$(resolve_path_relaxed "$helper_root"); then
warn "failed to resolve helper root '$helper_root' during cleanup" warn "failed to resolve helper root '$helper_root' during cleanup"
return 0 return 0
fi fi
if ! resolved_stub=$(realpath -m "$stub_base") 2>/dev/null; then if ! resolved_stub=$(resolve_path_relaxed "$stub_base"); then
warn "failed to resolve helper stub '$stub_base' during cleanup" warn "failed to resolve helper stub '$stub_base' during cleanup"
return 0 return 0
fi fi
@@ -269,6 +339,18 @@ validate_basename() {
[[ $name =~ ^[A-Za-z0-9._+-]+$ ]] || error "invalid basename '$name'" [[ $name =~ ^[A-Za-z0-9._+-]+$ ]] || error "invalid basename '$name'"
} }
sanitize_engine_name() {
local name=$1
local lowered=${name,,}
if [[ $lowered != "$name" ]]; then
warn "normalizing name '$name' to '$lowered' for container engine compatibility"
fi
if [[ ! $lowered =~ ^[a-z0-9_.-]+$ ]]; then
error "engine name '$name' is invalid after normalization"
fi
printf '%s' "$lowered"
}
prepare_build_context() { prepare_build_context() {
if [[ -n $SLOPTRAP_BUILD_CONTEXT && -d $SLOPTRAP_BUILD_CONTEXT ]]; then if [[ -n $SLOPTRAP_BUILD_CONTEXT && -d $SLOPTRAP_BUILD_CONTEXT ]]; then
return 0 return 0
@@ -282,6 +364,9 @@ prepare_build_context() {
select_codex_home() { select_codex_home() {
local preferred="$HOME/.codex" local preferred="$HOME/.codex"
if [[ -L $preferred ]]; then
error "Codex home '$preferred' must not be a symlink"
fi
if [[ -e $preferred && ! -d $preferred ]]; then if [[ -e $preferred && ! -d $preferred ]]; then
error "expected Codex home '$preferred' to be a directory" error "expected Codex home '$preferred' to be a directory"
fi fi
@@ -298,7 +383,7 @@ select_codex_home() {
assert_path_within_code_dir() { assert_path_within_code_dir() {
local candidate=$1 local candidate=$1
local resolved local resolved
resolved=$(realpath -m "$candidate") resolved=$(resolve_path_strict "$candidate")
if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then
error "path '$candidate' escapes project root '$CODE_DIR'" error "path '$candidate' escapes project root '$CODE_DIR'"
fi fi
@@ -345,7 +430,7 @@ sanitize_ignore_rel() {
error "$source: .sloptrapignore entry '$original' contains control characters" error "$source: .sloptrapignore entry '$original' contains control characters"
fi fi
local resolved local resolved
resolved=$(realpath -m "$CODE_DIR/$rel") resolved=$(resolve_path_strict "$CODE_DIR/$rel")
if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then
error "$source: .sloptrapignore entry '$original' resolves outside the project root" error "$source: .sloptrapignore entry '$original' resolves outside the project root"
fi fi
@@ -529,11 +614,12 @@ ensure_safe_for_make() {
validate_package_list() { validate_package_list() {
local key=$1 local key=$1
local raw=$2 local raw=$2
local source=${3:-$MANIFEST_PATH}
[[ -z $raw ]] && return 0 [[ -z $raw ]] && return 0
local token local token
for token in $raw; do for token in $raw; do
if [[ ! $token =~ ^[A-Za-z0-9+.-]+$ ]]; then if [[ ! $token =~ ^[A-Za-z0-9+.-]+$ ]]; then
error "$MANIFEST_PATH: invalid package name '$token' in '$key'" error "$source: invalid package name '$token' in '$key'"
fi fi
done done
} }
@@ -552,7 +638,11 @@ detect_container_engine() {
printf 'podman' printf 'podman'
return 0 return 0
fi fi
error "podman is required but was not found in PATH; set SLOPTRAP_CONTAINER_ENGINE to override explicitly" if command -v docker >/dev/null 2>&1; then
printf 'docker'
return 0
fi
error "container engine not found in PATH; install podman (preferred) or docker, or set SLOPTRAP_CONTAINER_ENGINE explicitly"
} }
parse_manifest() { parse_manifest() {
@@ -592,6 +682,8 @@ print_config() {
info_line "container_name=%s\n" "$SLOPTRAP_CONTAINER_NAME" info_line "container_name=%s\n" "$SLOPTRAP_CONTAINER_NAME"
info_line "codex_home=%s\n" "$CODEX_HOME_HOST" info_line "codex_home=%s\n" "$CODEX_HOME_HOST"
info_line "codex_home_bootstrap=%s\n" "$CODEX_HOME_BOOTSTRAP" info_line "codex_home_bootstrap=%s\n" "$CODEX_HOME_BOOTSTRAP"
info_line "codex_archive=%s\n" "$SLOPTRAP_CODEX_ARCHIVE"
info_line "codex_url=%s\n" "$SLOPTRAP_CODEX_URL"
info_line "needs_login=%s\n" "$NEED_LOGIN" info_line "needs_login=%s\n" "$NEED_LOGIN"
info_line "codex_args=%s\n" "$CODEX_ARGS_DISPLAY" info_line "codex_args=%s\n" "$CODEX_ARGS_DISPLAY"
info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE" info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE"
@@ -626,6 +718,7 @@ SLOPTRAP_PACKAGES_BASE=""
SLOPTRAP_PACKAGES_EXTRA_RESOLVED="" SLOPTRAP_PACKAGES_EXTRA_RESOLVED=""
SLOPTRAP_CODEX_BIN_NAME="" SLOPTRAP_CODEX_BIN_NAME=""
SLOPTRAP_CODEX_URL="" SLOPTRAP_CODEX_URL=""
SLOPTRAP_CODEX_ARCHIVE=""
SLOPTRAP_CODEX_HOME_CONT="" SLOPTRAP_CODEX_HOME_CONT=""
SLOPTRAP_SECURITY_OPTS_EXTRA="" SLOPTRAP_SECURITY_OPTS_EXTRA=""
SLOPTRAP_VOLUME_LABEL="" SLOPTRAP_VOLUME_LABEL=""
@@ -650,6 +743,29 @@ get_env_default() {
fi fi
} }
validate_codex_archive_name() {
local name=$1
[[ $name =~ ^codex-[A-Za-z0-9_.-]+$ ]] || error "invalid Codex archive name '$name'"
}
detect_codex_archive_name() {
local os arch codex_os codex_arch
os=$(uname -s 2>/dev/null || true)
arch=$(uname -m 2>/dev/null || true)
[[ -n $os ]] || error "failed to detect host OS for Codex download"
[[ -n $arch ]] || error "failed to detect host architecture for Codex download"
case "$os" in
Linux|Darwin) codex_os="unknown-linux-gnu" ;; # Codex runs inside a Debian-based image
*) error "unsupported host OS '$os' for Codex download" ;;
esac
case "$arch" in
x86_64|amd64) codex_arch="x86_64" ;;
arm64|aarch64) codex_arch="aarch64" ;;
*) error "unsupported host architecture '$arch' for Codex download" ;;
esac
printf 'codex-%s-%s' "$codex_arch" "$codex_os"
}
resolve_container_workdir() { resolve_container_workdir() {
if [[ -z ${SLOPTRAP_WORKDIR:-} ]]; then if [[ -z ${SLOPTRAP_WORKDIR:-} ]]; then
SLOPTRAP_WORKDIR=$(get_env_default "SLOPTRAP_WORKDIR" "/workspace") SLOPTRAP_WORKDIR=$(get_env_default "SLOPTRAP_WORKDIR" "/workspace")
@@ -690,6 +806,8 @@ ensure_codex_home_dir() {
fetch_latest_codex_digest() { fetch_latest_codex_digest() {
local api_url="https://api.github.com/repos/openai/codex/releases/latest" local api_url="https://api.github.com/repos/openai/codex/releases/latest"
local target_asset="${SLOPTRAP_CODEX_ARCHIVE}.tar.gz"
[[ -n $SLOPTRAP_CODEX_ARCHIVE ]] || error "Codex archive name is not set"
if ! command -v jq >/dev/null 2>&1; then if ! command -v jq >/dev/null 2>&1; then
error "jq is required to verify the Codex binary digest" error "jq is required to verify the Codex binary digest"
fi fi
@@ -698,7 +816,7 @@ fetch_latest_codex_digest() {
error "failed to download Codex release metadata from GitHub" error "failed to download Codex release metadata from GitHub"
fi fi
local digest_line local digest_line
digest_line=$(jq -r '.assets[] | select(.name == "codex-x86_64-unknown-linux-gnu.tar.gz") | .digest' <<<"$response" | head -n 1) digest_line=$(jq -r --arg name "$target_asset" '.assets[] | select(.name == $name) | .digest' <<<"$response" | head -n 1)
if [[ -z $digest_line || $digest_line == "null" ]]; then if [[ -z $digest_line || $digest_line == "null" ]]; then
error "failed to resolve Codex digest from GitHub response" error "failed to resolve Codex digest from GitHub response"
fi fi
@@ -711,14 +829,15 @@ ensure_codex_binary() {
if [[ -x $CODEX_BIN_PATH ]]; then if [[ -x $CODEX_BIN_PATH ]]; then
return 0 return 0
fi fi
local tar_transform="s/${SLOPTRAP_CODEX_ARCHIVE}/${SLOPTRAP_CODEX_BIN_NAME}/"
local download_dir local download_dir
download_dir=$(create_temp_dir "codex") download_dir=$(create_temp_dir "codex")
local tmp_archive="$download_dir/codex.tar.gz" local tmp_archive="$download_dir/codex.tar.gz"
if $DRY_RUN; then if $DRY_RUN; then
print_command curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL" print_command curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"
print_command sha256sum -c - print_command sha256sum -c -
print_command tar -xzf "$tmp_archive" --transform="s/codex-x86_64-unknown-linux-gnu/$SLOPTRAP_CODEX_BIN_NAME/" -C "$SLOPTRAP_BUILD_CONTEXT" print_command tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"
print_command chmod +x "$CODEX_BIN_PATH" print_command chmod 0755 "$CODEX_BIN_PATH"
return 0 return 0
fi fi
if ! curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"; then if ! curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"; then
@@ -731,12 +850,12 @@ ensure_codex_binary() {
rm -rf "$download_dir" "$CODEX_BIN_PATH" rm -rf "$download_dir" "$CODEX_BIN_PATH"
error "checksum verification failed for $SLOPTRAP_CODEX_BIN_NAME" error "checksum verification failed for $SLOPTRAP_CODEX_BIN_NAME"
fi fi
if ! tar -xzf "$tmp_archive" --transform="s/codex-x86_64-unknown-linux-gnu/$SLOPTRAP_CODEX_BIN_NAME/" -C "$SLOPTRAP_BUILD_CONTEXT"; then if ! tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"; then
rm -rf "$download_dir" rm -rf "$download_dir"
error "failed to extract Codex binary" error "failed to extract Codex binary"
fi fi
rm -rf "$download_dir" rm -rf "$download_dir"
chmod +x "$CODEX_BIN_PATH" chmod 0755 "$CODEX_BIN_PATH"
} }
ensure_safe_sandbox() { ensure_safe_sandbox() {
@@ -776,7 +895,7 @@ normalize_package_list() {
prepare_container_runtime() { prepare_container_runtime() {
resolve_container_workdir resolve_container_workdir
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA
SLOPTRAP_SHARED_DIR_ABS=$(get_env_default "SLOPTRAP_SHARED_DIR" "$CODE_DIR") SLOPTRAP_SHARED_DIR_ABS="$CODE_DIR"
if [[ ! -d $SLOPTRAP_SHARED_DIR_ABS ]]; then if [[ ! -d $SLOPTRAP_SHARED_DIR_ABS ]]; then
error "shared directory '$SLOPTRAP_SHARED_DIR_ABS' does not exist" error "shared directory '$SLOPTRAP_SHARED_DIR_ABS' does not exist"
fi fi
@@ -799,7 +918,28 @@ prepare_container_runtime() {
fi fi
SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps") SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps")
SLOPTRAP_CODEX_URL=$(get_env_default "SLOPTRAP_CODEX_URL" "https://github.com/openai/codex/releases/latest/download/codex-x86_64-unknown-linux-gnu.tar.gz") validate_package_list "SLOPTRAP_PACKAGES" "$SLOPTRAP_PACKAGES_BASE" "SLOPTRAP_PACKAGES"
local default_codex_archive
default_codex_archive=$(detect_codex_archive_name)
local env_codex_archive
env_codex_archive=$(printenv "SLOPTRAP_CODEX_ARCHIVE" 2>/dev/null || true)
if [[ -n $env_codex_archive ]]; then
SLOPTRAP_CODEX_ARCHIVE=$env_codex_archive
else
SLOPTRAP_CODEX_ARCHIVE=$default_codex_archive
fi
validate_codex_archive_name "$SLOPTRAP_CODEX_ARCHIVE"
local default_codex_url="https://github.com/openai/codex/releases/latest/download/${SLOPTRAP_CODEX_ARCHIVE}.tar.gz"
SLOPTRAP_CODEX_URL=$(get_env_default "SLOPTRAP_CODEX_URL" "$default_codex_url")
if [[ -z $env_codex_archive ]]; then
local inferred_archive
inferred_archive=$(basename "${SLOPTRAP_CODEX_URL%%\?*}")
inferred_archive=${inferred_archive%.tar.gz}
if [[ $inferred_archive == codex-* ]]; then
validate_codex_archive_name "$inferred_archive"
SLOPTRAP_CODEX_ARCHIVE=$inferred_archive
fi
fi
SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "codex") SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "codex")
SLOPTRAP_CODEX_HOME_CONT=$(get_env_default "SLOPTRAP_CODEX_HOME_CONT" "/codex") SLOPTRAP_CODEX_HOME_CONT=$(get_env_default "SLOPTRAP_CODEX_HOME_CONT" "/codex")
SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337") SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337")
@@ -828,6 +968,8 @@ prepare_container_runtime() {
SLOPTRAP_ROOTFS_READONLY=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1") SLOPTRAP_ROOTFS_READONLY=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1")
SLOPTRAP_IMAGE_NAME=$(get_env_default "SLOPTRAP_IMAGE_NAME" "${PROJECT_NAME}-sloptrap-image") SLOPTRAP_IMAGE_NAME=$(get_env_default "SLOPTRAP_IMAGE_NAME" "${PROJECT_NAME}-sloptrap-image")
SLOPTRAP_CONTAINER_NAME=$(get_env_default "SLOPTRAP_CONTAINER_NAME" "${PROJECT_NAME}-sloptrap-container") SLOPTRAP_CONTAINER_NAME=$(get_env_default "SLOPTRAP_CONTAINER_NAME" "${PROJECT_NAME}-sloptrap-container")
SLOPTRAP_IMAGE_NAME=$(sanitize_engine_name "$SLOPTRAP_IMAGE_NAME")
SLOPTRAP_CONTAINER_NAME=$(sanitize_engine_name "$SLOPTRAP_CONTAINER_NAME")
local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME" --init) local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME" --init)
local -a security_opts=(--cap-drop=ALL --security-opt no-new-privileges) local -a security_opts=(--cap-drop=ALL --security-opt no-new-privileges)
@@ -1167,6 +1309,9 @@ if [[ ! -d $CODE_DIR_INPUT ]]; then
fi fi
CODE_DIR="$(cd "$CODE_DIR_INPUT" && pwd -P)" CODE_DIR="$(cd "$CODE_DIR_INPUT" && pwd -P)"
if [[ $CODE_DIR == "/" ]]; then
error "project root may not be '/'"
fi
MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME" MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME"
if [[ -f $MANIFEST_PATH ]]; then if [[ -f $MANIFEST_PATH ]]; then