Files
skz-sloptrap/sloptrap
2026-03-09 19:23:21 +01:00

2419 lines
71 KiB
Bash
Executable File

#!/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] <code-directory> [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 <id> Build if needed, then run 'codex resume <id>'\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:-/run/sloptrap-helper}
mkdir -p "$helper_dir/queue"
chmod 700 "$helper_dir"
if [[ -n ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then
/usr/local/bin/sloptrap-helperd &
helper_pid=$!
fi
exec runuser -u sloptrap --preserve-environment -- "$@"
fi
exec "$@"
EOF
;;
sloptrap-helperd)
cat <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-}
audit_log=${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}
mkdir -p "$queue_dir" "$(dirname "$audit_log")"
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"
}
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 ]]; 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
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=$(<"$iface_file")
filter_file="$request_dir/filter"
output_file="$request_dir/output"
stdout_mode=0
[[ -f "$request_dir/stdout_mode" ]] && stdout_mode=$(<"$request_dir/stdout_mode")
local -a cmd=(tcpdump -i "$iface")
if [[ -s $filter_file ]]; then
local filter
filter=$(<"$filter_file")
local -a filter_tokens=()
read -r -a filter_tokens <<< "$filter"
cmd+=("${filter_tokens[@]}")
fi
if [[ -s $output_file ]]; then
local capture_path
capture_path=$(<"$output_file")
mkdir -p "$(dirname "$capture_path")"
cmd+=(-w "$capture_path")
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
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
op=$(<"$request_dir/op")
: >"$request_dir/stdout"
: >"$request_dir/stderr"
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
done
EOF
;;
slop-apt)
cat <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
mkdir -p "$queue_dir"
if [[ ${1-} != "install" ]]; then
printf 'usage: slop-apt install <package...>\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+.-]+$ ]]; then
printf 'slop-apt: invalid package name %s\n' "$package" >&2
exit 2
fi
done
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:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
default_output=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}
mkdir -p "$queue_dir" "$default_output"
if [[ ${1-} != "capture" ]]; then
printf 'usage: slopcap capture --interface <iface> [--filter <expr>] [--output <path>] [--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
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
if [[ $# -eq 0 ]]; then
printf 'usage: sloppodman <pull|build|tag|run|ps|logs|stop|rm|inspect> ...\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
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
}
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]}"
;;
--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
;;
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]}
if [[ $arg == "--network" ]]; then
((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
fi
((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"
}
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+.-]+$ ]]; 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" <<EOF
name=$default_name
packages_extra=$default_packages_extra
capabilities=$default_capabilities
allow_host_network=$default_allow_host_network
EOF
info_line "Wrote %s\n" "$manifest_path"
local ignore_path="$CODE_DIR/.sloptrapignore"
if [[ ! -f $ignore_path ]]; then
info_line "Hint: create %s to hide files from the container (e.g., .git/ or secrets/).\n" "$ignore_path"
fi
info_line "Hint: run 'sloptrap %s build' or 'sloptrap %s rebuild' to apply changes.\n" "$CODE_DIR" "$CODE_DIR"
}
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_STATE_HOME_HOST"
info_line "codex_root=%s\n" "$CODEX_ROOT_HOST"
info_line "codex_state_home=%s\n" "$CODEX_STATE_HOME_HOST"
info_line "codex_auth_file=%s\n" "$CODEX_AUTH_FILE_HOST"
info_line "codex_state_key=%s\n" "$CODEX_STATE_KEY"
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 "runtime_flags=%s\n" "$CODEX_ARGS_DISPLAY"
info_line "requested_capabilities=%s\n" "$REQUESTED_CAPABILITIES"
info_line "enabled_capabilities=%s\n" "$ENABLED_CAPABILITIES"
info_line "capability_trust=%s\n" "$(capability_trust_matches_current && printf true || printf false)"
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
}
print_manifest_summary() {
highlight_line "Manifest summary\n"
comment_line " manifest_path=%s\n" "$MANIFEST_PATH"
comment_line " name=%s\n" "$PROJECT_NAME"
comment_line " packages_extra=%s\n" "$PACKAGES_EXTRA"
comment_line " capabilities=%s\n" "$REQUESTED_CAPABILITIES"
comment_line " runtime_flags=%s\n" "$CODEX_ARGS_DISPLAY"
comment_line " allow_host_network=%s\n" "$ALLOW_HOST_NETWORK"
}
build_runtime_context_prompt() {
local manifest_present prompt manifest_capabilities trusted enabled network_mode
manifest_present="false"
if [[ -f $MANIFEST_PATH ]]; then
manifest_present="true"
fi
manifest_capabilities=${REQUESTED_CAPABILITIES:-none}
trusted="none"
if [[ -n $REQUESTED_CAPABILITIES ]] && capability_trust_matches_current; then
trusted=$REQUESTED_CAPABILITIES
fi
enabled=${ENABLED_CAPABILITIES:-none}
network_mode="isolated"
if [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then
network_mode="host"
fi
prompt=$(cat <<EOF
You are running inside sloptrap, which confines Codex inside a container.
This startup note describes the sloptrap runtime only; it does not replace higher-priority instructions from AGENTS.md or the system.
Container layout:
- /workspace is the project mount.
- /codex is persistent Codex state for this project.
- The project manifest path is /workspace/.sloptrap and it may be absent.
Manifest key meanings:
- name: labels the sloptrap project/image/container names.
- packages_extra: Debian packages added when the image was built.
- capabilities: privileged features requested by the manifest; only capabilities enabled for this run are currently usable.
- allow_host_network: enables host networking when true; otherwise networking is isolated.
Current resolved sloptrap state:
- manifest_present=$manifest_present
- project_name=$PROJECT_NAME
- packages_extra=${PACKAGES_EXTRA:-none}
- manifest_capabilities=$manifest_capabilities
- trusted_capabilities=$trusted
- enabled_capabilities=$enabled
- network_mode=$network_mode
- runtime_flags=$CODEX_ARGS_DISPLAY
If you need exact project configuration, inspect /workspace/.sloptrap directly.
EOF
)
printf '%s' "$prompt"
}
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_PACKAGES_CAPABILITY=""
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=""
SLOPTRAP_ROOTFS_READONLY_DEFAULT=""
SLOPTRAP_RUN_AS_ROOT=false
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_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 <mode>' (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 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
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=/run/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)
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=()
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