Add a wizzard to configure .sloptrap files

This commit is contained in:
Samuel Aubertin
2026-01-24 17:44:47 +01:00
parent 7630e7edba
commit 046b56e3f6
8 changed files with 365 additions and 73 deletions

263
sloptrap
View File

@@ -126,7 +126,6 @@ status_line() {
comment_line() {
print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@"
}
warn_line() {
print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@"
}
@@ -154,7 +153,9 @@ EOF
MANIFEST_BASENAME=".sloptrap"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALID_NAME_REGEX='^[A-Za-z0-9_.-]+$'
DEFAULT_CODEX_ARGS=(--sandbox workspace-write)
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"
@@ -170,13 +171,13 @@ usage() {
info_line "Example manifest entries:\n"
comment_line " name=my-project\n"
comment_line " packages_extra=kubectl helm\n"
comment_line " default_targets=run\n"
comment_line " codex_args=--sandbox workspace-write\n"
comment_line " codex_args=--sandbox danger-full-access --ask-for-approval never\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 " wizzard 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"
}
@@ -670,6 +671,145 @@ parse_manifest() {
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 "wizzard requires an interactive terminal"
fi
printf '%s' "$input"
}
validate_wizzard_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_wizzard_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
}
validate_wizzard_codex_args() {
local value=$1
ensure_safe_for_make "codex_args" "$value"
local -a args=("${DEFAULT_CODEX_ARGS[@]}")
local -a tokens=()
if [[ -n $value ]]; then
read -r -a tokens <<< "$value"
args+=("${tokens[@]}")
fi
ensure_safe_sandbox "${args[@]}"
}
run_wizzard() {
local manifest_path=$1
if [[ -L $manifest_path ]]; then
error "$manifest_path: manifest must not be a symlink"
fi
if [[ ! -t 0 ]]; then
error "wizzard requires an interactive terminal"
fi
if [[ ! -f $manifest_path ]]; then
print_banner
fi
local default_name
local default_packages_extra
local default_codex_args
local default_allow_host_network
default_name=$(manifest_default_value "name" "$(basename "$CODE_DIR")")
default_packages_extra=$(manifest_default_value "packages_extra" "")
default_codex_args=$(manifest_default_value "codex_args" "$DEFAULT_CODEX_ARGS_DISPLAY")
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_wizzard_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 "codex_args: Extra CLI flags passed to Codex at runtime.\n"
value=$(prompt_manifest_value "codex_args" "$default_codex_args")
value=$(trim "$value")
[[ -n $value ]] || value=$default_codex_args
validate_wizzard_codex_args "$value"
default_codex_args=$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_wizzard_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
codex_args=$default_codex_args
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"
@@ -705,6 +845,15 @@ print_config() {
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 " codex_args=%s\n" "$CODEX_ARGS_DISPLAY"
comment_line " allow_host_network=%s\n" "$ALLOW_HOST_NETWORK"
}
declare -a CONTAINER_SHARED_OPTS=()
declare -a BASE_CONTAINER_CMD=()
SLOPTRAP_IMAGE_NAME=""
@@ -865,20 +1014,20 @@ ensure_safe_sandbox() {
while [[ $i -lt ${#args[@]} ]]; do
if [[ ${args[$i]} == "--sandbox" ]]; then
if (( i + 1 >= ${#args[@]} )); then
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
error "$MANIFEST_PATH: '--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 "$MANIFEST_PATH: codex_args must include '--sandbox <mode>' (workspace-write or workspace-read-only)"
error "$MANIFEST_PATH: codex_args must include '--sandbox <mode>' (workspace-write, workspace-read-only, or danger-full-access)"
fi
case "$sandbox_mode" in
workspace-write|workspace-read-only)
workspace-write|workspace-read-only|danger-full-access)
;;
*)
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write or workspace-read-only)"
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write, workspace-read-only, or danger-full-access)"
;;
esac
}
@@ -892,6 +1041,22 @@ normalize_package_list() {
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
@@ -1051,7 +1216,10 @@ prepare_container_runtime() {
build_image() {
ensure_codex_binary
print_banner
if [[ $SKIP_BUILD_BANNER != true ]]; then
print_banner
fi
print_manifest_summary
local extra_packages_arg
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
if ! $DRY_RUN; then
@@ -1088,13 +1256,17 @@ build_image() {
rebuild_image() {
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
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
local -a cmd=(
"$CONTAINER_ENGINE" build --no-cache
"$CONTAINER_ENGINE" build --no-cache --quiet
-t "$SLOPTRAP_IMAGE_NAME"
-f "$SLOPTRAP_DOCKERFILE_PATH"
--label "$SLOPTRAP_IMAGE_LABEL"
@@ -1128,6 +1300,20 @@ build_if_missing() {
return 0
fi
if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then
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
@@ -1250,6 +1436,10 @@ dispatch_target() {
build_if_missing
run_shell_target
;;
wizzard)
run_wizzard "$MANIFEST_PATH"
exit 0
;;
stop)
stop_container
;;
@@ -1267,6 +1457,7 @@ dispatch_target() {
DRY_RUN=false
PRINT_CONFIG=false
SKIP_BUILD_BANNER=false
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -1313,12 +1504,41 @@ 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]} == "wizzard" ]]; then
if (( ${#TARGETS_INPUT[@]} > 1 )); then
warn "wizzard runs standalone; ignoring other targets"
fi
run_wizzard "$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_wizzard "$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 wizzard' 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"
@@ -1341,13 +1561,7 @@ fi
TARGETS=("$@")
if [[ ${#TARGETS[@]} -eq 0 ]]; then
if [[ -n ${MANIFEST[default_targets]-} ]]; then
read -r -a TARGETS <<< "${MANIFEST[default_targets]}"
elif [[ -n ${MANIFEST[default_target]-} ]]; then
read -r -a TARGETS <<< "${MANIFEST[default_target]}"
else
TARGETS=("run")
fi
TARGETS=("run")
fi
DEFAULT_TARGETS=("${TARGETS[@]}")
@@ -1367,7 +1581,7 @@ if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
esac
fi
forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough)
forbidden_keys=(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
error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons"
@@ -1382,10 +1596,13 @@ CONTAINER_ENGINE="$(detect_container_engine)"
CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}")
if [[ -n ${MANIFEST[codex_args]-} ]]; then
ensure_safe_for_make "codex_args" "${MANIFEST[codex_args]}"
declare -a manifest_codex_args=()
read -r -a manifest_codex_args <<< "${MANIFEST[codex_args]}"
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
unset -v manifest_codex_args
manifest_codex_args_value=$(trim "${MANIFEST[codex_args]}")
if [[ $manifest_codex_args_value != "$DEFAULT_CODEX_ARGS_DISPLAY" ]]; then
declare -a manifest_codex_args=()
read -r -a manifest_codex_args <<< "$manifest_codex_args_value"
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
unset -v manifest_codex_args
fi
fi
declare -a sanitized_codex_args=()
declare -a sandbox_pair=()
@@ -1393,7 +1610,7 @@ codex_args_index=0
while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do
if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then
if (( codex_args_index + 1 >= ${#CODEX_ARGS_ARRAY[@]} )); then
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write, workspace-read-only, or danger-full-access)"
fi
sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}")
((codex_args_index+=2))