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

View File

@@ -1,15 +1,4 @@
# Example Sloptrap manifest.
# Project identifier used for container/image naming.
name=sloptrap name=sloptrap
# Default targets invoked when ./sloptrap is run without explicit args.
# default_targets=run
# Extra Debian packages installed into the helper image (space-delimited list).
packages_extra=make shellcheck jq packages_extra=make shellcheck jq
# Additional Codex CLI switches appended when launching Codex.
codex_args=--sandbox workspace-write codex_args=--sandbox workspace-write
allow_host_network=false
# Allow the container host to be reachable from within
# allow_host_network=false

View File

@@ -30,17 +30,8 @@ install update: $(PROGRAM)
@install -Dm755 $(PROGRAM) "$(INSTALL_PATH)" @install -Dm755 $(PROGRAM) "$(INSTALL_PATH)"
@printf '%b%bSuccess!%b Run it with:%b\n' '$(PREFIX_COMMENT)' '$(COLOR_HIGHLIGHT)' '$(COLOR_TEXT)' '$(RESET)' @printf '%b%bSuccess!%b Run it with:%b\n' '$(PREFIX_COMMENT)' '$(COLOR_HIGHLIGHT)' '$(COLOR_TEXT)' '$(RESET)'
@printf '%b%b%b %s /path/to/project%b\n' '$(PREFIX_HIGHLIGHT)' '$(COLOR_HIGHLIGHT)' '\033[1m' '$(PROGRAM)' '$(RESET)' @printf '%b%b%b %s /path/to/project%b\n' '$(PREFIX_HIGHLIGHT)' '$(COLOR_HIGHLIGHT)' '\033[1m' '$(PROGRAM)' '$(RESET)'
@printf '%b%bExample files to configure your project:%b\n' '$(PREFIX_TEXT)' '$(COLOR_TEXT)' '$(RESET)' @printf '%b%bConfigure your project with the wizard:%b\n' '$(PREFIX_TEXT)' '$(COLOR_TEXT)' '$(RESET)'
@printf '%b%b /path/to/project/%b.sloptrap%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(COLOR_TEXT)' '$(RESET)' @printf '%b%b sloptrap /path/to/project wizzard%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b name=your-project%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b default_targets=build run%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b packages_extra=make jq%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b codex_args=--sandbox workspace-write%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b allow_host_network=false%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b /path/to/project/%b.sloptrapignore%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(COLOR_TEXT)' '$(RESET)'
@printf '%b%b .git/%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b secrets/%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
@printf '%b%b build/output.log%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)'
uninstall: uninstall:
@printf '%b%bRemoving%b %b%s%b\n' '$(PREFIX_COMMENT)' '$(COLOR_TEXT)' '$(COLOR_TEXT)' '$(COLOR_COMMENT)' '$(INSTALL_PATH)' '$(RESET)' @printf '%b%bRemoving%b %b%s%b\n' '$(PREFIX_COMMENT)' '$(COLOR_TEXT)' '$(COLOR_TEXT)' '$(COLOR_COMMENT)' '$(INSTALL_PATH)' '$(RESET)'

View File

