Add a wizzard to configure .sloptrap files
This commit is contained in:
13
.sloptrap
13
.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
|
||||
|
||||
13
Makefile
13
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)'
|
||||
|
||||
17
README.md
17
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 <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`. |
|
||||
| `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. |
|
||||
|
||||
|
||||
263
sloptrap
263
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 <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))
|
||||
|
||||
@@ -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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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 >/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'
|
||||
|
||||
4
tests/wizzard_build/.sloptrap
Normal file
4
tests/wizzard_build/.sloptrap
Normal file
@@ -0,0 +1,4 @@
|
||||
name=wizzard_build
|
||||
packages_extra=
|
||||
codex_args=--sandbox danger-full-access --ask-for-approval never
|
||||
allow_host_network=false
|
||||
4
tests/wizzard_empty/.sloptrap
Normal file
4
tests/wizzard_empty/.sloptrap
Normal file
@@ -0,0 +1,4 @@
|
||||
name=wizzard_empty
|
||||
packages_extra=
|
||||
codex_args=--sandbox danger-full-access --ask-for-approval never
|
||||
allow_host_network=false
|
||||
4
tests/wizzard_existing/.sloptrap
Normal file
4
tests/wizzard_existing/.sloptrap
Normal 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
|
||||
Reference in New Issue
Block a user