#!/usr/bin/env bash # sloptrap set -euo pipefail 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 COLOR_TEXT=$'\033[38;5;247m' COLOR_HIGHLIGHT=$'\033[38;5;202m' COLOR_ERROR=$'\033[38;5;160m' COLOR_COMMENT=$'\033[38;5;242m' RESET=$'\033[0m' BOLD=$'\033[1m' PREFIX_TEXT=$'\033[38;5;247m░\033[0m ' PREFIX_HIGHLIGHT=$'\033[38;5;202m█\033[0m ' PREFIX_ERROR=$'\033[38;5;160m▒\033[0m ' PREFIX_COMMENT=$'\033[38;5;242m▒\033[0m ' print_styled() { local color=$1 local prefix=$2 local fmt=$3 shift 3 printf '%s' "$prefix" printf '%b' "$color" if (($# > 0)); then local -a colored_args=() local arg for arg in "$@"; do colored_args+=("$COLOR_HIGHLIGHT$arg$color") done # Format strings are defined within sloptrap, not user input. # shellcheck disable=SC2059 printf "$fmt" "${colored_args[@]}" else printf '%b' "$fmt" fi printf '%b' "$RESET" } print_styled_err() { local color=$1 local prefix=$2 local fmt=$3 shift 3 printf '%s' "$prefix" >&2 printf '%b' "$color" >&2 if (($# > 0)); then local -a colored_args=() local arg for arg in "$@"; do colored_args+=("$COLOR_HIGHLIGHT$arg$color") done # Format strings are defined within sloptrap, not user input. # shellcheck disable=SC2059 printf "$fmt" "${colored_args[@]}" >&2 else printf '%b' "$fmt" >&2 fi printf '%b' "$RESET" >&2 } info_line() { print_styled "$COLOR_TEXT" "$PREFIX_TEXT" "$@" } highlight_line() { print_styled "$COLOR_HIGHLIGHT" "$PREFIX_TEXT" "$@" } status_line() { print_styled "$COLOR_COMMENT" "$PREFIX_TEXT" "$@" } comment_line() { print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@" } warn_line() { print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@" } error_line() { print_styled_err "$COLOR_ERROR" "$PREFIX_ERROR" "$@" } print_banner() { printf '%b' "${COLOR_HIGHLIGHT}${BOLD}" cat <<'EOF' ██████ ██▓ ▒█████ ██▓███ ▄▄▄█████▓ ██▀███ ▄▄▄ ██▓███ ▒██ ▒ ▓██▒ ▒██▒ ██▒▓██░ ██▒▓ ██▒ ▓▒▓██ ▒ ██▒▒████▄ ▓██░ ██▒ ░ ▓██▄ ▒██░ ▒██░ ██▒▓██░ ██▓▒▒ ▓██░ ▒░▓██ ░▄█ ▒▒██ ▀█▄ ▓██░ ██▓▒ ▒ ██▒▒██░ ▒██ ██░▒██▄█▓▒ ▒░ ▓██▓ ░ ▒██▀▀█▄ ░██▄▄▄▄██ ▒██▄█▓▒ ▒ ▒██████▒▒░██████▒░ ████▓▒░▒██▒ ░ ░ ▒██▒ ░ ░██▓ ▒██▒ ▓█ ▓██▒▒██▒ ░ ░ ▒ ▒▓▒ ▒ ░░ ▒░▓ ░░ ▒░▒░▒░ ▒▓▒░ ░ ░ ▒ ░░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░▒▓▒░ ░ ░ ░▒ ░ ░░ ░ ▒░ https://git.sk4.nz/sk4nz/skz-sloptrap ▒ ▒▒ ░░▒ ░ ░ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ EOF printf '%b' "$RESET" } MANIFEST_BASENAME=".sloptrap" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VALID_NAME_REGEX='^[A-Za-z0-9_.-]+$' DEFAULT_CODEX_ARGS=(--sandbox workspace-write) SLOPTRAP_IMAGE_LABEL_KEY="net.sk4nz.sloptrap.managed" SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1" usage() { print_banner info_line "Usage: %s [options] [target ...]\n" "$0" info_line "Options:\n" comment_line " --dry-run Show planned container command(s) and exit\n" comment_line " --print-config Display resolved manifest values\n" comment_line " -h, --help Show this message\n" info_line "\n" comment_line "Each project supplies configuration via a %s file in its root.\n" "$MANIFEST_BASENAME" info_line "Example manifest entries:\n" comment_line " name=my-project\n" comment_line " packages_extra=kubectl helm\n" comment_line " default_targets=run\n" comment_line " codex_args=--sandbox workspace-write\n" info_line "\n" info_line "Example targets:\n" comment_line " run Build if needed, then launch Codex\n" comment_line " resume Build if needed, then run 'codex resume '\n" comment_line " shell Drop into an interactive /bin/bash session\n" comment_line " clean Remove the project container/image cache\n" comment_line " prune Remove dangling/unused sloptrap images\n" } error() { error_line "error: %s\n" "$1" exit 1 } warn() { warn_line "warning: %s\n" "$1" } trim() { local value=$1 value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" 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 SLOPTRAP_IGNORE_ENTRIES=() declare -a IGNORE_MOUNT_ARGS=() MANIFEST_PRESENT=false CURRENT_IGNORE_FILE="" CONTAINER_ENGINE="" CODEX_HOME_HOST="" CODEX_HOME_BOOTSTRAP=false NEED_LOGIN=false IGNORE_STUB_BASE="" IGNORE_HELPER_ROOT="" ALLOW_HOST_NETWORK=false declare -a SLOPTRAP_TEMP_PATHS=() register_temp_path() { SLOPTRAP_TEMP_PATHS+=("$1") } cleanup_temp_paths() { local path for path in "${SLOPTRAP_TEMP_PATHS[@]}"; do [[ -n ${path:-} ]] || continue rm -rf "$path" >/dev/null 2>&1 || true done } cleanup_ignore_stub_dir() { local helper_root=${IGNORE_HELPER_ROOT:-} local stub_base=${IGNORE_STUB_BASE:-} [[ -n $helper_root && -n $stub_base ]] || return 0 [[ -d $stub_base ]] || return 0 local resolved_helper resolved_stub if ! resolved_helper=$(resolve_path_relaxed "$helper_root"); then warn "failed to resolve helper root '$helper_root' during cleanup" return 0 fi if ! resolved_stub=$(resolve_path_relaxed "$stub_base"); then warn "failed to resolve helper stub '$stub_base' during cleanup" return 0 fi case "$resolved_stub" in "$resolved_helper"/session-*|"$resolved_helper"/fallback) rm -rf "$resolved_stub" ;; *) warn "refusing to remove unexpected helper path '$resolved_stub'" ;; esac } sloptrap_exit_trap() { cleanup_temp_paths cleanup_ignore_stub_dir } trap sloptrap_exit_trap EXIT INT TERM HUP create_temp_dir() { local label=${1:-tmp} local template="${TMPDIR:-/tmp}/sloptrap.${label}.XXXXXXXX" local dir if ! dir=$(mktemp -d "$template"); then error "failed to create temporary directory under ${TMPDIR:-/tmp}" fi register_temp_path "$dir" printf '%s' "$dir" } write_embedded_dockerfile() { cat <<'EOF' # Dockerfile.sloptrap ARG BASE_IMAGE=debian:trixie-slim FROM ${BASE_IMAGE} ENV DEBIAN_FRONTEND=noninteractive ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps" ARG EXTRA_PACKAGES="" RUN apt-get update \ && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} \ && rm -rf /var/lib/apt/lists/* ARG CODEX_UID=1337 ARG CODEX_GID=1337 RUN groupadd --gid ${CODEX_GID} sloptrap \ && useradd --create-home --home-dir /home/sloptrap \ --gid sloptrap --uid ${CODEX_UID} --shell /bin/bash sloptrap ARG CODEX_BIN=codex ARG CODEX_CONF=config/config.toml COPY ${CODEX_BIN} /usr/local/bin/codex RUN chown -R sloptrap:sloptrap /home/sloptrap USER sloptrap WORKDIR /workspace ENV SHELL=/bin/bash HOME=/home/sloptrap ENTRYPOINT ["codex"] EOF } populate_dockerfile() { local destination=$1 mkdir -p "$(dirname "$destination")" if [[ -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then if [[ ! -f $SLOPTRAP_DOCKERFILE_SOURCE ]]; then error "container recipe '$SLOPTRAP_DOCKERFILE_SOURCE' not found" fi cp "$SLOPTRAP_DOCKERFILE_SOURCE" "$destination" else write_embedded_dockerfile >"$destination" fi } validate_basename() { local name=$1 [[ $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() { if [[ -n $SLOPTRAP_BUILD_CONTEXT && -d $SLOPTRAP_BUILD_CONTEXT ]]; then return 0 fi SLOPTRAP_BUILD_CONTEXT=$(create_temp_dir "context") SLOPTRAP_DOCKERFILE_PATH="$SLOPTRAP_BUILD_CONTEXT/Dockerfile.sloptrap" populate_dockerfile "$SLOPTRAP_DOCKERFILE_PATH" validate_basename "$SLOPTRAP_CODEX_BIN_NAME" CODEX_BIN_PATH="$SLOPTRAP_BUILD_CONTEXT/$SLOPTRAP_CODEX_BIN_NAME" } select_codex_home() { local preferred="$HOME/.codex" if [[ -L $preferred ]]; then error "Codex home '$preferred' must not be a symlink" fi if [[ -e $preferred && ! -d $preferred ]]; then error "expected Codex home '$preferred' to be a directory" fi CODEX_HOME_HOST="$preferred" if [[ -d $CODEX_HOME_HOST ]]; then CODEX_HOME_HOST="$(cd "$CODEX_HOME_HOST" && pwd -P)" CODEX_HOME_BOOTSTRAP=false else CODEX_HOME_BOOTSTRAP=true fi } assert_path_within_code_dir() { local candidate=$1 local resolved resolved=$(resolve_path_strict "$candidate") if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then error "path '$candidate' escapes project root '$CODE_DIR'" fi } ensure_ignore_helper_root() { local helper_root="$CODE_DIR/.sloptrap-ignores" if [[ -L $helper_root ]]; then error "$helper_root: helper directory may not be a symlink" fi if [[ -e $helper_root && ! -d $helper_root ]]; then error "$helper_root: expected a directory" fi assert_path_within_code_dir "$helper_root" IGNORE_HELPER_ROOT="$helper_root" } sanitize_ignore_rel() { local rel=$1 local original=$rel local source=${CURRENT_IGNORE_FILE:-$MANIFEST_PATH} while [[ $rel == ./* ]]; do rel=${rel:2} done if [[ -z $rel || $rel == "." ]]; then error "$source: .sloptrapignore entry '$original' may not target the project root" fi if [[ ${rel:0:1} == "/" ]]; then error "$source: .sloptrapignore entry '$original' must be relative" fi local IFS='/' local -a segments=() read -r -a segments <<< "$rel" if [[ ${#segments[@]} -eq 0 ]]; then error "$source: .sloptrapignore entry '$original' is invalid" fi local segment for segment in "${segments[@]}"; do if [[ -z $segment || $segment == "." || $segment == ".." ]]; then error "$source: .sloptrapignore entry '$original' uses disallowed path components" fi done if [[ ${rel//$'\n'/} != "$rel" || ${rel//$'\r'/} != "$rel" ]]; then error "$source: .sloptrapignore entry '$original' contains control characters" fi local resolved resolved=$(resolve_path_strict "$CODE_DIR/$rel") if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then error "$source: .sloptrapignore entry '$original' resolves outside the project root" fi printf '%s' "$rel" } resolve_sloptrap_ignore() { local root=$1 local ignore_file="$root/.sloptrapignore" SLOPTRAP_IGNORE_ENTRIES=() [[ -f $ignore_file ]] || return 0 CURRENT_IGNORE_FILE="$ignore_file" local -a patterns=() local line trimmed while IFS= read -r line || [[ -n $line ]]; do trimmed="$(trim "$line")" [[ -z $trimmed ]] && continue [[ ${trimmed:0:1} == "#" ]] && continue patterns+=("$trimmed") done <"$ignore_file" [[ ${#patterns[@]} -gt 0 ]] || return 0 local -A selected=() local pattern negate anchored dir_only raw had_match local -a matches local match rel local prev_gs prev_ng prev_dg prev_gs=$(shopt -p globstar 2>/dev/null || true) prev_ng=$(shopt -p nullglob 2>/dev/null || true) prev_dg=$(shopt -p dotglob 2>/dev/null || true) shopt -s globstar nullglob dotglob for pattern in "${patterns[@]}"; do negate=false anchored=false dir_only=false raw="$pattern" had_match=false if [[ ${raw:0:1} == "!" ]]; then negate=true raw=${raw:1} fi if [[ ${raw:0:1} == "/" ]]; then anchored=true raw=${raw:1} fi if [[ ${raw: -1} == "/" ]]; then dir_only=true raw=${raw%/} fi [[ -n $raw ]] || raw="." matches=() if $anchored; then while IFS= read -r match; do matches+=("$root/$match") done < <( cd "$root" && { compgen -G "$raw" || true; } ) else while IFS= read -r match; do matches+=("$root/$match") done < <( cd "$root" && { compgen -G "$raw" || true; } ) while IFS= read -r match; do matches+=("$root/$match") done < <( cd "$root" && { compgen -G "**/$raw" || true; } ) fi local -A seen=() for match in "${matches[@]}"; do [[ -e $match ]] || continue if [[ $match != "$root" && $match != $root/* ]]; then continue fi rel=${match#"$root"/} [[ -n $rel ]] || rel="." if $dir_only && [[ ! -d $match ]]; then continue fi if [[ -n ${seen[$rel]-} ]]; then continue fi seen[$rel]=1 rel=$(sanitize_ignore_rel "$rel") had_match=true if $negate; then unset 'selected[$rel]' else if [[ -d $match ]]; then selected[$rel]="dir" else selected[$rel]="file" fi fi done if ! $had_match && ! $negate; then warn "${CURRENT_IGNORE_FILE:-$MANIFEST_PATH}: .sloptrapignore entry '$pattern' matched no files" fi done eval "$prev_gs" eval "$prev_ng" eval "$prev_dg" if [[ ${#selected[@]} -eq 0 ]]; then CURRENT_IGNORE_FILE="" return 0 fi SLOPTRAP_IGNORE_ENTRIES=() for rel in "${!selected[@]}"; do SLOPTRAP_IGNORE_ENTRIES+=("${selected[$rel]}"$'\t'"$rel") done mapfile -t SLOPTRAP_IGNORE_ENTRIES < <(printf '%s\n' "${SLOPTRAP_IGNORE_ENTRIES[@]}" | sort) CURRENT_IGNORE_FILE="" } escape_mount_value() { local value=$1 value=${value//\\/\\\\} value=${value//,/\\,} value=${value//=/\\=} printf '%s' "$value" } prepare_ignore_mounts() { IGNORE_MOUNT_ARGS=() [[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]] || return 0 local container_root="${SLOPTRAP_WORKDIR:-/workspace}" if [[ $container_root != "/" ]]; then container_root="${container_root%/}" [[ -n $container_root ]] || container_root="/" fi local entry type rel target host_path stub_base target_mount host_path_mount stub_base="$IGNORE_STUB_BASE" assert_path_within_code_dir "$stub_base" rm -rf "$stub_base" 2>/dev/null || true if ! mkdir -p "$stub_base/files" 2>/dev/null; then stub_base="$CODE_DIR/.sloptrap-ignores/fallback" assert_path_within_code_dir "$stub_base" rm -rf "$stub_base" 2>/dev/null || true mkdir -p "$stub_base/files" fi IGNORE_STUB_BASE="$stub_base" for entry in "${SLOPTRAP_IGNORE_ENTRIES[@]}"; do type=${entry%%$'\t'*} rel=${entry#*$'\t'} if [[ $container_root == "/" ]]; then target="/$rel" else target="$container_root/$rel" fi target_mount=$(escape_mount_value "$target") case "$type" in dir) IGNORE_MOUNT_ARGS+=("--mount" "type=tmpfs,target=$target_mount") ;; file) host_path="$stub_base/files/$rel" assert_path_within_code_dir "$host_path" mkdir -p "$(dirname "$host_path")" : > "$host_path" host_path_mount=$(escape_mount_value "$host_path") IGNORE_MOUNT_ARGS+=("--mount" "type=bind,source=$host_path_mount,target=$target_mount,readonly") ;; esac done } ensure_safe_for_make() { local key=$1 local value=$2 if [[ $value == *'$'* || $value == *'`'* ]]; then error "$MANIFEST_PATH: value for '$key' must not contain \$ or \` characters" fi if [[ $value == *$'\n'* ]]; then error "$MANIFEST_PATH: value for '$key' must not span multiple lines" fi } validate_package_list() { local key=$1 local raw=$2 local source=${3:-$MANIFEST_PATH} [[ -z $raw ]] && return 0 local token for token in $raw; do if [[ ! $token =~ ^[A-Za-z0-9+.-]+$ ]]; then error "$source: invalid package name '$token' in '$key'" fi done } detect_container_engine() { local override=${SLOPTRAP_CONTAINER_ENGINE-} if [[ -n $override ]]; then local engine="${override,,}" if ! command -v "$engine" >/dev/null 2>&1; then error "container engine '$engine' not found in PATH" fi printf '%s' "$engine" return 0 fi if command -v podman >/dev/null 2>&1; then printf 'podman' return 0 fi 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() { local manifest_path=$1 local line key value while IFS= read -r line || [[ -n $line ]]; do line="$(trim "$line")" [[ -z $line ]] && continue [[ ${line:0:1} == "#" ]] && continue if [[ $line != *"="* ]]; then error "$manifest_path: expected KEY=VALUE entry (got '$line')" fi key="$(trim "${line%%=*}")" value="$(trim "${line#*=}")" if [[ -z $key ]]; then error "$manifest_path: blank key in line '$line'" fi if [[ ( ${value:0:1} == '"' && ${value: -1} == '"' ) || ( ${value:0:1} == "'" && ${value: -1} == "'" ) ]]; then value="${value:1:-1}" fi if [[ $key == make.* ]]; then error "$manifest_path: make.* overrides are no longer supported; use packages_extra instead" fi MANIFEST["$key"]=$value done < "$manifest_path" } print_config() { local manifest_path=$1 info_line "manifest_path=%s\n" "$manifest_path" info_line "manifest_present=%s\n" "$MANIFEST_PRESENT" info_line "project_name=%s\n" "$PROJECT_NAME" info_line "project_dir=%s\n" "$CODE_DIR" info_line "resolved_targets=%s\n" "${DEFAULT_TARGETS[*]}" info_line "container_engine=%s\n" "$CONTAINER_ENGINE" info_line "image_name=%s\n" "$SLOPTRAP_IMAGE_NAME" info_line "container_name=%s\n" "$SLOPTRAP_CONTAINER_NAME" info_line "codex_home=%s\n" "$CODEX_HOME_HOST" 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 "codex_args=%s\n" "$CODEX_ARGS_DISPLAY" info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE" if [[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]]; then local ignore_paths ignore_paths=$(printf '%s ' "${SLOPTRAP_IGNORE_ENTRIES[@]}") ignore_paths=${ignore_paths% } info_line "ignore_paths=%s\n" "$ignore_paths" else info_line "ignore_paths=\n" fi if [[ -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then info_line "dockerfile_source=%s\n" "$SLOPTRAP_DOCKERFILE_SOURCE" else info_line "dockerfile_source=embedded\n" fi for key in "${!MANIFEST[@]}"; do info_line "%s=%s\n" "$key" "${MANIFEST[$key]}" done } declare -a CONTAINER_SHARED_OPTS=() declare -a BASE_CONTAINER_CMD=() SLOPTRAP_IMAGE_NAME="" SLOPTRAP_CONTAINER_NAME="" SLOPTRAP_DOCKERFILE_PATH="" SLOPTRAP_BUILD_CONTEXT="" SLOPTRAP_DOCKERFILE_SOURCE="" CODEX_BIN_PATH="" SLOPTRAP_SHARED_DIR_ABS="" SLOPTRAP_PACKAGES_BASE="" SLOPTRAP_PACKAGES_EXTRA_RESOLVED="" SLOPTRAP_CODEX_BIN_NAME="" SLOPTRAP_CODEX_URL="" SLOPTRAP_CODEX_ARCHIVE="" SLOPTRAP_CODEX_HOME_CONT="" SLOPTRAP_SECURITY_OPTS_EXTRA="" SLOPTRAP_VOLUME_LABEL="" SLOPTRAP_WORKDIR=${SLOPTRAP_WORKDIR-} SLOPTRAP_NETWORK_NAME="" SLOPTRAP_LIMITS_PID="" SLOPTRAP_LIMITS_RAM="" SLOPTRAP_LIMITS_SWP="" SLOPTRAP_LIMITS_SHM="" SLOPTRAP_LIMITS_CPU="" SLOPTRAP_TMPFS_PATHS="" SLOPTRAP_ROOTFS_READONLY="" get_env_default() { local var=$1 local default=$2 local value if value=$(printenv "$var" 2>/dev/null); then printf '%s' "$value" else printf '%s' "$default" 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() { if [[ -z ${SLOPTRAP_WORKDIR:-} ]]; then SLOPTRAP_WORKDIR=$(get_env_default "SLOPTRAP_WORKDIR" "/workspace") fi [[ -n $SLOPTRAP_WORKDIR ]] || SLOPTRAP_WORKDIR="/workspace" if [[ $SLOPTRAP_WORKDIR == "/" ]]; then error "SLOPTRAP_WORKDIR may not be '/'; use a subdirectory like /workspace" fi } print_command() { local rendered="" if [[ $# -gt 0 ]]; then rendered=$(printf '%q ' "$@") rendered=${rendered% } fi comment_line "%s\n" "$rendered" } run_or_print() { if $DRY_RUN; then print_command "$@" return 0 fi "$@" } ensure_codex_home_dir() { if [[ -d $CODEX_HOME_HOST ]]; then return 0 fi if $DRY_RUN; then print_command mkdir -p "$CODEX_HOME_HOST" return 0 fi mkdir -p "$CODEX_HOME_HOST" } fetch_latest_codex_digest() { 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 error "jq is required to verify the Codex binary digest" fi local response if ! response=$(curl -fsSL "$api_url"); then error "failed to download Codex release metadata from GitHub" fi local digest_line 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 error "failed to resolve Codex digest from GitHub response" fi digest_line=${digest_line#sha256:} printf '%s' "$digest_line" } ensure_codex_binary() { prepare_build_context if [[ -x $CODEX_BIN_PATH ]]; then return 0 fi local tar_transform="s/${SLOPTRAP_CODEX_ARCHIVE}/${SLOPTRAP_CODEX_BIN_NAME}/" local download_dir download_dir=$(create_temp_dir "codex") local tmp_archive="$download_dir/codex.tar.gz" if $DRY_RUN; then print_command curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL" print_command sha256sum -c - print_command tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT" print_command chmod 0755 "$CODEX_BIN_PATH" return 0 fi if ! curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"; then rm -rf "$download_dir" error "failed to download Codex binary from '$SLOPTRAP_CODEX_URL'" fi local expected_digest expected_digest=$(fetch_latest_codex_digest) if ! printf "%s %s\n" "$expected_digest" "$tmp_archive" | sha256sum -c - >/dev/null 2>&1; then rm -rf "$download_dir" "$CODEX_BIN_PATH" error "checksum verification failed for $SLOPTRAP_CODEX_BIN_NAME" fi if ! tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"; then rm -rf "$download_dir" error "failed to extract Codex binary" fi rm -rf "$download_dir" chmod 0755 "$CODEX_BIN_PATH" } ensure_safe_sandbox() { local -a args=("$@") local sandbox_mode="" local i=0 while [[ $i -lt ${#args[@]} ]]; do if [[ ${args[$i]} == "--sandbox" ]]; then if (( i + 1 >= ${#args[@]} )); then error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)" fi sandbox_mode="${args[$((i + 1))]}" fi ((i+=1)) done if [[ -z $sandbox_mode ]]; then error "$MANIFEST_PATH: codex_args must include '--sandbox ' (workspace-write or workspace-read-only)" fi case "$sandbox_mode" in workspace-write|workspace-read-only) ;; *) error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write or workspace-read-only)" ;; esac } normalize_package_list() { local raw=$1 raw="$(trim "$raw")" [[ -z $raw ]] && return 0 local -a tokens=() read -r -a tokens <<< "$raw" printf '%s' "${tokens[*]}" } prepare_container_runtime() { resolve_container_workdir SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA SLOPTRAP_SHARED_DIR_ABS="$CODE_DIR" if [[ ! -d $SLOPTRAP_SHARED_DIR_ABS ]]; then error "shared directory '$SLOPTRAP_SHARED_DIR_ABS' does not exist" fi SLOPTRAP_SHARED_DIR_ABS="$(cd "$SLOPTRAP_SHARED_DIR_ABS" && pwd -P)" local dockerfile_override dockerfile_override=$(get_env_default "SLOPTRAP_DOCKERFILE_PATH" "") if [[ -n $dockerfile_override ]]; then if [[ $dockerfile_override != /* ]]; then dockerfile_override="$SCRIPT_DIR/$dockerfile_override" fi if [[ ! -f $dockerfile_override ]]; then error "container recipe '$dockerfile_override' not found" fi SLOPTRAP_DOCKERFILE_SOURCE="$dockerfile_override" elif [[ -f "$SCRIPT_DIR/Dockerfile.sloptrap" ]]; then SLOPTRAP_DOCKERFILE_SOURCE="$SCRIPT_DIR/Dockerfile.sloptrap" else SLOPTRAP_DOCKERFILE_SOURCE="" fi SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps") 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_HOME_CONT=$(get_env_default "SLOPTRAP_CODEX_HOME_CONT" "/codex") SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337") SLOPTRAP_CODEX_GID=$(get_env_default "SLOPTRAP_CODEX_GID" "1337") SLOPTRAP_SECURITY_OPTS_EXTRA=$(get_env_default "SLOPTRAP_SECURITY_OPTS_EXTRA" "") local default_network="bridge" if [[ $CONTAINER_ENGINE == "podman" ]]; then if command -v slirp4netns >/dev/null 2>&1; then default_network="slirp4netns" else warn "podman detected but 'slirp4netns' is missing; falling back to 'bridge' networking" fi fi SLOPTRAP_NETWORK_NAME=$(get_env_default "SLOPTRAP_NETWORK_NAME" "$default_network") if $ALLOW_HOST_NETWORK; then SLOPTRAP_NETWORK_NAME="host" elif [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then error "$MANIFEST_PATH: host networking requires allow_host_network=true" fi SLOPTRAP_LIMITS_PID=$(get_env_default "SLOPTRAP_LIMITS_PID" "1024") SLOPTRAP_LIMITS_RAM=$(get_env_default "SLOPTRAP_LIMITS_RAM" "1024m") SLOPTRAP_LIMITS_SWP=$(get_env_default "SLOPTRAP_LIMITS_SWP" "1024m") SLOPTRAP_LIMITS_SHM=$(get_env_default "SLOPTRAP_LIMITS_SHM" "1024m") SLOPTRAP_LIMITS_CPU=$(get_env_default "SLOPTRAP_LIMITS_CPU" "8") SLOPTRAP_TMPFS_PATHS=$(get_env_default "SLOPTRAP_TMPFS_PATHS" "/tmp:exec /run /run/lock") SLOPTRAP_ROOTFS_READONLY=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1") 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_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 security_opts=(--cap-drop=ALL --security-opt no-new-privileges) if [[ -n $SLOPTRAP_SECURITY_OPTS_EXTRA ]]; then local -a extra_opts=() read -r -a extra_opts <<< "$SLOPTRAP_SECURITY_OPTS_EXTRA" security_opts+=("${extra_opts[@]}") fi local -a resource_opts=( --pids-limit "$SLOPTRAP_LIMITS_PID" --memory "$SLOPTRAP_LIMITS_RAM" --memory-swap "$SLOPTRAP_LIMITS_SWP" --shm-size "$SLOPTRAP_LIMITS_SHM" --cpus "$SLOPTRAP_LIMITS_CPU" ) local -a tmpfs_opts=() if [[ -n $SLOPTRAP_TMPFS_PATHS ]]; then local -a tmpfs_paths=() read -r -a tmpfs_paths <<< "$SLOPTRAP_TMPFS_PATHS" local path for path in "${tmpfs_paths[@]}"; do tmpfs_opts+=(--tmpfs "$path") done fi local rootfs_flag=() case "${SLOPTRAP_ROOTFS_READONLY,,}" in 1|true|yes) rootfs_flag=(--read-only) ;; esac if [[ $CONTAINER_ENGINE == "podman" ]]; then SLOPTRAP_VOLUME_LABEL=":Z" else SLOPTRAP_VOLUME_LABEL="" fi local -a volume_opts=( -v "$SLOPTRAP_SHARED_DIR_ABS:$SLOPTRAP_WORKDIR$SLOPTRAP_VOLUME_LABEL" -v "$CODEX_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL" ) local -a env_args=( -e "HOME=$SLOPTRAP_CODEX_HOME_CONT" -e "XDG_CONFIG_HOME=$SLOPTRAP_CODEX_HOME_CONT/config" -e "XDG_CACHE_HOME=$SLOPTRAP_CODEX_HOME_CONT/cache" -e "XDG_STATE_HOME=$SLOPTRAP_CODEX_HOME_CONT/state" -e "CODEX_HOME=$SLOPTRAP_CODEX_HOME_CONT" ) local uid gid uid=$(id -u) gid=$(id -g) local -a user_opts=("--user" "$uid:$gid") if [[ $CONTAINER_ENGINE == "podman" ]]; then user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}") fi CONTAINER_SHARED_OPTS=( "${network_opts[@]}" "${security_opts[@]}" "${resource_opts[@]}" "${rootfs_flag[@]}" "${tmpfs_opts[@]}" "${volume_opts[@]}" "${env_args[@]}" "${IGNORE_MOUNT_ARGS[@]}" "${user_opts[@]}" -w "$SLOPTRAP_WORKDIR" ) BASE_CONTAINER_CMD=( "$CONTAINER_ENGINE" run --rm -it --name "$SLOPTRAP_CONTAINER_NAME" "${CONTAINER_SHARED_OPTS[@]}" ) } build_image() { ensure_codex_binary print_banner local extra_packages_arg extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") if ! $DRY_RUN; then status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME" fi local -a cmd=( "$CONTAINER_ENGINE" build --quiet -t "$SLOPTRAP_IMAGE_NAME" -f "$SLOPTRAP_DOCKERFILE_PATH" --label "$SLOPTRAP_IMAGE_LABEL" --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" --build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME" --build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID" --build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID" ) if [[ -n $extra_packages_arg ]]; then cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg") fi cmd+=("$SLOPTRAP_BUILD_CONTEXT") if $DRY_RUN; then run_or_print "${cmd[@]}" return fi local build_output if ! build_output=$("${cmd[@]}"); then return 1 fi build_output=$(trim "$build_output") if [[ -n $build_output ]]; then comment_line "Image %s\n" "$build_output" fi } rebuild_image() { ensure_codex_binary if ! $DRY_RUN; then status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME" fi local extra_packages_arg extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") local -a cmd=( "$CONTAINER_ENGINE" build --no-cache -t "$SLOPTRAP_IMAGE_NAME" -f "$SLOPTRAP_DOCKERFILE_PATH" --label "$SLOPTRAP_IMAGE_LABEL" --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" --build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME" --build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID" --build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID" ) if [[ -n $extra_packages_arg ]]; then cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg") fi cmd+=("$SLOPTRAP_BUILD_CONTEXT") if $DRY_RUN; then run_or_print "${cmd[@]}" return fi local build_output if ! build_output=$("${cmd[@]}"); then return 1 fi build_output=$(trim "$build_output") if [[ -n $build_output ]]; then comment_line "Image %s\n" "$build_output" fi } build_if_missing() { if $DRY_RUN; then print_command "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" return 0 fi if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then return 0 fi build_image } stop_container() { if $DRY_RUN; then print_command "$CONTAINER_ENGINE" stop "$SLOPTRAP_CONTAINER_NAME" return 0 fi "$CONTAINER_ENGINE" stop "$SLOPTRAP_CONTAINER_NAME" >/dev/null 2>&1 || true } clean_environment() { if ! $DRY_RUN; then status_line "Cleaning %s\n" "$PROJECT_NAME" fi local helper_root="$CODE_DIR/.sloptrap-ignores" if [[ -L $helper_root ]]; then error "$helper_root: helper directory may not be a symlink" fi assert_path_within_code_dir "$helper_root" stop_container if $DRY_RUN; then print_command "$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME" print_command "$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME" print_command rm -rf "$helper_root" return 0 fi "$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME" >/dev/null 2>&1 || true "$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1 || true rm -rf "$helper_root" } prune_sloptrap_images() { if ! $DRY_RUN; then status_line "Pruning unused sloptrap images\n" fi local -a cmd=( "$CONTAINER_ENGINE" image prune --force --all --filter "label=$SLOPTRAP_IMAGE_LABEL" ) run_or_print "${cmd[@]}" } run_codex_command() { local -a extra_args=("$@") local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME") if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then cmd+=("${CODEX_ARGS_ARRAY[@]}") fi if [[ ${#extra_args[@]} -gt 0 ]]; then cmd+=("${extra_args[@]}") fi run_or_print "${cmd[@]}" } run_codex() { if ! $DRY_RUN; then status_line "Running %s\n" "$SLOPTRAP_IMAGE_NAME" fi run_codex_command } run_login_target() { if ! $DRY_RUN; then status_line "Login %s\n" "$SLOPTRAP_IMAGE_NAME" fi local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" login) run_or_print "${cmd[@]}" } run_shell_target() { if ! $DRY_RUN; then status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME" fi local -a cmd=("${BASE_CONTAINER_CMD[@]}" --entrypoint /bin/bash "$SLOPTRAP_IMAGE_NAME") run_or_print "${cmd[@]}" } run_resume_target() { local session_id=$1 if ! $DRY_RUN; then status_line "Resume %s (%s)\n" "$SLOPTRAP_IMAGE_NAME" "$session_id" fi run_codex_command resume "$session_id" } process_resume_target() { local session_id=$1 if [[ -z $session_id ]]; then error "target 'resume' requires a session identifier" fi build_if_missing run_resume_target "$session_id" } dispatch_target() { local target=$1 case "$target" in build) build_image ;; rebuild) rebuild_image ;; build-if-missing) build_if_missing ;; run) build_if_missing run_codex ;; login) build_if_missing ensure_codex_home_dir run_login_target ;; shell) build_if_missing run_shell_target ;; stop) stop_container ;; clean) clean_environment ;; prune) prune_sloptrap_images ;; *) error "unknown target '$target'" ;; esac } DRY_RUN=false PRINT_CONFIG=false while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true shift ;; --print-config) PRINT_CONFIG=true shift ;; -h|--help) usage exit 0 ;; --) shift break ;; -*) usage >&2 error "unknown flag '$1'" ;; *) break ;; esac done if [[ $# -lt 1 ]]; then usage >&2 exit 1 fi CODE_DIR_INPUT=$1 shift if [[ ! -d $CODE_DIR_INPUT ]]; then error "code directory '$CODE_DIR_INPUT' does not exist" fi 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" if [[ -f $MANIFEST_PATH ]]; then MANIFEST_PRESENT=true parse_manifest "$MANIFEST_PATH" fi PROJECT_NAME=${MANIFEST[name]-$(basename "$CODE_DIR")} if [[ -z $PROJECT_NAME ]]; then error "$MANIFEST_PATH: project name resolved to empty string" fi if [[ ! $PROJECT_NAME =~ $VALID_NAME_REGEX ]]; then error "$MANIFEST_PATH: invalid project name '$PROJECT_NAME' (allowed: letters, digits, ., _, -)" fi select_codex_home "$CODE_DIR" ensure_ignore_helper_root IGNORE_STUB_BASE="$IGNORE_HELPER_ROOT/session-${BASHPID:-$$}" resolve_sloptrap_ignore "$CODE_DIR" resolve_container_workdir NEED_LOGIN=false if [[ $CODEX_HOME_BOOTSTRAP == true ]]; then NEED_LOGIN=true elif [[ ! -f "$CODEX_HOME_HOST/auth.json" ]]; then NEED_LOGIN=true fi TARGETS=("$@") if [[ ${#TARGETS[@]} -eq 0 ]]; then if [[ -n ${MANIFEST[default_targets]-} ]]; then read -r -a TARGETS <<< "${MANIFEST[default_targets]}" elif [[ -n ${MANIFEST[default_target]-} ]]; then read -r -a TARGETS <<< "${MANIFEST[default_target]}" else TARGETS=("run") fi fi DEFAULT_TARGETS=("${TARGETS[@]}") PACKAGES_EXTRA=${MANIFEST[packages_extra]-} if [[ -n ${MANIFEST[allow_host_network]-} ]]; then case "${MANIFEST[allow_host_network],,}" in 1|true|yes) ALLOW_HOST_NETWORK=true ;; 0|false|no) ALLOW_HOST_NETWORK=false ;; *) error "$MANIFEST_PATH: allow_host_network must be true or false (got '${MANIFEST[allow_host_network]}')" ;; esac fi forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough) for forbidden_key in "${forbidden_keys[@]}"; do if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons" fi done if [[ -n $PACKAGES_EXTRA ]]; then ensure_safe_for_make "packages_extra" "$PACKAGES_EXTRA" validate_package_list "packages_extra" "$PACKAGES_EXTRA" fi CONTAINER_ENGINE="$(detect_container_engine)" CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}") if [[ -n ${MANIFEST[codex_args]-} ]]; then ensure_safe_for_make "codex_args" "${MANIFEST[codex_args]}" declare -a manifest_codex_args=() read -r -a manifest_codex_args <<< "${MANIFEST[codex_args]}" CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}") unset -v manifest_codex_args fi declare -a sanitized_codex_args=() declare -a sandbox_pair=() codex_args_index=0 while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then if (( codex_args_index + 1 >= ${#CODEX_ARGS_ARRAY[@]} )); then error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)" fi sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}") ((codex_args_index+=2)) continue fi sanitized_codex_args+=("${CODEX_ARGS_ARRAY[$codex_args_index]}") ((codex_args_index+=1)) done if [[ ${#sandbox_pair[@]} -gt 0 ]]; then sanitized_codex_args+=("${sandbox_pair[@]}") fi CODEX_ARGS_ARRAY=("${sanitized_codex_args[@]}") unset -v sanitized_codex_args sandbox_pair codex_args_index ensure_safe_sandbox "${CODEX_ARGS_ARRAY[@]}" if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then CODEX_ARGS_DISPLAY=$(printf '%s ' "${CODEX_ARGS_ARRAY[@]}") CODEX_ARGS_DISPLAY=${CODEX_ARGS_DISPLAY% } else CODEX_ARGS_DISPLAY="" fi prepare_ignore_mounts "$CODE_DIR" prepare_container_runtime if $PRINT_CONFIG; then print_config "$MANIFEST_PATH" exit 0 fi AUTO_LOGIN=false if [[ $NEED_LOGIN == true ]]; then AUTO_LOGIN=true for tgt in "${TARGETS[@]}"; do if [[ $tgt == "login" ]]; then AUTO_LOGIN=false break fi done fi if $AUTO_LOGIN; then if ! $DRY_RUN; then status_line "Codex login required (%s)\n" "$CODEX_HOME_HOST" fi ensure_codex_home_dir dispatch_target login fi target_index=0 while (( target_index < ${#TARGETS[@]} )); do current_target="${TARGETS[$target_index]}" if [[ $current_target == "resume" ]]; then if (( target_index + 1 >= ${#TARGETS[@]} )); then error "target 'resume' requires a session identifier" fi process_resume_target "${TARGETS[$((target_index + 1))]}" ((target_index+=2)) continue fi dispatch_target "$current_target" ((target_index+=1)) done