#!/usr/bin/env bash # sloptrap if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@" fi 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 danger-full-access --ask-for-approval never) DEFAULT_CODEX_ARGS_DISPLAY=$(printf '%s ' "${DEFAULT_CODEX_ARGS[@]}") DEFAULT_CODEX_ARGS_DISPLAY=${DEFAULT_CODEX_ARGS_DISPLAY% } SLOPTRAP_IMAGE_LABEL_KEY="net.sk4nz.sloptrap.managed" SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1" SLOPTRAP_SUPPORTED_CAPABILITIES=(apt-install packet-capture nested-podman) 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 " --trust-capabilities Trust the manifest's requested capabilities for this build\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 " capabilities=apt-install packet-capture\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 " wizard Create or update %s interactively\n" "$MANIFEST_BASENAME" 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=() declare -a CODEX_ARGS_ARRAY=() declare -a DEFAULT_TARGETS=() MANIFEST_PRESENT=false CURRENT_IGNORE_FILE="" CONTAINER_ENGINE="" CODEX_ROOT_HOST="" CODEX_STATE_HOME_HOST="" CODEX_AUTH_FILE_HOST="" CODEX_STATE_KEY="" CODEX_HOME_BOOTSTRAP=false NEED_LOGIN=false REQUESTED_CAPABILITIES="" ENABLED_CAPABILITIES="" CAPABILITY_MANIFEST_DIGEST="" CAPABILITY_TRUST_ROOT_HOST="" CAPABILITY_TRUST_FILE_HOST="" CAPABILITY_BUILD_STAMP_HOST="" CAPABILITY_STATE_HOST="" 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 util-linux" ARG EXTRA_PACKAGES="" ARG CAPABILITY_PACKAGES="" RUN apt-get update \ && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} ${CAPABILITY_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 COPY sloptrap-entrypoint /usr/local/bin/sloptrap-entrypoint COPY sloptrap-helperd /usr/local/bin/sloptrap-helperd COPY slop-apt /usr/local/bin/slop-apt COPY slopcap /usr/local/bin/slopcap COPY sloppodman /usr/local/bin/sloppodman RUN chmod 0755 /usr/local/bin/sloptrap-entrypoint /usr/local/bin/sloptrap-helperd \ /usr/local/bin/slop-apt /usr/local/bin/slopcap /usr/local/bin/sloppodman \ && chown -R sloptrap:sloptrap /home/sloptrap WORKDIR /workspace ENV SHELL=/bin/bash HOME=/home/sloptrap ENTRYPOINT ["/usr/local/bin/sloptrap-entrypoint"] EOF } write_embedded_helper() { local helper=$1 case "$helper" in sloptrap-entrypoint) cat <<'EOF' #!/usr/bin/env bash set -euo pipefail helper_pid="" cleanup() { if [[ -n $helper_pid ]]; then kill "$helper_pid" >/dev/null 2>&1 || true wait "$helper_pid" >/dev/null 2>&1 || true fi } trap cleanup EXIT INT TERM HUP if [[ $# -eq 0 ]]; then set -- codex fi if [[ $(id -u) -eq 0 ]]; then helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" mkdir -p "$queue_dir" chmod 711 "$helper_dir" chmod 700 "$queue_dir" target_uid=${SLOPTRAP_HOST_UID:-} target_gid=${SLOPTRAP_HOST_GID:-} if [[ -n $target_uid && -n $target_gid ]]; then chown "$target_uid:$target_gid" "$queue_dir" fi if [[ -n ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then /usr/local/bin/sloptrap-helperd & helper_pid=$! fi if [[ -n $target_uid && -n $target_gid ]]; then exec setpriv --reuid "$target_uid" --regid "$target_gid" --clear-groups -- "$@" fi exec runuser -u sloptrap --preserve-environment -- "$@" fi exec "$@" EOF ;; sloptrap-helperd) cat <<'EOF' #!/usr/bin/env bash set -euo pipefail umask 077 helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-} audit_log=${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log} capture_root=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures} workspace_root=${SLOPTRAP_WORKDIR:-/workspace} pidfile="$helper_dir/helperd.pid" mkdir -p "$queue_dir" "$(dirname "$audit_log")" cleanup_pidfile() { rm -f "$pidfile" } trap cleanup_pidfile EXIT INT TERM HUP printf '%s\n' "$$" >"$pidfile" chmod 644 "$pidfile" 2>/dev/null || true has_capability() { local needle=$1 local token for token in $caps; do if [[ $token == "$needle" ]]; then return 0 fi done return 1 } log_action() { local op=$1 local details=$2 local status=$3 printf '%s op=%s status=%s %s\n' "$(date -u +%FT%TZ)" "$op" "$status" "$details" >>"$audit_log" } resolve_path() { local raw=$1 if command -v realpath >/dev/null 2>&1; then realpath -m "$raw" return fi case "$raw" in /*) printf '%s\n' "$raw" ;; *) printf '%s/%s\n' "$(pwd -P)" "$raw" ;; esac } path_within_root() { local root path root=$(resolve_path "$1") path=$(resolve_path "$2") case "$path" in "$root"|"${root}/"*) return 0 ;; esac return 1 } claim_request_dir() { local request_dir=$1 [[ -d $request_dir && ! -L $request_dir ]] || return 1 path_within_root "$queue_dir" "$request_dir" || return 1 chown root:root "$request_dir" 2>/dev/null || true chmod 700 "$request_dir" 2>/dev/null || true } init_request_outputs() { local request_dir=$1 local path [[ ! -e "$request_dir/status" && ! -L "$request_dir/status" ]] || return 1 for path in "$request_dir/stdout" "$request_dir/stderr"; do [[ ! -L $path ]] || return 1 rm -f "$path" : >"$path" chmod 600 "$path" 2>/dev/null || true done } read_request_value() { local path=$1 [[ -f $path && ! -L $path ]] || return 1 cat "$path" } validate_package_name() { local package=$1 [[ $package =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*$ ]] } validate_packet_interface() { local iface=$1 [[ $iface =~ ^[A-Za-z0-9][A-Za-z0-9_.:@+-]*$ ]] } validate_capture_path() { local path=$1 local resolved resolved=$(resolve_path "$path") path_within_root "$capture_root" "$resolved" && return 0 path_within_root "$workspace_root" "$resolved" } write_status() { local request_dir=$1 local status=$2 printf '%s\n' "$status" >"$request_dir/status" } run_apt_install() { local request_dir=$1 has_capability "apt-install" || { printf 'capability apt-install is not active\n' >"$request_dir/stderr" write_status "$request_dir" 126 log_action "apt-install" "packages=denied" 126 return } local packages_file="$request_dir/packages" if [[ ! -f $packages_file || -L $packages_file ]]; then printf 'missing package list\n' >"$request_dir/stderr" write_status "$request_dir" 2 log_action "apt-install" "packages=missing" 2 return fi mapfile -t packages <"$packages_file" if [[ ${#packages[@]} -eq 0 ]]; then printf 'package list is empty\n' >"$request_dir/stderr" write_status "$request_dir" 2 log_action "apt-install" "packages=empty" 2 return fi local package for package in "${packages[@]}"; do if ! validate_package_name "$package"; then printf 'invalid package name %s\n' "$package" >"$request_dir/stderr" write_status "$request_dir" 2 log_action "apt-install" "packages=invalid" 2 return fi done if apt-get update >"$request_dir/stdout" 2>"$request_dir/stderr" \ && apt-get install -y --no-install-recommends "${packages[@]}" >>"$request_dir/stdout" 2>>"$request_dir/stderr"; then write_status "$request_dir" 0 log_action "apt-install" "packages=${packages[*]}" 0 return fi write_status "$request_dir" 1 log_action "apt-install" "packages=${packages[*]}" 1 } run_packet_capture() { local request_dir=$1 has_capability "packet-capture" || { printf 'capability packet-capture is not active\n' >"$request_dir/stderr" write_status "$request_dir" 126 log_action "packet-capture" "interface=denied" 126 return } local iface_file="$request_dir/interface" [[ -f $iface_file ]] || { printf 'missing interface\n' >"$request_dir/stderr" write_status "$request_dir" 2 log_action "packet-capture" "interface=missing" 2 return } local iface filter_file output_file stdout_mode iface=$(read_request_value "$iface_file" || true) filter_file="$request_dir/filter" output_file="$request_dir/output" stdout_mode=0 [[ -f "$request_dir/stdout_mode" ]] && stdout_mode=$(read_request_value "$request_dir/stdout_mode" || true) if ! validate_packet_interface "$iface"; then printf 'invalid interface %s\n' "$iface" >"$request_dir/stderr" write_status "$request_dir" 2 log_action "packet-capture" "interface=invalid" 2 return fi if [[ $stdout_mode != "0" && $stdout_mode != "1" ]]; then printf 'invalid stdout mode %s\n' "$stdout_mode" >"$request_dir/stderr" write_status "$request_dir" 2 log_action "packet-capture" "interface=$iface stdout=invalid" 2 return fi local -a cmd=(tcpdump -i "$iface") if [[ -s $output_file ]]; then local capture_path capture_path=$(read_request_value "$output_file" || true) if ! validate_capture_path "$capture_path"; then printf 'output path must stay within %s or %s\n' "$capture_root" "$workspace_root" >"$request_dir/stderr" write_status "$request_dir" 2 log_action "packet-capture" "interface=$iface output=invalid" 2 return fi mkdir -p "$(dirname "$capture_path")" cmd+=(-w "$capture_path") fi if [[ -s $filter_file ]]; then local filter filter=$(read_request_value "$filter_file" || true) local -a filter_tokens=() if [[ -n $filter ]]; then read -r -a filter_tokens <<< "$filter" fi if [[ ${#filter_tokens[@]} -gt 0 ]]; then cmd+=(-- "${filter_tokens[@]}") fi fi if [[ $stdout_mode == "1" ]]; then "${cmd[@]}" >"$request_dir/stdout" 2>"$request_dir/stderr" || { write_status "$request_dir" 1 log_action "packet-capture" "interface=$iface stdout=1" 1 return } else "${cmd[@]}" >"$request_dir/stdout" 2>"$request_dir/stderr" || { write_status "$request_dir" 1 log_action "packet-capture" "interface=$iface stdout=0" 1 return } fi write_status "$request_dir" 0 log_action "packet-capture" "interface=$iface stdout=$stdout_mode" 0 } while true; do shopt -s nullglob request_dirs=("$queue_dir"/*.req) shopt -u nullglob pending_requests=0 if [[ ${#request_dirs[@]} -eq 0 ]]; then sleep 1 continue fi for request_dir in "${request_dirs[@]}"; do [[ -d $request_dir ]] || continue [[ ! -f "$request_dir/status" ]] || continue pending_requests=1 if ! claim_request_dir "$request_dir"; then log_action "request" "unsafe=1 path=$request_dir" 2 continue fi if ! init_request_outputs "$request_dir"; then log_action "request" "unsafe-output=1 path=$request_dir" 2 continue fi if ! op=$(read_request_value "$request_dir/op"); then printf 'missing operation\n' >"$request_dir/stderr" write_status "$request_dir" 2 log_action "request" "op=missing" 2 continue fi case "$op" in apt-install) run_apt_install "$request_dir" ;; packet-capture) run_packet_capture "$request_dir" ;; *) printf 'unknown operation %s\n' "$op" >"$request_dir/stderr" write_status "$request_dir" 2 log_action "$op" "unknown=1" 2 ;; esac done if [[ $pending_requests -eq 0 ]]; then sleep 1 fi done EOF ;; slop-apt) cat <<'EOF' #!/usr/bin/env bash set -euo pipefail helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" pidfile="$helper_dir/helperd.pid" helper_running() { local pid [[ -r $pidfile ]] || return 1 pid=$(<"$pidfile") [[ -n $pid ]] || return 1 kill -0 "$pid" 2>/dev/null } ensure_helper_ready() { if [[ -w $queue_dir ]] && helper_running; then return 0 fi if [[ -z ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then printf 'slop-apt: capability helper is not available in this session\n' >&2 exit 1 fi if ! command -v setpriv >/dev/null 2>&1; then printf 'slop-apt: setpriv is required to bootstrap the capability helper\n' >&2 exit 1 fi setpriv --reuid 0 --regid 0 --clear-groups -- env \ SLOPTRAP_HELPER_DIR="$helper_dir" \ SLOPTRAP_ACTIVE_CAPABILITIES="${SLOPTRAP_ACTIVE_CAPABILITIES:-}" \ SLOPTRAP_AUDIT_LOG="${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}" \ SLOPTRAP_CAPTURE_DIR="${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}" \ SLOPTRAP_WORKDIR="${SLOPTRAP_WORKDIR:-/workspace}" \ SLOPTRAP_HOST_UID="${SLOPTRAP_HOST_UID:-$(id -u)}" \ SLOPTRAP_HOST_GID="${SLOPTRAP_HOST_GID:-$(id -g)}" \ bash -c ' set -euo pipefail helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" pidfile="$helper_dir/helperd.pid" helper_bin=$(command -v sloptrap-helperd) [[ -n $helper_bin ]] || exit 1 mkdir -p "$queue_dir" chmod 711 "$helper_dir" chmod 700 "$queue_dir" chown "$SLOPTRAP_HOST_UID:$SLOPTRAP_HOST_GID" "$queue_dir" if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then exit 0 fi fi "$helper_bin" >/dev/null 2>&1 & for ((i=0; i<30; i+=1)); do if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then exit 0 fi fi sleep 0.1 done exit 1 ' } if [[ ${1-} != "install" ]]; then printf 'usage: slop-apt install \n' >&2 exit 2 fi shift if [[ $# -eq 0 ]]; then printf 'slop-apt: at least one package is required\n' >&2 exit 2 fi for package in "$@"; do if [[ ! $package =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*$ ]]; then printf 'slop-apt: invalid package name %s\n' "$package" >&2 exit 2 fi done ensure_helper_ready request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req") trap 'rm -rf "$request_dir"' EXIT INT TERM HUP printf 'apt-install\n' >"$request_dir/op" printf '%s\n' "$@" >"$request_dir/packages" while [[ ! -f "$request_dir/status" ]]; do sleep 1 done if [[ -s "$request_dir/stdout" ]]; then cat "$request_dir/stdout" fi if [[ -s "$request_dir/stderr" ]]; then cat "$request_dir/stderr" >&2 fi status=$(<"$request_dir/status") exit "$status" EOF ;; slopcap) cat <<'EOF' #!/usr/bin/env bash set -euo pipefail helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" default_output=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures} workspace_root=${SLOPTRAP_WORKDIR:-/workspace} pidfile="$helper_dir/helperd.pid" mkdir -p "$default_output" helper_running() { local pid [[ -r $pidfile ]] || return 1 pid=$(<"$pidfile") [[ -n $pid ]] || return 1 kill -0 "$pid" 2>/dev/null } ensure_helper_ready() { if [[ -w $queue_dir ]] && helper_running; then return 0 fi if [[ -z ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then printf 'slopcap: capability helper is not available in this session\n' >&2 exit 1 fi if ! command -v setpriv >/dev/null 2>&1; then printf 'slopcap: setpriv is required to bootstrap the capability helper\n' >&2 exit 1 fi setpriv --reuid 0 --regid 0 --clear-groups -- env \ SLOPTRAP_HELPER_DIR="$helper_dir" \ SLOPTRAP_ACTIVE_CAPABILITIES="${SLOPTRAP_ACTIVE_CAPABILITIES:-}" \ SLOPTRAP_AUDIT_LOG="${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}" \ SLOPTRAP_CAPTURE_DIR="${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}" \ SLOPTRAP_WORKDIR="${SLOPTRAP_WORKDIR:-/workspace}" \ SLOPTRAP_HOST_UID="${SLOPTRAP_HOST_UID:-$(id -u)}" \ SLOPTRAP_HOST_GID="${SLOPTRAP_HOST_GID:-$(id -g)}" \ bash -c ' set -euo pipefail helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" pidfile="$helper_dir/helperd.pid" helper_bin=$(command -v sloptrap-helperd) [[ -n $helper_bin ]] || exit 1 mkdir -p "$queue_dir" chmod 711 "$helper_dir" chmod 700 "$queue_dir" chown "$SLOPTRAP_HOST_UID:$SLOPTRAP_HOST_GID" "$queue_dir" if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then exit 0 fi fi "$helper_bin" >/dev/null 2>&1 & for ((i=0; i<30; i+=1)); do if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then exit 0 fi fi sleep 0.1 done exit 1 ' } resolve_requested_path() { local raw=$1 if command -v realpath >/dev/null 2>&1; then realpath -m "$raw" return fi case "$raw" in /*) printf '%s\n' "$raw" ;; *) printf '%s/%s\n' "$(pwd -P)" "$raw" ;; esac } validate_output_path() { local path=$1 local resolved capture_root workspace_path resolved=$(resolve_requested_path "$path") capture_root=$(resolve_requested_path "$default_output") workspace_path=$(resolve_requested_path "$workspace_root") case "$resolved" in "$capture_root"|"${capture_root}/"*|"$workspace_path"|"${workspace_path}/"*) return 0 ;; esac return 1 } if [[ ${1-} != "capture" ]]; then printf 'usage: slopcap capture --interface [--filter ] [--output ] [--stdout]\n' >&2 exit 2 fi shift iface="" filter="" output="" stdout_mode=0 while [[ $# -gt 0 ]]; do case "$1" in --interface) shift [[ $# -gt 0 ]] || { printf 'slopcap: --interface requires a value\n' >&2; exit 2; } iface=$1 ;; --filter) shift [[ $# -gt 0 ]] || { printf 'slopcap: --filter requires a value\n' >&2; exit 2; } filter=$1 ;; --output) shift [[ $# -gt 0 ]] || { printf 'slopcap: --output requires a value\n' >&2; exit 2; } output=$1 ;; --stdout) stdout_mode=1 ;; *) printf 'slopcap: unsupported argument %s\n' "$1" >&2 exit 2 ;; esac shift done [[ -n $iface ]] || { printf 'slopcap: --interface is required\n' >&2; exit 2; } if [[ -z $output && $stdout_mode -eq 0 ]]; then output="$default_output/capture-$(date +%s).pcap" fi if [[ -n $output ]] && ! validate_output_path "$output"; then printf 'slopcap: output path must stay within %s or %s\n' "$default_output" "$workspace_root" >&2 exit 2 fi ensure_helper_ready request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req") trap 'rm -rf "$request_dir"' EXIT INT TERM HUP printf 'packet-capture\n' >"$request_dir/op" printf '%s\n' "$iface" >"$request_dir/interface" printf '%s\n' "$filter" >"$request_dir/filter" printf '%s\n' "$output" >"$request_dir/output" printf '%s\n' "$stdout_mode" >"$request_dir/stdout_mode" stream_pid="" if [[ $stdout_mode -eq 1 ]]; then touch "$request_dir/stdout" tail -f "$request_dir/stdout" & stream_pid=$! fi while [[ ! -f "$request_dir/status" ]]; do sleep 1 done if [[ -n $stream_pid ]]; then kill "$stream_pid" >/dev/null 2>&1 || true wait "$stream_pid" >/dev/null 2>&1 || true fi if [[ $stdout_mode -eq 0 && -s "$request_dir/stdout" ]]; then cat "$request_dir/stdout" fi if [[ -s "$request_dir/stderr" ]]; then cat "$request_dir/stderr" >&2 fi status=$(<"$request_dir/status") exit "$status" EOF ;; sloppodman) cat <<'EOF' #!/usr/bin/env bash set -euo pipefail caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-} has_capability() { local needle=$1 local token for token in $caps; do if [[ $token == "$needle" ]]; then return 0 fi done return 1 } if [[ $# -eq 0 ]]; then printf 'usage: sloppodman ...\n' >&2 exit 2 fi subcommand=$1 shift case "$subcommand" in pull|build|tag|run|ps|logs|stop|rm|inspect) ;; *) printf 'sloppodman: unsupported subcommand %s\n' "$subcommand" >&2 exit 2 ;; esac has_capability "nested-podman" || { printf 'sloppodman: capability nested-podman is not active\n' >&2 exit 126 } workspace_root=${SLOPTRAP_WORKDIR:-/workspace} podman_root=${SLOPTRAP_INNER_PODMAN_ROOT:-/codex/capabilities/podman/storage} podman_runroot=${SLOPTRAP_INNER_PODMAN_RUNROOT:-/codex/capabilities/podman/run} runtime_dir=${XDG_RUNTIME_DIR:-/codex/capabilities/podman/runtime} mkdir -p "$podman_root" "$podman_runroot" "$runtime_dir" resolve_inner_path() { local raw=$1 if command -v realpath >/dev/null 2>&1; then realpath -m "$raw" return fi case "$raw" in /*) printf '%s\n' "$raw" ;; *) printf '%s/%s\n' "$(pwd -P)" "$raw" ;; esac } validate_workspace_path() { local path=$1 path=$(resolve_inner_path "$path") case "$path" in "$workspace_root"|"${workspace_root}/"*) ;; *) printf 'sloppodman: path must stay within %s (%s)\n' "$workspace_root" "$path" >&2 exit 2 ;; esac } reject_flag() { local flag=$1 printf 'sloppodman: %s is not permitted\n' "$flag" >&2 exit 2 } validate_volume_spec() { local spec=$1 local source if [[ $spec != *:* ]]; then return 0 fi source=${spec%%:*} case "$source" in "" ) return 0 ;; .|..|/*|./*|../*|*/*) validate_workspace_path "$source" ;; * ) return 0 ;; esac } validate_mount_spec() { local spec=$1 local type="" local source="" local part IFS=',' read -r -a mount_parts <<< "$spec" for part in "${mount_parts[@]}"; do case "$part" in type=*) type=${part#type=} ;; source=*|src=*) source=${part#*=} ;; esac done case "$type" in volume|tmpfs) return 0 ;; bind) [[ -n $source ]] || reject_flag "--mount" validate_workspace_path "$source" return 0 ;; *) reject_flag "--mount" ;; esac } if [[ $subcommand == "build" ]]; then args=("$@") context="" idx=0 while (( idx < ${#args[@]} )); do arg=${args[$idx]} case "$arg" in -f|--file) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: %s requires a path\n' "$arg" >&2; exit 2; } validate_workspace_path "${args[$idx]}" ;; --file=*) validate_workspace_path "${arg#*=}" ;; --network) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; } if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then printf 'sloppodman: host networking is not available in this session\n' >&2 exit 2 fi ;; --network=*) if [[ ${arg#*=} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then printf 'sloppodman: host networking is not available in this session\n' >&2 exit 2 fi ;; -v|--volume|--volume=*|--mount|--mount=*|--security-opt|--security-opt=*|--cap-add|--cap-add=*|--cap-drop|--cap-drop=*|--device|--device=*|--privileged|--privileged=*|--rootfs|--rootfs=*) reject_flag "${arg%%=*}" ;; esac ((idx+=1)) done if [[ ${#args[@]} -gt 0 ]]; then context=${args[$(( ${#args[@]} - 1 ))]} validate_workspace_path "$context" fi fi if [[ $subcommand == "run" ]]; then args=("$@") idx=0 while (( idx < ${#args[@]} )); do arg=${args[$idx]} case "$arg" in --) break ;; --network) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; } if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then printf 'sloppodman: host networking is not available in this session\n' >&2 exit 2 fi ;; --network=*) if [[ ${arg#*=} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then printf 'sloppodman: host networking is not available in this session\n' >&2 exit 2 fi ;; -v|--volume) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: %s requires a value\n' "$arg" >&2; exit 2; } validate_volume_spec "${args[$idx]}" ;; --volume=*) validate_volume_spec "${arg#*=}" ;; --mount) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: --mount requires a value\n' >&2; exit 2; } validate_mount_spec "${args[$idx]}" ;; --mount=*) validate_mount_spec "${arg#*=}" ;; --privileged|--privileged=*|--cap-add|--cap-add=*|--cap-drop|--cap-drop=*|--device|--device=*|--security-opt|--security-opt=*|--rootfs|--rootfs=*|--pid|--pid=*|--ipc|--ipc=*|--uts|--uts=*|--userns|--userns=*|--cgroupns|--cgroupns=*) reject_flag "${arg%%=*}" ;; -*) ;; *) break ;; esac ((idx+=1)) done fi exec podman --root "$podman_root" --runroot "$podman_runroot" "$subcommand" "$@" EOF ;; *) error "unknown embedded helper '$helper'" ;; esac } populate_embedded_helper() { local helper=$1 local destination=$2 mkdir -p "$(dirname "$destination")" write_embedded_helper "$helper" >"$destination" } 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" local helper for helper in sloptrap-entrypoint sloptrap-helperd slop-apt slopcap sloppodman; do populate_embedded_helper "$helper" "$SLOPTRAP_BUILD_CONTEXT/$helper" done } 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_ROOT_HOST="$preferred" if [[ -d $CODEX_ROOT_HOST ]]; then CODEX_ROOT_HOST="$(cd "$CODEX_ROOT_HOST" && pwd -P)" CODEX_HOME_BOOTSTRAP=false else CODEX_HOME_BOOTSTRAP=true fi CODEX_STATE_KEY=$(printf '%s' "$CODE_DIR" | sha256sum) CODEX_STATE_KEY=${CODEX_STATE_KEY%% *} CODEX_STATE_HOME_HOST="$CODEX_ROOT_HOST/sloptrap/state/$CODEX_STATE_KEY" CODEX_AUTH_FILE_HOST="$CODEX_ROOT_HOST/auth.json" if [[ -L $CODEX_AUTH_FILE_HOST ]]; then error "Codex auth file '$CODEX_AUTH_FILE_HOST' must not be a symlink" fi if [[ -e $CODEX_AUTH_FILE_HOST && ! -f $CODEX_AUTH_FILE_HOST ]]; then error "expected Codex auth file '$CODEX_AUTH_FILE_HOST' to be a regular file" fi } compute_manifest_digest() { if [[ -f $MANIFEST_PATH ]]; then local digest digest=$(sha256sum "$MANIFEST_PATH") printf '%s' "${digest%% *}" return 0 fi printf 'no-manifest' } select_capability_state_paths() { local capability_root="$CODEX_ROOT_HOST/sloptrap/capabilities" CAPABILITY_TRUST_ROOT_HOST="$capability_root/trust" CAPABILITY_TRUST_FILE_HOST="$CAPABILITY_TRUST_ROOT_HOST/$CODEX_STATE_KEY.trust" CAPABILITY_BUILD_STAMP_HOST="$capability_root/builds/$CODEX_STATE_KEY.stamp" CAPABILITY_STATE_HOST="$CODEX_STATE_HOME_HOST/capabilities" } ensure_capability_state_paths() { local capability_root="$CODEX_ROOT_HOST/sloptrap/capabilities" ensure_codex_directory "$capability_root" "sloptrap capability namespace" ensure_codex_directory "$CAPABILITY_TRUST_ROOT_HOST" "sloptrap capability trust root" ensure_codex_directory "$(dirname "$CAPABILITY_BUILD_STAMP_HOST")" "sloptrap capability build stamp root" ensure_codex_directory "$CAPABILITY_STATE_HOST" "project capability state" if capability_list_contains "$REQUESTED_CAPABILITIES" "nested-podman"; then ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-storage" "nested podman storage state" ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-run" "nested podman runroot state" ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-runtime" "nested podman runtime state" fi } capability_trust_matches_current() { [[ -f $CAPABILITY_TRUST_FILE_HOST ]] || return 1 local trusted_digest trusted_caps trusted_digest=$(sed -n '1p' "$CAPABILITY_TRUST_FILE_HOST" 2>/dev/null || true) trusted_caps=$(sed -n '2p' "$CAPABILITY_TRUST_FILE_HOST" 2>/dev/null || true) [[ $trusted_digest == "$CAPABILITY_MANIFEST_DIGEST" && $trusted_caps == "$REQUESTED_CAPABILITIES" ]] } record_capability_trust() { ensure_capability_state_paths if $DRY_RUN; then print_command mkdir -p "$(dirname "$CAPABILITY_TRUST_FILE_HOST")" print_command sh -c "printf '%s\\n%s\\n' '$CAPABILITY_MANIFEST_DIGEST' '$REQUESTED_CAPABILITIES' > '$CAPABILITY_TRUST_FILE_HOST'" return 0 fi printf '%s\n%s\n' "$CAPABILITY_MANIFEST_DIGEST" "$REQUESTED_CAPABILITIES" >"$CAPABILITY_TRUST_FILE_HOST" } prompt_capability_trust() { local tty_path="/dev/tty" info_line "Manifest requests privileged capabilities: %s\n" "$REQUESTED_CAPABILITIES" printf '%s' "$PREFIX_TEXT" >"$tty_path" printf '%b' "$COLOR_TEXT" >"$tty_path" printf 'Trust these capabilities for this project build? [y/N]: ' >"$tty_path" printf '%b' "$RESET" >"$tty_path" local input if ! IFS= read -r input <"$tty_path"; then error "capability trust requires an interactive terminal or --trust-capabilities" fi case "${input,,}" in y|yes) record_capability_trust ;; *) error "capability trust not granted" ;; esac } ensure_capability_trust() { [[ -n $REQUESTED_CAPABILITIES ]] || return 0 capability_trust_matches_current && return 0 if $TRUST_CAPABILITIES; then record_capability_trust return 0 fi if [[ ! -t 0 ]]; then error "requested capabilities require prior trust or --trust-capabilities" fi prompt_capability_trust } write_capability_build_stamp() { ensure_capability_state_paths if $DRY_RUN; then print_command sh -c "printf '%s\\n%s\\n' '$CAPABILITY_MANIFEST_DIGEST' '$REQUESTED_CAPABILITIES' > '$CAPABILITY_BUILD_STAMP_HOST'" return 0 fi printf '%s\n%s\n' "$CAPABILITY_MANIFEST_DIGEST" "$REQUESTED_CAPABILITIES" >"$CAPABILITY_BUILD_STAMP_HOST" } capability_build_stamp_matches_current() { [[ -f $CAPABILITY_BUILD_STAMP_HOST ]] || return 1 local stamp_digest stamp_caps stamp_digest=$(sed -n '1p' "$CAPABILITY_BUILD_STAMP_HOST" 2>/dev/null || true) stamp_caps=$(sed -n '2p' "$CAPABILITY_BUILD_STAMP_HOST" 2>/dev/null || true) [[ $stamp_digest == "$CAPABILITY_MANIFEST_DIGEST" && $stamp_caps == "$REQUESTED_CAPABILITIES" ]] } 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][A-Za-z0-9+.-]*$ ]]; then error "$source: invalid package name '$token' in '$key'" fi done } validate_capability_list() { local key=$1 local raw=$2 local source=${3:-$MANIFEST_PATH} [[ -z $raw ]] && return 0 local token supported capability for token in $raw; do supported=false for capability in "${SLOPTRAP_SUPPORTED_CAPABILITIES[@]}"; do if [[ $token == "$capability" ]]; then supported=true break fi done if [[ $supported != true ]]; then error "$source: invalid capability '$token' in '$key'" fi done } normalize_capability_list() { local raw=$1 [[ -z $raw ]] && return 0 local token for token in $raw; do printf '%s\n' "$token" done | sort -u | tr '\n' ' ' | sed 's/ $//' } capability_list_contains() { local list=$1 local needle=$2 local token for token in $list; do if [[ $token == "$needle" ]]; then return 0 fi done return 1 } 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" } manifest_default_value() { local key=$1 local fallback=$2 if [[ -v MANIFEST["$key"] ]]; then printf '%s' "${MANIFEST[$key]}" else printf '%s' "$fallback" fi } prompt_manifest_value() { local label=$1 local default_value=$2 local input local tty_path="/dev/tty" printf '%s' "$PREFIX_TEXT" >"$tty_path" printf '%b' "$COLOR_TEXT" >"$tty_path" printf '%s [%s]: ' "$label" "$default_value" >"$tty_path" printf '%b' "$RESET" >"$tty_path" if ! IFS= read -r input <"$tty_path"; then error "wizard requires an interactive terminal" fi printf '%s' "$input" } validate_wizard_name() { local value=$1 [[ -n $value ]] || error "$MANIFEST_PATH: name must not be empty" if [[ ! $value =~ $VALID_NAME_REGEX ]]; then error "$MANIFEST_PATH: invalid project name '$value' (allowed: letters, digits, ., _, -)" fi } normalize_wizard_allow_host_network() { local value=${1,,} case "$value" in 1|true|yes) printf 'true' ;; 0|false|no) printf 'false' ;; *) error "$MANIFEST_PATH: allow_host_network must be true or false (got '$1')" ;; esac } run_wizard() { local manifest_path=$1 if [[ -L $manifest_path ]]; then error "$manifest_path: manifest must not be a symlink" fi if [[ ! -t 0 ]]; then error "wizard requires an interactive terminal" fi if [[ ! -f $manifest_path ]]; then print_banner fi local default_name local default_packages_extra local default_capabilities local default_allow_host_network default_name=$(manifest_default_value "name" "$(basename "$CODE_DIR")") default_packages_extra=$(manifest_default_value "packages_extra" "") default_capabilities=$(manifest_default_value "capabilities" "") default_allow_host_network=$(manifest_default_value "allow_host_network" "false") local action="Creating" if [[ -f $manifest_path ]]; then action="Updating" fi info_line "%s %s interactively.\n" "$action" "$MANIFEST_BASENAME" local value while true; do info_line "name: Labels the project, container, and image names.\n" value=$(prompt_manifest_value "name" "$default_name") value=$(trim "$value") [[ -n $value ]] || value=$default_name validate_wizard_name "$value" default_name=$value break done while true; do info_line "packages_extra: Extra Debian packages to install during image build.\n" value=$(prompt_manifest_value "packages_extra" "$default_packages_extra") value=$(trim "$value") [[ -n $value ]] || value=$default_packages_extra if [[ -n $value ]]; then validate_package_list "packages_extra" "$value" "$manifest_path" fi default_packages_extra=$value break done while true; do info_line "capabilities: Optional privileged features (%s).\n" "${SLOPTRAP_SUPPORTED_CAPABILITIES[*]}" value=$(prompt_manifest_value "capabilities" "$default_capabilities") value=$(trim "$value") [[ -n $value ]] || value=$default_capabilities value=$(normalize_capability_list "$value") if [[ -n $value ]]; then validate_capability_list "capabilities" "$value" "$manifest_path" fi default_capabilities=$value break done while true; do info_line "allow_host_network: Use host networking instead of an isolated bridge.\n" value=$(prompt_manifest_value "allow_host_network" "$default_allow_host_network") value=$(trim "$value") [[ -n $value ]] || value=$default_allow_host_network default_allow_host_network=$(normalize_wizard_allow_host_network "$value") break done assert_path_within_code_dir "$manifest_path" cat > "$manifest_path" </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_directory() { local path=$1 local label=$2 if [[ -L $path ]]; then error "$label '$path' must not be a symlink" fi if [[ -e $path && ! -d $path ]]; then error "expected $label '$path' to be a directory" fi if [[ -d $path ]]; then return 0 fi if $DRY_RUN; then print_command mkdir -p "$path" return 0 fi mkdir -p "$path" } ensure_codex_storage_paths() { local state_root="$CODEX_ROOT_HOST/sloptrap" local state_bucket="$state_root/state" ensure_codex_directory "$CODEX_ROOT_HOST" "Codex home" ensure_codex_directory "$state_root" "sloptrap Codex namespace" ensure_codex_directory "$state_bucket" "sloptrap Codex state root" ensure_codex_directory "$CODEX_STATE_HOME_HOST" "project Codex state" ensure_capability_state_paths if [[ -L $CODEX_AUTH_FILE_HOST ]]; then error "Codex auth file '$CODEX_AUTH_FILE_HOST' must not be a symlink" fi if [[ -e $CODEX_AUTH_FILE_HOST && ! -f $CODEX_AUTH_FILE_HOST ]]; then error "expected Codex auth file '$CODEX_AUTH_FILE_HOST' to be a regular file" fi if [[ -f $CODEX_AUTH_FILE_HOST ]]; then return 0 fi if $DRY_RUN; then print_command touch "$CODEX_AUTH_FILE_HOST" return 0 fi : > "$CODEX_AUTH_FILE_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 "runtime '--sandbox' flag requires a mode (workspace-write, workspace-read-only, or danger-full-access)" fi sandbox_mode="${args[$((i + 1))]}" fi ((i+=1)) done if [[ -z $sandbox_mode ]]; then error "runtime flags must include '--sandbox ' (workspace-write, workspace-read-only, or danger-full-access)" fi case "$sandbox_mode" in workspace-write|workspace-read-only|danger-full-access) ;; *) error "sandbox mode '$sandbox_mode' is not allowed (expected workspace-write, workspace-read-only, or danger-full-access)" ;; 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[*]}" } targets_need_build() { local -a targets=("$@") if [[ ${#targets[@]} -eq 0 ]]; then return 0 fi local target for target in "${targets[@]}"; do case "$target" in build|rebuild|build-if-missing|run|login|shell|resume) return 0 ;; esac done return 1 } prepare_container_runtime() { resolve_container_workdir SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA SLOPTRAP_PACKAGES_CAPABILITY="" SLOPTRAP_RUN_AS_ROOT=false 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 if [[ -n $REQUESTED_CAPABILITIES && -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then error "capabilities require the embedded Dockerfile; custom Dockerfile overrides are not supported" fi SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps util-linux") validate_package_list "SLOPTRAP_PACKAGES" "$SLOPTRAP_PACKAGES_BASE" "SLOPTRAP_PACKAGES" if capability_list_contains "$REQUESTED_CAPABILITIES" "packet-capture"; then SLOPTRAP_PACKAGES_CAPABILITY+=" tcpdump" fi if capability_list_contains "$REQUESTED_CAPABILITIES" "nested-podman"; then SLOPTRAP_PACKAGES_CAPABILITY+=" podman uidmap fuse-overlayfs slirp4netns" fi SLOPTRAP_PACKAGES_CAPABILITY=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY") 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_DEFAULT=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1") SLOPTRAP_ROOTFS_READONLY=$SLOPTRAP_ROOTFS_READONLY_DEFAULT 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) local -a capability_opts=() 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 if capability_list_contains "$ENABLED_CAPABILITIES" "apt-install"; then SLOPTRAP_ROOTFS_READONLY=0 SLOPTRAP_RUN_AS_ROOT=true fi if capability_list_contains "$ENABLED_CAPABILITIES" "packet-capture"; then capability_opts+=(--cap-add NET_RAW --cap-add NET_ADMIN) SLOPTRAP_RUN_AS_ROOT=true fi if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then capability_opts+=(--device /dev/fuse) fi if $SLOPTRAP_RUN_AS_ROOT; then capability_opts+=( --cap-add SETUID --cap-add SETGID --cap-add CHOWN --cap-add DAC_OVERRIDE --cap-add FOWNER ) 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_STATE_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL" -v "$CODEX_AUTH_FILE_HOST:$SLOPTRAP_CODEX_HOME_CONT/auth.json$SLOPTRAP_VOLUME_LABEL" ) if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then volume_opts+=( -v "$CAPABILITY_STATE_HOST/podman-storage:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/storage$SLOPTRAP_VOLUME_LABEL" -v "$CAPABILITY_STATE_HOST/podman-run:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/run$SLOPTRAP_VOLUME_LABEL" -v "$CAPABILITY_STATE_HOST/podman-runtime:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/runtime$SLOPTRAP_VOLUME_LABEL" ) fi 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" -e "SLOPTRAP_WORKDIR=$SLOPTRAP_WORKDIR" -e "SLOPTRAP_HELPER_DIR=/tmp/sloptrap-helper" -e "SLOPTRAP_ACTIVE_CAPABILITIES=$ENABLED_CAPABILITIES" -e "SLOPTRAP_CAPTURE_DIR=$SLOPTRAP_CODEX_HOME_CONT/state/captures" -e "SLOPTRAP_AUDIT_LOG=$SLOPTRAP_CODEX_HOME_CONT/state/capabilities.log" -e "SLOPTRAP_INNER_PODMAN_ROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/storage" -e "SLOPTRAP_INNER_PODMAN_RUNROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/run" -e "XDG_RUNTIME_DIR=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/runtime" ) if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman" && [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then env_args+=(-e "SLOPTRAP_INNER_PODMAN_HOST_NETWORK=1") fi local uid gid uid=$(id -u) gid=$(id -g) env_args+=( -e "SLOPTRAP_HOST_UID=$uid" -e "SLOPTRAP_HOST_GID=$gid" ) local -a user_opts=("--user" "$uid:$gid") if [[ $CONTAINER_ENGINE == "podman" ]]; then user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}") fi if $SLOPTRAP_RUN_AS_ROOT; then user_opts=() if [[ $CONTAINER_ENGINE == "podman" ]]; then user_opts=(--userns="keep-id:uid=$uid,gid=$gid") fi fi CONTAINER_SHARED_OPTS=( "${network_opts[@]}" "${security_opts[@]}" "${capability_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_capability_trust ensure_codex_binary if [[ $SKIP_BUILD_BANNER != true ]]; then print_banner fi print_manifest_summary local extra_packages_arg local capability_packages_arg extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") capability_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY") 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 "CAPABILITY_PACKAGES=$capability_packages_arg" --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 write_capability_build_stamp } rebuild_image() { ensure_capability_trust ensure_codex_binary if [[ $SKIP_BUILD_BANNER != true ]]; then print_banner fi print_manifest_summary if ! $DRY_RUN; then status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME" fi local extra_packages_arg local capability_packages_arg extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") capability_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY") local -a cmd=( "$CONTAINER_ENGINE" build --no-cache --quiet -t "$SLOPTRAP_IMAGE_NAME" -f "$SLOPTRAP_DOCKERFILE_PATH" --label "$SLOPTRAP_IMAGE_LABEL" --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" --build-arg "CAPABILITY_PACKAGES=$capability_packages_arg" --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 write_capability_build_stamp } build_if_missing() { ensure_capability_trust 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 if ! capability_build_stamp_matches_current; then warn "image '$SLOPTRAP_IMAGE_NAME' capability stamp mismatch; rebuilding" rebuild_image return 0 fi local created if created=$("$CONTAINER_ENGINE" image inspect --format '{{.Created}}' "$SLOPTRAP_IMAGE_NAME" 2>/dev/null); then local created_epoch created_epoch=$(date -d "$created" +%s 2>/dev/null || true) if [[ -n $created_epoch ]]; then local now_epoch now_epoch=$(date +%s) local age_days=$(( (now_epoch - created_epoch) / 86400 )) if (( age_days > 30 )); then warn "image '$SLOPTRAP_IMAGE_NAME' is ${age_days} days old; rebuilding" rebuild_image fi fi fi 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=("$@") ensure_codex_storage_paths local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" "codex") 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 local runtime_prompt runtime_prompt=$(build_runtime_context_prompt) run_codex_command "$runtime_prompt" } run_login_target() { ensure_codex_storage_paths if ! $DRY_RUN; then status_line "Login %s\n" "$SLOPTRAP_IMAGE_NAME" fi local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" "codex" login) run_or_print "${cmd[@]}" } run_shell_target() { ensure_codex_storage_paths if ! $DRY_RUN; then status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME" fi local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" /bin/bash) 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 run_login_target ;; shell) build_if_missing run_shell_target ;; wizard) run_wizard "$MANIFEST_PATH" exit 0 ;; stop) stop_container ;; clean) clean_environment ;; prune) prune_sloptrap_images ;; *) error "unknown target '$target'" ;; esac } DRY_RUN=false PRINT_CONFIG=false SKIP_BUILD_BANNER=false TRUST_CAPABILITIES=false while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true shift ;; --print-config) PRINT_CONFIG=true shift ;; --trust-capabilities) TRUST_CAPABILITIES=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" TARGETS_INPUT=("$@") if [[ -f $MANIFEST_PATH ]]; then MANIFEST_PRESENT=true parse_manifest "$MANIFEST_PATH" fi if [[ ${#TARGETS_INPUT[@]} -gt 0 ]]; then target_index=0 while (( target_index < ${#TARGETS_INPUT[@]} )); do if [[ ${TARGETS_INPUT[$target_index]} == "wizard" ]]; then if (( ${#TARGETS_INPUT[@]} > 1 )); then warn "wizard runs standalone; ignoring other targets" fi run_wizard "$MANIFEST_PATH" exit 0 fi ((target_index+=1)) done fi if [[ ! -f $MANIFEST_PATH ]]; then if targets_need_build "${TARGETS_INPUT[@]}"; then if [[ -t 0 ]]; then run_wizard "$MANIFEST_PATH" SKIP_BUILD_BANNER=true MANIFEST=() MANIFEST_PRESENT=true parse_manifest "$MANIFEST_PATH" else warn "missing $MANIFEST_BASENAME; proceeding with defaults (run '$0 $CODE_DIR wizard' to create one)" fi fi 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" select_capability_state_paths CAPABILITY_MANIFEST_DIGEST=$(compute_manifest_digest) ensure_ignore_helper_root IGNORE_STUB_BASE="$IGNORE_HELPER_ROOT/session-${BASHPID:-$$}" resolve_sloptrap_ignore "$CODE_DIR" resolve_container_workdir NEED_LOGIN=false if [[ ! -s "$CODEX_AUTH_FILE_HOST" ]]; then NEED_LOGIN=true fi TARGETS=("${TARGETS_INPUT[@]}") if [[ ${#TARGETS[@]} -eq 0 ]]; then TARGETS=("run") fi DEFAULT_TARGETS=("${TARGETS[@]}") PACKAGES_EXTRA=${MANIFEST[packages_extra]-} REQUESTED_CAPABILITIES=$(normalize_capability_list "${MANIFEST[capabilities]-}") if [[ -n $REQUESTED_CAPABILITIES ]]; then ensure_safe_for_make "capabilities" "$REQUESTED_CAPABILITIES" fi validate_capability_list "capabilities" "$REQUESTED_CAPABILITIES" if [[ -n $REQUESTED_CAPABILITIES ]] && { $TRUST_CAPABILITIES || capability_trust_matches_current; }; then ENABLED_CAPABILITIES="$REQUESTED_CAPABILITIES" else ENABLED_CAPABILITIES="" fi 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=(codex_args container_opts_extra security_opts_extra env_extra env_passthrough default_targets default_target) for forbidden_key in "${forbidden_keys[@]}"; do if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then if [[ $forbidden_key == "codex_args" ]]; then error "$MANIFEST_PATH: key 'codex_args' has been deprecated; sloptrap now always uses '$DEFAULT_CODEX_ARGS_DISPLAY'" fi 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[@]}") ensure_safe_sandbox "${CODEX_ARGS_ARRAY[@]}" CODEX_ARGS_DISPLAY=$DEFAULT_CODEX_ARGS_DISPLAY 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_AUTH_FILE_HOST" fi 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