@@ -24,9 +24,8 @@ brew install coreutils gnu-tar jq
```bash ```bash
cat > path/to/project/.sloptrap <<'EOF' cat > path/to/project/.sloptrap <<'EOF'
name=path/to/project name=path/to/project
default_targets=run
packages_extra=make packages_extra=make
codex_args=--sandbox workspace-write codex_args=--sandbox danger-full-access --ask-for-approval never
EOF EOF
cat > path/to/project/.sloptrapignore <<'EOF' cat > path/to/project/.sloptrapignore <<'EOF'
@@ -54,20 +53,19 @@ brew install coreutils gnu-tar jq
The manifest is optional. When absent, sloptrap derives: The manifest is optional. When absent, sloptrap derives:
- `name = basename(project directory)` - `name = basename(project directory)`
- `default_targets = run`
- `packages_extra = ""` (none) - `packages_extra = ""` (none)
- `codex_args = "--sandbox workspace-write"` - `codex_args = "--sandbox danger-full-access --ask-for-approval never"`
If a build is requested and no `.sloptrap` exists, sloptrap prompts to create one interactively.
Supported keys when the manifest is present: Supported keys when the manifest is present:
| Key | Default | Notes | | Key | Default | Notes |
| --- | --- | --- | | --- | --- | --- |
| `name` | project directory name | Must match `^[A-Za-z0-9_.-]+$`. Used for image/container naming. | | `name` | project directory name | Must match `^[A-Za-z0-9_.-]+$`. Used for image/container naming. |
| `default_targets` | `run` | Space-separated targets invoked when none are provided on the CLI. |
| `packages_extra` | *empty* | Additional Debian packages installed during `docker/podman build`. Tokens must be alphanumeric plus `+.-`. | | `packages_extra` | *empty* | Additional Debian packages installed during `docker/podman build`. Tokens must be alphanumeric plus `+.-`. |
| `codex_args` | `--sandbox workspace-write` | Passed verbatim to the Codex CLI entrypoint. Tokens are shell-split, so quote values with spaces (e.g., `--profile security-audit`). | | `codex_args` | `--sandbox danger-full-access --ask-for-approval never` | Passed verbatim to the Codex CLI entrypoint. Tokens are shell-split, so quote values with spaces (e.g., `--profile security-audit`). |
| `allow_host_network` | `false` | `true` opts into `--network host`; keep `false` unless the project absolutely requires direct access to host-local services. | | `allow_host_network` | `false` | `true` opts into `--network host`; keep `false` unless the project absolutely requires direct access to host-local services. |
`codex_args` are appended after the default sandbox flag, and sloptrap refuses to run if the resulting `--sandbox` mode is anything other than `workspace-write` or `workspace-read-only`. `codex_args` are appended after the default sandbox flag, and sloptrap refuses to run if the resulting `--sandbox` mode is anything other than `workspace-write`, `workspace-read-only`, or `danger-full-access`.
Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start. Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start.
@@ -93,7 +91,7 @@ Options:
Behaviour: Behaviour:
- Missing manifests are treated as default configuration. - Missing manifests are treated as default configuration; when a build is requested, sloptrap runs the interactive wizard if a TTY is available, otherwise it warns and continues with defaults.
- `SLOPTRAP_CONTAINER_ENGINE` overrides engine auto-detection. - `SLOPTRAP_CONTAINER_ENGINE` overrides engine auto-detection.
- If `${HOME}/.codex/auth.json` is absent, sloptrap prepends a login run before executing your targets. - If `${HOME}/.codex/auth.json` is absent, sloptrap prepends a login run before executing your targets.
- Exit status mirrors the last target executed; errors in parsing or setup abort early with a message. - Exit status mirrors the last target executed; errors in parsing or setup abort early with a message.
@@ -107,7 +105,7 @@ Behaviour:
## Built-in Targets ## Built-in Targets
Targets are supplied after the code directory (or via `default_targets` in the manifest). When omitted, sloptrap defaults to `run`. Targets are supplied after the code directory. When omitted, sloptrap defaults to `run`.
| Target | Description | | Target | Description |
| --- | --- | | --- | --- |
@@ -118,6 +116,7 @@ Targets are supplied after the code directory (or via `default_targets` in the m
| `resume <session-id>` | Continues a Codex session by running `codex resume <session-id>` inside the container (builds if needed). | | `resume <session-id>` | Continues a Codex session by running `codex resume <session-id>` inside the container (builds if needed). |
| `login` | Starts Codex in login mode to bootstrap `${HOME}/.codex`. | | `login` | Starts Codex in login mode to bootstrap `${HOME}/.codex`. |
| `shell` | Launches `/bin/bash` inside the container for debugging. | | `shell` | Launches `/bin/bash` inside the container for debugging. |
| `wizzard` | Creates or updates `.sloptrap` interactively (no build); rerun `build` or `rebuild` afterward. |
| `stop` | Best-effort stop of the running container (if any). | | `stop` | Best-effort stop of the running container (if any). |
| `clean` | Removes `.sloptrap-ignores`, deletes the container/image, and stops the container if necessary. | | `clean` | Removes `.sloptrap-ignores`, deletes the container/image, and stops the container if necessary. |

253
sloptrap
View File

@@ -126,7 +126,6 @@ status_line() {
comment_line() { comment_line() {
print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@" print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@"
} }
warn_line() { warn_line() {
print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@" print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@"
} }
@@ -154,7 +153,9 @@ EOF
MANIFEST_BASENAME=".sloptrap" MANIFEST_BASENAME=".sloptrap"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALID_NAME_REGEX='^[A-Za-z0-9_.-]+$' 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_KEY="net.sk4nz.sloptrap.managed"
SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1" SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1"
@@ -170,13 +171,13 @@ usage() {
info_line "Example manifest entries:\n" info_line "Example manifest entries:\n"
comment_line " name=my-project\n" comment_line " name=my-project\n"
comment_line " packages_extra=kubectl helm\n" comment_line " packages_extra=kubectl helm\n"
comment_line " default_targets=run\n" comment_line " codex_args=--sandbox danger-full-access --ask-for-approval never\n"
comment_line " codex_args=--sandbox workspace-write\n"
info_line "\n" info_line "\n"
info_line "Example targets:\n" info_line "Example targets:\n"
comment_line " run Build if needed, then launch Codex\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 " resume <id> Build if needed, then run 'codex resume <id>'\n"
comment_line " shell Drop into an interactive /bin/bash session\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 " clean Remove the project container/image cache\n"
comment_line " prune Remove dangling/unused sloptrap images\n" comment_line " prune Remove dangling/unused sloptrap images\n"
} }
@@ -670,6 +671,145 @@ parse_manifest() {
done < "$manifest_path" 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() { print_config() {
local manifest_path=$1 local manifest_path=$1
info_line "manifest_path=%s\n" "$manifest_path" info_line "manifest_path=%s\n" "$manifest_path"
@@ -705,6 +845,15 @@ print_config() {
done 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 CONTAINER_SHARED_OPTS=()
declare -a BASE_CONTAINER_CMD=() declare -a BASE_CONTAINER_CMD=()
SLOPTRAP_IMAGE_NAME="" SLOPTRAP_IMAGE_NAME=""
@@ -865,20 +1014,20 @@ ensure_safe_sandbox() {
while [[ $i -lt ${#args[@]} ]]; do while [[ $i -lt ${#args[@]} ]]; do
if [[ ${args[$i]} == "--sandbox" ]]; then if [[ ${args[$i]} == "--sandbox" ]]; then
if (( i + 1 >= ${#args[@]} )); 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 fi
sandbox_mode="${args[$((i + 1))]}" sandbox_mode="${args[$((i + 1))]}"
fi fi
((i+=1)) ((i+=1))
done done
if [[ -z $sandbox_mode ]]; then 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 fi
case "$sandbox_mode" in 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 esac
} }
@@ -892,6 +1041,22 @@ normalize_package_list() {
printf '%s' "${tokens[*]}" 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() { prepare_container_runtime() {
resolve_container_workdir resolve_container_workdir
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA
@@ -1051,7 +1216,10 @@ prepare_container_runtime() {
build_image() { build_image() {
ensure_codex_binary ensure_codex_binary
if [[ $SKIP_BUILD_BANNER != true ]]; then
print_banner print_banner
fi
print_manifest_summary
local extra_packages_arg local extra_packages_arg
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
if ! $DRY_RUN; then if ! $DRY_RUN; then
@@ -1088,13 +1256,17 @@ build_image() {
rebuild_image() { rebuild_image() {
ensure_codex_binary ensure_codex_binary
if [[ $SKIP_BUILD_BANNER != true ]]; then
print_banner
fi
print_manifest_summary
if ! $DRY_RUN; then if ! $DRY_RUN; then
status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME" status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME"
fi fi
local extra_packages_arg local extra_packages_arg
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
local -a cmd=( local -a cmd=(
"$CONTAINER_ENGINE" build --no-cache "$CONTAINER_ENGINE" build --no-cache --quiet
-t "$SLOPTRAP_IMAGE_NAME" -t "$SLOPTRAP_IMAGE_NAME"
-f "$SLOPTRAP_DOCKERFILE_PATH" -f "$SLOPTRAP_DOCKERFILE_PATH"
--label "$SLOPTRAP_IMAGE_LABEL" --label "$SLOPTRAP_IMAGE_LABEL"
@@ -1128,6 +1300,20 @@ build_if_missing() {
return 0 return 0
fi fi
if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then 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 return 0
fi fi
build_image build_image
@@ -1250,6 +1436,10 @@ dispatch_target() {
build_if_missing build_if_missing
run_shell_target run_shell_target
;; ;;
wizzard)
run_wizzard "$MANIFEST_PATH"
exit 0
;;
stop) stop)
stop_container stop_container
;; ;;
@@ -1267,6 +1457,7 @@ dispatch_target() {
DRY_RUN=false DRY_RUN=false
PRINT_CONFIG=false PRINT_CONFIG=false
SKIP_BUILD_BANNER=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@@ -1313,12 +1504,41 @@ if [[ $CODE_DIR == "/" ]]; then
error "project root may not be '/'" error "project root may not be '/'"
fi fi
MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME" MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME"
TARGETS_INPUT=("$@")
if [[ -f $MANIFEST_PATH ]]; then if [[ -f $MANIFEST_PATH ]]; then
MANIFEST_PRESENT=true MANIFEST_PRESENT=true
parse_manifest "$MANIFEST_PATH" parse_manifest "$MANIFEST_PATH"
fi 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")} PROJECT_NAME=${MANIFEST[name]-$(basename "$CODE_DIR")}
if [[ -z $PROJECT_NAME ]]; then if [[ -z $PROJECT_NAME ]]; then
error "$MANIFEST_PATH: project name resolved to empty string" error "$MANIFEST_PATH: project name resolved to empty string"
@@ -1341,14 +1561,8 @@ fi
TARGETS=("$@") TARGETS=("$@")
if [[ ${#TARGETS[@]} -eq 0 ]]; then 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") TARGETS=("run")
fi fi
fi
DEFAULT_TARGETS=("${TARGETS[@]}") DEFAULT_TARGETS=("${TARGETS[@]}")
@@ -1367,7 +1581,7 @@ if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
esac esac
fi 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 for forbidden_key in "${forbidden_keys[@]}"; do
if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then
error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons" error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons"
@@ -1382,18 +1596,21 @@ CONTAINER_ENGINE="$(detect_container_engine)"
CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}") CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}")
if [[ -n ${MANIFEST[codex_args]-} ]]; then if [[ -n ${MANIFEST[codex_args]-} ]]; then
ensure_safe_for_make "codex_args" "${MANIFEST[codex_args]}" ensure_safe_for_make "codex_args" "${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=() declare -a manifest_codex_args=()
read -r -a manifest_codex_args <<< "${MANIFEST[codex_args]}" read -r -a manifest_codex_args <<< "$manifest_codex_args_value"
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}") CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
unset -v manifest_codex_args unset -v manifest_codex_args
fi fi
fi
declare -a sanitized_codex_args=() declare -a sanitized_codex_args=()
declare -a sandbox_pair=() declare -a sandbox_pair=()
codex_args_index=0 codex_args_index=0
while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do
if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then
if (( codex_args_index + 1 >= ${#CODEX_ARGS_ARRAY[@]} )); 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 fi
sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}") sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}")
((codex_args_index+=2)) ((codex_args_index+=2))

View File

@@ -208,7 +208,7 @@ run_mount_injection() {
setup_stub_env setup_stub_env
rm -rf "$scenario_dir/.sloptrap-ignores" rm -rf "$scenario_dir/.sloptrap-ignores"
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" >/dev/null 2>&1; then "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "mount_injection: sloptrap exited non-zero" record_failure "mount_injection: sloptrap exited non-zero"
teardown_stub_env teardown_stub_env
return return
@@ -233,7 +233,7 @@ run_mount_injection() {
run_root_target() { run_root_target() {
local scenario_dir="$TEST_ROOT/root_target" local scenario_dir="$TEST_ROOT/root_target"
printf '==> root_target\n' printf '==> root_target\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "root_target: expected rejection for project-root mask" record_failure "root_target: expected rejection for project-root mask"
return return
fi fi
@@ -244,7 +244,7 @@ run_symlink_escape() {
printf '==> symlink_escape\n' printf '==> symlink_escape\n'
local secret_path="$ROOT_DIR/secrets.txt" local secret_path="$ROOT_DIR/secrets.txt"
touch "$secret_path" touch "$secret_path"
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "symlink_escape: expected failure for symlink escape" record_failure "symlink_escape: expected failure for symlink escape"
rm -f "$secret_path" rm -f "$secret_path"
return return
@@ -255,7 +255,7 @@ run_symlink_escape() {
run_manifest_injection() { run_manifest_injection() {
local scenario_dir="$TEST_ROOT/manifest_injection" local scenario_dir="$TEST_ROOT/manifest_injection"
printf '==> manifest_injection\n' printf '==> manifest_injection\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "manifest_injection: expected rejection of bad make override" record_failure "manifest_injection: expected rejection of bad make override"
return return
fi fi
@@ -264,10 +264,10 @@ run_manifest_injection() {
run_helper_symlink() { run_helper_symlink() {
local scenario_dir="$TEST_ROOT/helper_symlink" local scenario_dir="$TEST_ROOT/helper_symlink"
printf '==> helper_symlink\n' printf '==> helper_symlink\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "helper_symlink: expected rejection when helper directory is a symlink" record_failure "helper_symlink: expected rejection when helper directory is a symlink"
fi fi
if "$SLOPTRAP_BIN" "$scenario_dir" clean >/dev/null 2>&1; then if "$SLOPTRAP_BIN" "$scenario_dir" clean </dev/null >/dev/null 2>&1; then
record_failure "helper_symlink: expected rejection for clean when helper directory is a symlink" record_failure "helper_symlink: expected rejection for clean when helper directory is a symlink"
fi fi
} }
@@ -281,7 +281,7 @@ run_secret_mask() {
FAKE_PODMAN_INSPECT_FAIL=1 SECRET_MASK_VERIFY=1 \ FAKE_PODMAN_INSPECT_FAIL=1 SECRET_MASK_VERIFY=1 \
SECRET_MASK_EXPECTED_TARGET="${custom_workdir}/secret.txt" \ SECRET_MASK_EXPECTED_TARGET="${custom_workdir}/secret.txt" \
SLOPTRAP_WORKDIR="$custom_workdir" \ SLOPTRAP_WORKDIR="$custom_workdir" \
"$SLOPTRAP_BIN" "$scenario_dir" >/dev/null 2>&1; then "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "secret_mask: masking check failed" record_failure "secret_mask: masking check failed"
teardown_stub_env teardown_stub_env
return return
@@ -295,7 +295,7 @@ run_resume_target() {
setup_stub_env setup_stub_env
local session_id="019a81b7-32d2-7622-8639-6698c6579625" local session_id="019a81b7-32d2-7622-8639-6698c6579625"
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \ if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \
"$SLOPTRAP_BIN" "$scenario_dir" resume "$session_id" >/dev/null 2>&1; then "$SLOPTRAP_BIN" "$scenario_dir" resume "$session_id" </dev/null >/dev/null 2>&1; then
record_failure "resume_target: sloptrap exited non-zero" record_failure "resume_target: sloptrap exited non-zero"
teardown_stub_env teardown_stub_env
return return
@@ -313,7 +313,7 @@ run_codex_symlink_home() {
local tmp_home local tmp_home
tmp_home=$(mktemp -d) tmp_home=$(mktemp -d)
ln -s /etc "$tmp_home/.codex" ln -s /etc "$tmp_home/.codex"
if HOME="$tmp_home" "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if HOME="$tmp_home" "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "codex_symlink_home: expected rejection when ~/.codex is a symlink" record_failure "codex_symlink_home: expected rejection when ~/.codex is a symlink"
fi fi
rm -rf "$tmp_home" rm -rf "$tmp_home"
@@ -323,7 +323,7 @@ run_root_directory_project() {
printf '==> root_directory_project\n' printf '==> root_directory_project\n'
local tmp_home local tmp_home
tmp_home=$(mktemp -d) tmp_home=$(mktemp -d)
if HOME="$tmp_home" "$SLOPTRAP_BIN" --dry-run / >/dev/null 2>&1; then if HOME="$tmp_home" "$SLOPTRAP_BIN" --dry-run / </dev/null >/dev/null 2>&1; then
record_failure "root_directory_project: expected rejection for '/' project root" record_failure "root_directory_project: expected rejection for '/' project root"
fi fi
rm -rf "$tmp_home" rm -rf "$tmp_home"
@@ -338,7 +338,7 @@ run_shared_dir_override() {
bogus_shared=$(mktemp -d) bogus_shared=$(mktemp -d)
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \ if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \
SLOPTRAP_SHARED_DIR="$bogus_shared" FAKE_PODMAN_INSPECT_FAIL=1 \ SLOPTRAP_SHARED_DIR="$bogus_shared" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" >/dev/null 2>&1; then "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "shared_dir_override: sloptrap exited non-zero" record_failure "shared_dir_override: sloptrap exited non-zero"
teardown_stub_env teardown_stub_env
rm -rf "$bogus_shared" rm -rf "$bogus_shared"
@@ -361,7 +361,7 @@ run_packages_env_validation() {
local tmp_home local tmp_home
tmp_home=$(mktemp -d) tmp_home=$(mktemp -d)
if HOME="$tmp_home" SLOPTRAP_PACKAGES='curl";touch /tmp/pwn #' \ if HOME="$tmp_home" SLOPTRAP_PACKAGES='curl";touch /tmp/pwn #' \
"$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "packages_env_validation: expected rejection of invalid SLOPTRAP_PACKAGES" record_failure "packages_env_validation: expected rejection of invalid SLOPTRAP_PACKAGES"
fi fi
rm -rf "$tmp_home" rm -rf "$tmp_home"
@@ -370,7 +370,7 @@ run_packages_env_validation() {
run_abs_path_ignore() { run_abs_path_ignore() {
local scenario_dir="$TEST_ROOT/abs_path_ignore" local scenario_dir="$TEST_ROOT/abs_path_ignore"
printf '==> abs_path_ignore\n' printf '==> abs_path_ignore\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "abs_path_ignore: expected rejection for anchored parent traversal entry" record_failure "abs_path_ignore: expected rejection for anchored parent traversal entry"
fi fi
} }
@@ -378,7 +378,7 @@ run_abs_path_ignore() {
run_dotdot_ignore() { run_dotdot_ignore() {
local scenario_dir="$TEST_ROOT/dotdot_ignore" local scenario_dir="$TEST_ROOT/dotdot_ignore"
printf '==> dotdot_ignore\n' printf '==> dotdot_ignore\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "dotdot_ignore: expected rejection for parent traversal entry" record_failure "dotdot_ignore: expected rejection for parent traversal entry"
fi fi
} }
@@ -386,7 +386,7 @@ run_dotdot_ignore() {
run_invalid_manifest_name() { run_invalid_manifest_name() {
local scenario_dir="$TEST_ROOT/invalid_manifest_name" local scenario_dir="$TEST_ROOT/invalid_manifest_name"
printf '==> invalid_manifest_name\n' printf '==> invalid_manifest_name\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_manifest_name: expected rejection for illegal name" record_failure "invalid_manifest_name: expected rejection for illegal name"
fi fi
} }
@@ -394,7 +394,7 @@ run_invalid_manifest_name() {
run_invalid_manifest_sandbox() { run_invalid_manifest_sandbox() {
local scenario_dir="$TEST_ROOT/invalid_manifest_sandbox" local scenario_dir="$TEST_ROOT/invalid_manifest_sandbox"
printf '==> invalid_manifest_sandbox\n' printf '==> invalid_manifest_sandbox\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_manifest_sandbox: expected rejection for sandbox mode" record_failure "invalid_manifest_sandbox: expected rejection for sandbox mode"
fi fi
} }
@@ -402,7 +402,7 @@ run_invalid_manifest_sandbox() {
run_invalid_manifest_packages() { run_invalid_manifest_packages() {
local scenario_dir="$TEST_ROOT/invalid_manifest_packages" local scenario_dir="$TEST_ROOT/invalid_manifest_packages"
printf '==> invalid_manifest_packages\n' printf '==> invalid_manifest_packages\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_manifest_packages: expected rejection for bad packages" record_failure "invalid_manifest_packages: expected rejection for bad packages"
fi fi
} }
@@ -410,11 +410,92 @@ run_invalid_manifest_packages() {
run_invalid_allow_host_network() { run_invalid_allow_host_network() {
local scenario_dir="$TEST_ROOT/invalid_allow_host_network" local scenario_dir="$TEST_ROOT/invalid_allow_host_network"
printf '==> invalid_allow_host_network\n' printf '==> invalid_allow_host_network\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_allow_host_network: expected rejection for invalid value" record_failure "invalid_allow_host_network: expected rejection for invalid value"
fi fi
} }
run_wizzard_create_manifest() {
local scenario_dir="$TEST_ROOT/wizzard_empty"
printf '==> wizzard_create_manifest\n'
if ! command -v script >/dev/null 2>&1; then
printf 'skipping wizzard_create_manifest: script binary not found in PATH\n'
return
fi
rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizzard" /dev/null >/dev/null 2>&1; then
record_failure "wizzard_create_manifest: wizzard failed"
return
fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_create_manifest: manifest not created"
return
fi
if ! grep -qx "name=wizzard_empty" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: name default mismatch"
fi
if ! grep -qx "packages_extra=" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: packages_extra mismatch"
fi
if ! grep -qx "codex_args=--sandbox danger-full-access --ask-for-approval never" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: codex_args mismatch"
fi
if ! grep -qx "allow_host_network=false" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: allow_host_network mismatch"
fi
}
run_wizzard_existing_defaults() {
local scenario_dir="$TEST_ROOT/wizzard_existing"
printf '==> wizzard_existing_defaults\n'
if ! command -v script >/dev/null 2>&1; then
printf 'skipping wizzard_existing_defaults: script binary not found in PATH\n'
return
fi
local input=$'\n\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizzard" /dev/null >/dev/null 2>&1; then
record_failure "wizzard_existing_defaults: wizzard failed"
return
fi
if ! grep -qx "name=custom-wizzard" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: name not preserved"
fi
if ! grep -qx "packages_extra=make git" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: packages_extra not preserved"
fi
if ! grep -qx "codex_args=--sandbox workspace-write --ask-for-approval on-request" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: codex_args not preserved"
fi
if ! grep -qx "allow_host_network=true" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: allow_host_network not preserved"
fi
}
run_wizzard_build_trigger() {
local scenario_dir="$TEST_ROOT/wizzard_build"
printf '==> wizzard_build_trigger\n'
if ! command -v script >/dev/null 2>&1; then
printf 'skipping wizzard_build_trigger: script binary not found in PATH\n'
return
fi
setup_stub_env
rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "env PATH=\"$STUB_BIN:$PATH\" HOME=\"$STUB_HOME\" FAKE_PODMAN_LOG=\"$STUB_LOG\" FAKE_PODMAN_INSPECT_FAIL=1 \"$SLOPTRAP_BIN\" \"$scenario_dir\"" /dev/null >/dev/null 2>&1; then
record_failure "wizzard_build_trigger: sloptrap failed"
teardown_stub_env
return
fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_build_trigger: manifest not created"
fi
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then
record_failure "wizzard_build_trigger: build not invoked after wizard"
fi
teardown_stub_env
}
run_shellcheck run_shellcheck
run_mount_injection run_mount_injection
run_root_target run_root_target
@@ -433,6 +514,9 @@ run_invalid_manifest_name
run_invalid_manifest_sandbox run_invalid_manifest_sandbox
run_invalid_manifest_packages run_invalid_manifest_packages
run_invalid_allow_host_network run_invalid_allow_host_network
run_wizzard_create_manifest
run_wizzard_existing_defaults
run_wizzard_build_trigger
if [[ ${#failures[@]} -gt 0 ]]; then if [[ ${#failures[@]} -gt 0 ]]; then
printf '\nTest failures:\n' printf '\nTest failures:\n'

View File

@@ -0,0 +1,4 @@
name=wizzard_build
packages_extra=
codex_args=--sandbox danger-full-access --ask-for-approval never
allow_host_network=false

View File

@@ -0,0 +1,4 @@
name=wizzard_empty
packages_extra=
codex_args=--sandbox danger-full-access --ask-for-approval never
allow_host_network=false

View File

@@ -0,0 +1,4 @@
name=custom-wizzard
packages_extra=make git
codex_args=--sandbox workspace-write --ask-for-approval on-request
allow_host_network=true