From 046b56e3f65f211301090cb528edd058df490500 Mon Sep 17 00:00:00 2001 From: Samuel Aubertin Date: Sat, 24 Jan 2026 17:44:47 +0100 Subject: [PATCH] Add a wizzard to configure .sloptrap files --- .sloptrap | 13 +- Makefile | 13 +- README.md | 17 +- sloptrap | 263 ++++++++++++++++++++++++++++--- tests/run_tests.sh | 120 +++++++++++--- tests/wizzard_build/.sloptrap | 4 + tests/wizzard_empty/.sloptrap | 4 + tests/wizzard_existing/.sloptrap | 4 + 8 files changed, 365 insertions(+), 73 deletions(-) create mode 100644 tests/wizzard_build/.sloptrap create mode 100644 tests/wizzard_empty/.sloptrap create mode 100644 tests/wizzard_existing/.sloptrap diff --git a/.sloptrap b/.sloptrap index e6b1ced..54d552a 100644 --- a/.sloptrap +++ b/.sloptrap @@ -1,15 +1,4 @@ -# Example Sloptrap manifest. -# Project identifier used for container/image naming. 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 - -# Additional Codex CLI switches appended when launching Codex. codex_args=--sandbox workspace-write - -# Allow the container host to be reachable from within -# allow_host_network=false +allow_host_network=false diff --git a/Makefile b/Makefile index 4653b8e..04fbe46 100644 --- a/Makefile +++ b/Makefile @@ -30,17 +30,8 @@ install update: $(PROGRAM) @install -Dm755 $(PROGRAM) "$(INSTALL_PATH)" @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%bExample files to configure your project:%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 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)' + @printf '%b%bConfigure your project with the wizard:%b\n' '$(PREFIX_TEXT)' '$(COLOR_TEXT)' '$(RESET)' + @printf '%b%b sloptrap /path/to/project wizzard%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)' uninstall: @printf '%b%bRemoving%b %b%s%b\n' '$(PREFIX_COMMENT)' '$(COLOR_TEXT)' '$(COLOR_TEXT)' '$(COLOR_COMMENT)' '$(INSTALL_PATH)' '$(RESET)' diff --git a/README.md b/README.md index e27ad51..80fb71d 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,8 @@ brew install coreutils gnu-tar jq ```bash cat > path/to/project/.sloptrap <<'EOF' name=path/to/project - default_targets=run packages_extra=make - codex_args=--sandbox workspace-write + codex_args=--sandbox danger-full-access --ask-for-approval never 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: - `name = basename(project directory)` -- `default_targets = run` - `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: | Key | Default | Notes | | --- | --- | --- | | `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 `+.-`. | -| `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. | -`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. @@ -93,7 +91,7 @@ Options: 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. - 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. @@ -107,7 +105,7 @@ Behaviour: ## 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 | | --- | --- | @@ -118,6 +116,7 @@ Targets are supplied after the code directory (or via `default_targets` in the m | `resume ` | Continues a Codex session by running `codex resume ` inside the container (builds if needed). | | `login` | Starts Codex in login mode to bootstrap `${HOME}/.codex`. | | `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). | | `clean` | Removes `.sloptrap-ignores`, deletes the container/image, and stops the container if necessary. | diff --git a/sloptrap b/sloptrap index 9cee9a0..15817b0 100755 --- a/sloptrap +++ b/sloptrap @@ -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 Build if needed, then run 'codex resume '\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" <= ${#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 ' (workspace-write or workspace-read-only)" + error "$MANIFEST_PATH: codex_args must include '--sandbox ' (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)) diff --git a/tests/run_tests.sh b/tests/run_tests.sh index d58b1db..c5997b7 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -208,7 +208,7 @@ run_mount_injection() { setup_stub_env rm -rf "$scenario_dir/.sloptrap-ignores" 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 2>&1; then record_failure "mount_injection: sloptrap exited non-zero" teardown_stub_env return @@ -233,7 +233,7 @@ run_mount_injection() { run_root_target() { local scenario_dir="$TEST_ROOT/root_target" 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 2>&1; then record_failure "root_target: expected rejection for project-root mask" return fi @@ -244,7 +244,7 @@ run_symlink_escape() { printf '==> symlink_escape\n' local secret_path="$ROOT_DIR/secrets.txt" touch "$secret_path" - if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then + if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" /dev/null 2>&1; then record_failure "symlink_escape: expected failure for symlink escape" rm -f "$secret_path" return @@ -255,7 +255,7 @@ run_symlink_escape() { run_manifest_injection() { local scenario_dir="$TEST_ROOT/manifest_injection" 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 2>&1; then record_failure "manifest_injection: expected rejection of bad make override" return fi @@ -264,10 +264,10 @@ run_manifest_injection() { run_helper_symlink() { local scenario_dir="$TEST_ROOT/helper_symlink" 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 2>&1; then record_failure "helper_symlink: expected rejection when helper directory is a symlink" fi - if "$SLOPTRAP_BIN" "$scenario_dir" clean >/dev/null 2>&1; then + if "$SLOPTRAP_BIN" "$scenario_dir" clean /dev/null 2>&1; then record_failure "helper_symlink: expected rejection for clean when helper directory is a symlink" fi } @@ -281,7 +281,7 @@ run_secret_mask() { FAKE_PODMAN_INSPECT_FAIL=1 SECRET_MASK_VERIFY=1 \ SECRET_MASK_EXPECTED_TARGET="${custom_workdir}/secret.txt" \ SLOPTRAP_WORKDIR="$custom_workdir" \ - "$SLOPTRAP_BIN" "$scenario_dir" >/dev/null 2>&1; then + "$SLOPTRAP_BIN" "$scenario_dir" /dev/null 2>&1; then record_failure "secret_mask: masking check failed" teardown_stub_env return @@ -295,7 +295,7 @@ run_resume_target() { setup_stub_env local session_id="019a81b7-32d2-7622-8639-6698c6579625" 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 2>&1; then record_failure "resume_target: sloptrap exited non-zero" teardown_stub_env return @@ -313,7 +313,7 @@ run_codex_symlink_home() { local tmp_home tmp_home=$(mktemp -d) 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 2>&1; then record_failure "codex_symlink_home: expected rejection when ~/.codex is a symlink" fi rm -rf "$tmp_home" @@ -323,7 +323,7 @@ run_root_directory_project() { printf '==> root_directory_project\n' local tmp_home 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 2>&1; then record_failure "root_directory_project: expected rejection for '/' project root" fi rm -rf "$tmp_home" @@ -338,7 +338,7 @@ run_shared_dir_override() { bogus_shared=$(mktemp -d) if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \ 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 2>&1; then record_failure "shared_dir_override: sloptrap exited non-zero" teardown_stub_env rm -rf "$bogus_shared" @@ -361,7 +361,7 @@ run_packages_env_validation() { local tmp_home tmp_home=$(mktemp -d) 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 2>&1; then record_failure "packages_env_validation: expected rejection of invalid SLOPTRAP_PACKAGES" fi rm -rf "$tmp_home" @@ -370,7 +370,7 @@ run_packages_env_validation() { run_abs_path_ignore() { local scenario_dir="$TEST_ROOT/abs_path_ignore" 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 2>&1; then record_failure "abs_path_ignore: expected rejection for anchored parent traversal entry" fi } @@ -378,7 +378,7 @@ run_abs_path_ignore() { run_dotdot_ignore() { local scenario_dir="$TEST_ROOT/dotdot_ignore" 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 2>&1; then record_failure "dotdot_ignore: expected rejection for parent traversal entry" fi } @@ -386,7 +386,7 @@ run_dotdot_ignore() { run_invalid_manifest_name() { local scenario_dir="$TEST_ROOT/invalid_manifest_name" 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 2>&1; then record_failure "invalid_manifest_name: expected rejection for illegal name" fi } @@ -394,7 +394,7 @@ run_invalid_manifest_name() { run_invalid_manifest_sandbox() { local scenario_dir="$TEST_ROOT/invalid_manifest_sandbox" 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 2>&1; then record_failure "invalid_manifest_sandbox: expected rejection for sandbox mode" fi } @@ -402,7 +402,7 @@ run_invalid_manifest_sandbox() { run_invalid_manifest_packages() { local scenario_dir="$TEST_ROOT/invalid_manifest_packages" 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 2>&1; then record_failure "invalid_manifest_packages: expected rejection for bad packages" fi } @@ -410,11 +410,92 @@ run_invalid_manifest_packages() { run_invalid_allow_host_network() { local scenario_dir="$TEST_ROOT/invalid_allow_host_network" 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 2>&1; then record_failure "invalid_allow_host_network: expected rejection for invalid value" 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_mount_injection run_root_target @@ -433,6 +514,9 @@ run_invalid_manifest_name run_invalid_manifest_sandbox run_invalid_manifest_packages run_invalid_allow_host_network +run_wizzard_create_manifest +run_wizzard_existing_defaults +run_wizzard_build_trigger if [[ ${#failures[@]} -gt 0 ]]; then printf '\nTest failures:\n' diff --git a/tests/wizzard_build/.sloptrap b/tests/wizzard_build/.sloptrap new file mode 100644 index 0000000..e00e934 --- /dev/null +++ b/tests/wizzard_build/.sloptrap @@ -0,0 +1,4 @@ +name=wizzard_build +packages_extra= +codex_args=--sandbox danger-full-access --ask-for-approval never +allow_host_network=false diff --git a/tests/wizzard_empty/.sloptrap b/tests/wizzard_empty/.sloptrap new file mode 100644 index 0000000..85faf3a --- /dev/null +++ b/tests/wizzard_empty/.sloptrap @@ -0,0 +1,4 @@ +name=wizzard_empty +packages_extra= +codex_args=--sandbox danger-full-access --ask-for-approval never +allow_host_network=false diff --git a/tests/wizzard_existing/.sloptrap b/tests/wizzard_existing/.sloptrap new file mode 100644 index 0000000..777d348 --- /dev/null +++ b/tests/wizzard_existing/.sloptrap @@ -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