Compare commits

..

10 Commits

Author SHA1 Message Date
Samuel Aubertin
87d1577546 Cleanup capabilities 2026-03-10 16:51:17 +01:00
Samuel Aubertin
b080f06613 cleanup 2026-03-10 11:03:16 +01:00
Samuel Aubertin
fb3aca0f7d Remove nested-podman 2026-03-10 11:02:44 +01:00
Samuel Aubertin
438959ca03 Try to inject the parent image in slopslopstrap 2026-03-10 01:22:58 +01:00
Samuel Aubertin
3731094f60 debugging recursion 2026-03-10 00:42:19 +01:00
Samuel Aubertin
1e97baa534 Fix capabilities 2026-03-09 21:34:06 +01:00
Samuel Aubertin
87a23e9772 Self contained sloptrap with helpers within 2026-03-09 19:23:21 +01:00
Samuel Aubertin
0ad137c6dc Auto-enable trusted sloptrap capabilities and harden bash launcher 2026-03-09 19:06:36 +01:00
Samuel Aubertin
da001da48f Add capabilities at build 2026-03-09 18:46:36 +01:00
Samuel Aubertin
47c3c979e5 Split /codex mount per project 2026-03-09 13:49:06 +01:00
17 changed files with 2078 additions and 195 deletions

View File

@@ -1,4 +1,4 @@
name=sloptrap name=skz-sloptrap
packages_extra=make shellcheck jq packages_extra=bash make shellcheck jq podman iproute2 strace
codex_args=--sandbox workspace-write capabilities=
allow_host_network=false allow_host_network=false

22
AGENTS.md Normal file
View File

@@ -0,0 +1,22 @@
# skz-sloptrap
This workspace is the source code of `sloptrap`, eg. your own runtime.
Your author, 'sk4nz', has ran the command `sloptrap path/to/skz-sloptrap` and you are living in your own reflection.
# Keep Memory
Future sloptrap instances should add short, useful, durable notes they learn during their session to this file.
Only add high-signal information that is likely to help later runs.
Keep additions brief.
Do not remove existing instructions unless they are outdated or wrong.
---
# Session Memory
- When runtime changes need a fresh sloptrap session to validate, ask sk4nz to restart you and give an exact prompt to continue from the current checkpoint.
- Useful checks in the skz-sloptrap repo:
`shellcheck sloptrap`
`bash tests/run_tests.sh` (you can also run them separately)
- When running tests from inside sloptrap, inherited `CODEX_HOME=/codex` plus `SLOPTRAP_PREFER_CODEX_HOME=1` can leak into host-style child launches; ignore that preference when `HOME` has been redirected elsewhere and the runtime hints still point into the inherited `/codex` tree.
- Capability-enabled runs are Podman-only. `packet-capture` uses a dedicated helper container/pod, and host-network capture must prompt for an explicit acknowledgement on every runtime launch.

View File

@@ -31,7 +31,7 @@ install update: $(PROGRAM)
@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%bConfigure your project with the wizard:%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 sloptrap /path/to/project wizzard%b\n' '$(PREFIX_COMMENT)' '$(COLOR_COMMENT)' '$(RESET)' @printf '%b%b sloptrap /path/to/project wizard%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

@@ -19,13 +19,12 @@ brew install coreutils gnu-tar jq
## Quick Start ## Quick Start
1. Place `sloptrap` somewhere on your PATH/shared drive (the helper Dockerfile and Codex binary are bundled and downloaded automatically). 1. Place `sloptrap` somewhere on your PATH/shared drive, for example with `make install` (the helper payload, helper Dockerfile, and Codex binary handling are bundled into the launcher).
2. (Optional) Create a project-specific manifest and ignore file: 2. (Optional) Create a project-specific manifest and ignore file:
```bash ```bash
cat > path/to/project/.sloptrap <<'EOF' cat > path/to/project/.sloptrap <<'EOF'
name=path/to/project name=path/to/project
packages_extra=make packages_extra=make
codex_args=--sandbox danger-full-access --ask-for-approval never
EOF EOF
cat > path/to/project/.sloptrapignore <<'EOF' cat > path/to/project/.sloptrapignore <<'EOF'
@@ -36,13 +35,13 @@ brew install coreutils gnu-tar jq
3. Run `./sloptrap path/to/project`. On the first invocation sloptrap: 3. Run `./sloptrap path/to/project`. On the first invocation sloptrap:
- builds `path/to/project-sloptrap-image` if missing, - builds `path/to/project-sloptrap-image` if missing,
- verifies the Codex binary hash, - verifies the Codex binary hash,
- creates `${HOME}/.codex` and runs `login` if credentials are absent. - creates `${HOME}/.codex`, prepares a per-project state directory, and runs `login` if `${HOME}/.codex/auth.json` is missing or empty.
> Use `./sloptrap path/to/project shell` to enter a troubleshooting shell inside the container or `./sloptrap path/to/project clean` to remove cached images and state. > Use `./sloptrap path/to/project shell` to enter a troubleshooting shell inside the container or `./sloptrap path/to/project clean` to remove cached images and state.
## How It Works ## How It Works
- The project directory mounts at `/workspace`, and `${HOME}/.codex` mounts at `/codex`. - The project directory mounts at `/workspace`; project-scoped Codex state mounts at `/codex` from `${HOME}/.codex/sloptrap/state/<project-hash>`, and shared auth mounts from `${HOME}/.codex/auth.json` to `/codex/auth.json`.
- `.sloptrapignore` entries (if present in your project) are overlaid by tmpfs (for directories) or empty bind mounts (for files) so Codex cannot read the masked content. - `.sloptrapignore` entries (if present in your project) are overlaid by tmpfs (for directories) or empty bind mounts (for files) so Codex cannot read the masked content.
- sloptrap launches containers on an isolated network (`bridge` on Docker, `slirp4netns` on Podman) with `--cap-drop=ALL`, `--security-opt no-new-privileges`, a read-only root filesystem, and tmpfs-backed `/tmp`, `/run`, and `/run/lock`. Projects that explicitly set `allow_host_network=true` in their manifest opt into `--network host`. - sloptrap launches containers on an isolated network (`bridge` on Docker, `slirp4netns` on Podman) with `--cap-drop=ALL`, `--security-opt no-new-privileges`, a read-only root filesystem, and tmpfs-backed `/tmp`, `/run`, and `/run/lock`. Projects that explicitly set `allow_host_network=true` in their manifest opt into `--network host`.
- The helper Dockerfile is embedded inside `sloptrap`; set `SLOPTRAP_DOCKERFILE_PATH=/path/to/custom/Dockerfile` if you need to supply your own recipe. The default image installs `curl`, `bash`, `ca-certificates`, `libstdc++6`, `git`, `ripgrep`, `xxd`, and `file`, so most debugging helpers are already available without adding `packages_extra`. - The helper Dockerfile is embedded inside `sloptrap`; set `SLOPTRAP_DOCKERFILE_PATH=/path/to/custom/Dockerfile` if you need to supply your own recipe. The default image installs `curl`, `bash`, `ca-certificates`, `libstdc++6`, `git`, `ripgrep`, `xxd`, and `file`, so most debugging helpers are already available without adding `packages_extra`.
@@ -54,7 +53,7 @@ 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)`
- `packages_extra = ""` (none) - `packages_extra = ""` (none)
- `codex_args = "--sandbox danger-full-access --ask-for-approval never"` - `capabilities = ""` (none)
If a build is requested and no `.sloptrap` exists, sloptrap prompts to create one interactively. 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:
@@ -63,11 +62,13 @@ Supported keys when the manifest is present:
| --- | --- | --- | | --- | --- | --- |
| `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. |
| `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 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`). | | `capabilities` | *empty* | Optional privileged features. Supported values are `apt-install` and `packet-capture`. Capability-enabled runs require Podman. When `packet-capture` is combined with `allow_host_network=true`, sloptrap shows a runtime warning with concrete consequences and requires an interactive acknowledgement on every run. |
| `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`, `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.
sloptrap always runs Codex with `--sandbox danger-full-access --ask-for-approval never`. `codex_args` is deprecated and rejected if present.
Capability trust is local state, not part of the repository. Builds for manifests that request capabilities require either an interactive trust confirmation or `--trust-capabilities`. Once the current manifest is trusted, its requested capabilities are enabled automatically for that project configuration. The `allow_host_network=true` plus `packet-capture` combination still requires a separate interactive acknowledgement each time a runtime container is launched.
### `.sloptrapignore` ### `.sloptrapignore`
@@ -79,13 +80,14 @@ Values containing `$`, `` ` ``, or newlines are rejected to prevent command inje
## CLI Reference ## CLI Reference
``` ```
./sloptrap [--dry-run] [--print-config] <code-directory> [target ...] ./sloptrap [--dry-run] [--print-config] [--trust-capabilities] <code-directory> [target ...]
``` ```
Options: Options:
- `--dry-run` &mdash; print the container/engine commands that would run without executing them. - `--dry-run` &mdash; print the container/engine commands that would run without executing them.
- `--print-config` &mdash; output the resolved manifest values, defaults, and ignore list. - `--print-config` &mdash; output the resolved manifest values, defaults, and ignore list.
- `--trust-capabilities` &mdash; trust the manifest's requested capabilities for the current build flow.
- `-h, --help` &mdash; display usage. - `-h, --help` &mdash; display usage.
- `--` &mdash; stop option parsing; remaining arguments are treated as targets. - `--` &mdash; stop option parsing; remaining arguments are treated as targets.
@@ -93,10 +95,11 @@ Behaviour:
- 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. - 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 or empty, sloptrap prepends a login run before executing your targets.
- Fresh interactive `run` sessions receive a launcher-generated startup prompt telling the agent it is inside sloptrap, summarising the resolved manifest/runtime state, and pointing it at `/workspace/.sloptrap` for exact project configuration. `resume` does not inject that prompt again.
- 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.
`--print-config` fields include `manifest_present=true|false`, resolved paths, and the sanitised ignore mount roots so you can confirm what will be hidden inside the container. `--print-config` fields include `manifest_present=true|false`, requested/enabled capability lists, trust status, resolved paths, and the sanitised ignore mount roots so you can confirm what will be hidden inside the container.
### Regression Suite ### Regression Suite
@@ -112,34 +115,41 @@ Targets are supplied after the code directory. When omitted, sloptrap defaults t
| `build` | Download Codex (if missing), verify SHA-256, and build the container image. | | `build` | Download Codex (if missing), verify SHA-256, and build the container image. |
| `build-if-missing` | No-op when the image already exists; otherwise delegates to `build`. | | `build-if-missing` | No-op when the image already exists; otherwise delegates to `build`. |
| `rebuild` | Rebuild the image from scratch (`--no-cache`). | | `rebuild` | Rebuild the image from scratch (`--no-cache`). |
| `run` | Default goal. Runs the container with Codex as entrypoint and passes `codex_args`. | | `run` | Default goal. Runs the container with Codex using sloptrap's built-in runtime flags. |
| `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 shared `${HOME}/.codex/auth.json` credentials. |
| `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. | | `wizard` | 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. |
The launcher executes targets sequentially, so `./sloptrap repo build run` performs an explicit rebuild before invoking Codex. Extra targets may be added in the future; unknown names fail fast. The launcher executes targets sequentially, so `./sloptrap repo build run` performs an explicit rebuild before invoking Codex. Extra targets may be added in the future; unknown names fail fast.
### Capability Helpers
When the current manifest's capabilities are trusted and enabled, the container includes helper commands:
- `slop-apt install <package...>` for session-scoped package installation.
- `slopcap capture --interface <iface> [--filter <expr>] [--output <path>] [--stdout]` for non-promiscuous packet capture through a dedicated helper container. Host-network captures require an explicit acknowledgement each run.
## Execution Environment ## Execution Environment
- Container engine: Podman or podman with identical command lines. Podman uses `--userns=keep-id`; Docker receives the equivalent `--user UID:GID`. - Container engine: Podman or Docker for standard runs. Capability-enabled runs require Podman. Podman uses `--userns=keep-id`; Docker receives the equivalent `--user UID:GID` for standard runs.
- Filesystem view: the project directory mounts at `/workspace`; `${HOME}/.codex` mounts at `/codex`. - Filesystem view: the project directory mounts at `/workspace`; `${HOME}/.codex/sloptrap/state/<project-hash>` mounts at `/codex`; `${HOME}/.codex/auth.json` mounts at `/codex/auth.json`.
- Ignore filter: `.sloptrapignore` entries are overlaid with tmpfs directories or empty bind mounts so data remains unavailable to Codex. - Ignore filter: `.sloptrapignore` entries are overlaid with tmpfs directories or empty bind mounts so data remains unavailable to Codex.
- Network: the container always runs with `--network host`. sloptrap does not filter or proxy outbound traffic. - Network: isolated networking is used by default; `allow_host_network=true` opts into `--network host`. When `packet-capture` is enabled, sloptrap starts a separate capture helper container in the same Podman pod so the main Codex container does not receive `NET_RAW`.
- Process context: capabilities are dropped, `no-new-privileges` is set, the root filesystem is read-only, and scratch paths (`/tmp`, `/run`, `/run/lock`) are tmpfs mounts. Resource limits follow the launcher defaults. - Process context: standard runs drop capabilities, set `no-new-privileges`, use a read-only root filesystem, and keep scratch paths (`/tmp`, `/run`, `/run/lock`) on tmpfs. Capability-enabled runs may selectively add the runtime options required for the requested capability.
- Codex configuration: runtime flags come from `codex_args`. Persistent Codex state is stored under `${HOME}/.codex`. - Codex configuration: runtime flags are fixed to `--sandbox danger-full-access --ask-for-approval never`. Persistent Codex state is project-scoped under `${HOME}/.codex/sloptrap/state/`, while credentials are shared via `${HOME}/.codex/auth.json` and mounted read-only except during the `login` target.
## Threat Model and Limits ## Threat Model and Limits
- **Outbound disclosure**: prompts and referenced data travel from the container to the configured LLM endpoint. Any file content within `/workspace` or environment data exposed to the process can appear in that traffic. - **Outbound disclosure**: prompts and referenced data travel from the container to the configured LLM endpoint. Any file content within `/workspace` or environment data exposed to the process can appear in that traffic.
- **Shared storage**: `/workspace` and `/codex` are the only host mounts. Files written to these locations become visible on the host and to the LLM provider through prompts. - **Shared storage**: `/workspace`, project-scoped `/codex`, and `/codex/auth.json` are host mounts. Files written to these locations become visible on the host and to the LLM provider through prompts.
- **Environment surface**: the container receives a minimal fixed environment (HOME/XDG paths, `CODEX_HOME`). The manifest no longer allows injecting additional environment variables. - **Environment surface**: the container receives a minimal fixed environment (HOME/XDG paths, `CODEX_HOME`). The manifest no longer allows injecting additional environment variables.
- **Process isolation**: the container runs without additional Linux capabilities and with a read-only root filesystem. The container and host still share the same kernel; a kernel-level escape would affect host confidentiality. - **Process isolation**: standard runs keep a read-only root filesystem and no extra Linux capabilities. Capability-enabled runs deliberately relax specific runtime controls for the enabled feature, so they should be treated as a stronger trust decision than a default session. `packet-capture` now runs in a dedicated helper container so the main Codex container does not hold raw-socket capability.
- **Networking stance**: traffic is unrestricted once it leaves the container. sloptrap does not enforce an allowlist or DNS policy, and `--network host` is always used because the bundled Codex CLI must reach an upstream LLM provider. If you require an offline or firewalled workflow, sloptrap is not an appropriate launcher. - **Networking stance**: traffic is unrestricted once it leaves the container. sloptrap does not enforce an allowlist or DNS policy. Host networking is opt-in per manifest; when it is combined with `packet-capture`, sloptrap warns and requires an explicit acknowledgement for each runtime launch because the capture helper will have raw packet access in the host namespace and may observe plaintext traffic or inject spoofed packets. If you require an offline or firewalled workflow, sloptrap is not an appropriate launcher.
- **Persistence**: Codex history and logs accumulate under `${HOME}/.codex`. Sensitive prompts recorded on disk remain on the host after the session. Because `.git/` is ignored inside the container, any historical secrets in Git objects stay outside the LLM context unless explicitly surfaced in the working tree. - **Persistence**: Codex history and logs accumulate per project under `${HOME}/.codex/sloptrap/state/`. Sensitive prompts recorded on disk remain on the host after the session. Because `.git/` is ignored inside the container, any historical secrets in Git objects stay outside the LLM context unless explicitly surfaced in the working tree.
- **Codex cache hygiene**: the `${HOME}/.codex` mount remains writable by the container and will hold tokens, cached prompts, and other state. Rotate credentials regularly and avoid co-locating unrelated secrets inside that directory. - **Codex cache hygiene**: per-project state mounts remain writable by the container and hold prompts/history/state, while `${HOME}/.codex/auth.json` holds shared credentials. Rotate credentials regularly and protect both locations.
- **Secret scanning**: sloptrap does not perform secret discovery or redaction; any credentials present in the project remain available to Codex and the upstream provider. - **Secret scanning**: sloptrap does not perform secret discovery or redaction; any credentials present in the project remain available to Codex and the upstream provider.
- **Local model exception**: pointing Codex at a local or self-hosted model keeps data within the host network boundary, but the filesystem and environment exposure described above is unchanged. - **Local model exception**: pointing Codex at a local or self-hosted model keeps data within the host network boundary, but the filesystem and environment exposure described above is unchanged.

1348
sloptrap

File diff suppressed because it is too large Load Diff

View File

@@ -11,3 +11,7 @@ Current scenarios:
- `helper_symlink/` — ensures `.sloptrap-ignores` cannot be a symlink to directories outside the project. - `helper_symlink/` — ensures `.sloptrap-ignores` cannot be a symlink to directories outside the project.
- `secret_mask/` — verifies masked files remain hidden even when sloptrap remaps the workspace mount. - `secret_mask/` — verifies masked files remain hidden even when sloptrap remaps the workspace mount.
- `resume_target/` — verifies the resume target passes the requested session identifier to Codex. - `resume_target/` — verifies the resume target passes the requested session identifier to Codex.
- `auth_file_mount` — verifies `~/.codex/auth.json` is mounted directly into `/codex/auth.json`.
- `project_state_isolation` — verifies different projects map `/codex` to different host state directories.
- `auto_login_empty_auth` — verifies an empty `auth.json` still triggers automatic login before the main target.
- `host_network_packet_capture/` — exercises the per-run acknowledgement path for host networking combined with `packet-capture`.

View File

@@ -0,0 +1,3 @@
name=capability-repo
capabilities=apt-install packet-capture
allow_host_network=false

View File

@@ -0,0 +1,3 @@
name=host-network-packet-capture
capabilities=packet-capture
allow_host_network=true

View File

@@ -0,0 +1,3 @@
name=host-network-repo
capabilities=apt-install
allow_host_network=true

View File

@@ -0,0 +1,4 @@
name=invalid-capabilities
capabilities=packet-capture not-a-real-capability
codex_args=--sandbox workspace-write --ask-for-approval never
allow_host_network=false

View File

@@ -15,6 +15,16 @@ fi
failures=() failures=()
can_run_script_pty() {
if ! command -v script >/dev/null 2>&1; then
return 1
fi
if ! script -q -c "true" /dev/null >/dev/null 2>&1; then
return 1
fi
return 0
}
run_shellcheck() { run_shellcheck() {
printf '==> shellcheck\n' printf '==> shellcheck\n'
if ! command -v shellcheck >/dev/null 2>&1; then if ! command -v shellcheck >/dev/null 2>&1; then
@@ -87,7 +97,52 @@ verify_secret_mounts() {
return 0 return 0
} }
maybe_create_helper_pidfile() {
local -a args=("$@")
local codex_source=""
local helper_dir=""
local idx=0
while (( idx < ${#args[@]} )); do
local arg=${args[$idx]}
case "$arg" in
-v)
idx=$((idx + 1))
if (( idx < ${#args[@]} )); then
local spec=${args[$idx]}
case "$spec" in
*:/codex|*:/codex:* )
codex_source=${spec%%:/codex*}
;;
esac
fi
;;
-e)
idx=$((idx + 1))
if (( idx < ${#args[@]} )); then
local envspec=${args[$idx]}
case "$envspec" in
SLOPTRAP_HELPER_DIR=*)
helper_dir=${envspec#SLOPTRAP_HELPER_DIR=}
;;
esac
fi
;;
esac
idx=$((idx + 1))
done
if [[ -z $codex_source || $helper_dir != /codex/* ]]; then
return 0
fi
local helper_host=${codex_source}/${helper_dir#/codex/}
mkdir -p "$helper_host"
printf '12345\n' >"$helper_host/helperd.pid"
}
if [[ ${1-} == "image" && ${2-} == "inspect" && ${FAKE_PODMAN_INSPECT_FAIL:-0} == 1 ]]; then if [[ ${1-} == "image" && ${2-} == "inspect" && ${FAKE_PODMAN_INSPECT_FAIL:-0} == 1 ]]; then
if [[ " $* " == *" --format "* ]]; then
printf 'fake-image-id\n'
exit 0
fi
echo "FAKE PODMAN (fail): $*" >>"$FAKE_PODMAN_LOG" echo "FAKE PODMAN (fail): $*" >>"$FAKE_PODMAN_LOG"
exit 1 exit 1
fi fi
@@ -99,10 +154,16 @@ if [[ ${SECRET_MASK_VERIFY:-0} == 1 && ${1-} == "run" ]]; then
fi fi
fi fi
if [[ ${1-} == "run" ]]; then
maybe_create_helper_pidfile "$@"
fi
echo "FAKE PODMAN: $*" >>"$FAKE_PODMAN_LOG" echo "FAKE PODMAN: $*" >>"$FAKE_PODMAN_LOG"
exit 0 exit 0
EOF EOF
chmod +x "$STUB_BIN/podman" chmod +x "$STUB_BIN/podman"
cp "$STUB_BIN/podman" "$STUB_BIN/docker"
chmod +x "$STUB_BIN/docker"
cat >"$STUB_BIN/curl" <<'EOF' cat >"$STUB_BIN/curl" <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
@@ -154,6 +215,9 @@ if [[ ${1-} == "-c" ]]; then
exit 0 exit 0
fi fi
fi fi
if [[ -x /usr/bin/sha256sum ]]; then
exec /usr/bin/sha256sum "$@"
fi
printf 'sha256sum stub encountered unsupported args: %s\n' "$*" >&2 printf 'sha256sum stub encountered unsupported args: %s\n' "$*" >&2
exit 1 exit 1
EOF EOF
@@ -198,6 +262,33 @@ teardown_stub_env() {
rm -f "${STUB_LOG:-}" rm -f "${STUB_LOG:-}"
} }
extract_embedded_helper() {
local helper=$1
local output=$2
if ! awk -v helper="$helper" '
$0 ~ "^[[:space:]]*" helper "\\)$" { state=1; next }
state == 1 && /cat <<'\''EOF'\''$/ { state=2; next }
state == 2 && /^EOF$/ { exit }
state == 2 { print }
' "$SLOPTRAP_BIN" >"$output"; then
return 1
fi
chmod +x "$output"
}
wait_for_path() {
local path=$1
local attempts=${2:-100}
while (( attempts > 0 )); do
if [[ -e $path ]]; then
return 0
fi
sleep 0.1
((attempts-=1))
done
return 1
}
record_failure() { record_failure() {
failures+=("$1") failures+=("$1")
} }
@@ -289,6 +380,22 @@ run_secret_mask() {
teardown_stub_env teardown_stub_env
} }
run_git_ignore_mask() {
local scenario_dir="$ROOT_DIR"
printf '==> git_ignore_mask\n'
setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "git_ignore_mask: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "--mount type=tmpfs,target=/workspace/.git" "$STUB_LOG"; then
record_failure "git_ignore_mask: .git was not masked with tmpfs"
fi
teardown_stub_env
}
run_resume_target() { run_resume_target() {
local scenario_dir="$TEST_ROOT/resume_target" local scenario_dir="$TEST_ROOT/resume_target"
printf '==> resume_target\n' printf '==> resume_target\n'
@@ -306,6 +413,213 @@ run_resume_target() {
teardown_stub_env teardown_stub_env
} }
run_runtime_context_prompt() {
local scenario_dir="$TEST_ROOT/host_network_repo"
printf '==> runtime_context_prompt\n'
setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "runtime_context_prompt: sloptrap exited non-zero"
teardown_stub_env
return
fi
local login_line run_line
login_line=$(grep "FAKE PODMAN: run " "$STUB_LOG" | head -n 1 || true)
run_line=$(grep "FAKE PODMAN: run " "$STUB_LOG" | tail -n 1 || true)
if [[ -z $run_line || $run_line != *"You are running inside sloptrap"* ]]; then
record_failure "runtime_context_prompt: startup prompt missing from fresh run"
fi
if ! grep -q -- "name=host-network-repo" "$STUB_LOG" \
|| ! grep -q -- "enabled_capabilities=apt-install" "$STUB_LOG" \
|| ! grep -q -- "network_mode=host" "$STUB_LOG"; then
record_failure "runtime_context_prompt: runtime summary missing manifest or capability state"
fi
if [[ -n $login_line && $login_line == *"You are running inside sloptrap"* ]]; then
record_failure "runtime_context_prompt: login flow should not receive startup prompt"
fi
teardown_stub_env
}
run_sh_reexec() {
local scenario_dir="$TEST_ROOT/host_network_repo"
printf '==> sh_reexec\n'
setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
sh "$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "sh_reexec: sloptrap exited non-zero when launched via sh"
teardown_stub_env
return
fi
if ! grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then
record_failure "sh_reexec: startup prompt missing after sh re-exec"
fi
teardown_stub_env
}
run_resume_omits_runtime_context() {
local scenario_dir="$TEST_ROOT/host_network_repo"
local session_id="019a81b7-32d2-7622-8639-6698c6579625"
printf '==> resume_omits_runtime_context\n'
setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" resume "$session_id" </dev/null >/dev/null 2>&1; then
record_failure "resume_omits_runtime_context: sloptrap exited non-zero"
teardown_stub_env
return
fi
if grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then
record_failure "resume_omits_runtime_context: resume should not receive startup prompt"
fi
if ! grep -q -- "codex --sandbox danger-full-access --ask-for-approval never resume $session_id" "$STUB_LOG"; then
record_failure "resume_omits_runtime_context: resume invocation missing"
fi
teardown_stub_env
}
run_auth_file_mount() {
local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
printf '==> auth_file_mount\n'
setup_stub_env
mkdir -p "$STUB_HOME/.codex"
printf '{"access_token":"test"}\n' >"$STUB_HOME/.codex/auth.json"
if ! 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 "auth_file_mount: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z,ro" "$STUB_LOG"; then
record_failure "auth_file_mount: auth file should be mounted read-only for normal runs"
fi
if ! grep -q -- "-v ${STUB_HOME}/.codex/sloptrap/state/" "$STUB_LOG"; then
record_failure "auth_file_mount: missing project state bind mount"
fi
teardown_stub_env
}
run_codex_home_override() {
local scenario_dir codex_root
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
printf '==> codex_home_override\n'
setup_stub_env
codex_root="$STUB_HOME/codex-root"
mkdir -p "$codex_root"
printf '{"access_token":"test"}\n' >"$codex_root/auth.json"
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" CODEX_HOME="$codex_root" SLOPTRAP_PREFER_CODEX_HOME=1 \
FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "codex_home_override: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "-v ${codex_root}/auth.json:/codex/auth.json:Z,ro" "$STUB_LOG"; then
record_failure "codex_home_override: CODEX_HOME auth file should be mounted read-only for normal runs"
fi
if ! grep -q -- "-v ${codex_root}/sloptrap/state/" "$STUB_LOG"; then
record_failure "codex_home_override: missing CODEX_HOME project state bind mount"
fi
if grep -q -- "-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then
record_failure "codex_home_override: should not fall back to HOME/.codex when CODEX_HOME is set"
fi
local first_run
first_run=$(grep "FAKE PODMAN: run " "$STUB_LOG" | head -n 1 || true)
if [[ -z $first_run || $first_run == *" login" ]]; then
record_failure "codex_home_override: existing CODEX_HOME auth should avoid login target"
fi
teardown_stub_env
}
run_removed_nested_podman_manifest() {
local scenario_dir output_log
scenario_dir=$(mktemp -d)
output_log=$(mktemp)
printf '==> removed_nested_podman_manifest\n'
cat >"$scenario_dir/.sloptrap" <<'EOF'
name=removed-nested-podman
capabilities=nested-podman
EOF
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then
record_failure "removed_nested_podman_manifest: expected nested-podman manifest rejection"
fi
if ! "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >"$output_log" 2>&1; then
if ! grep -q -- "capability 'nested-podman' was removed" "$output_log"; then
record_failure "removed_nested_podman_manifest: missing explicit removal error"
fi
fi
rm -f "$output_log"
rm -rf "$scenario_dir"
}
run_project_state_isolation() {
local scenario_a scenario_b
scenario_a=$(cd "$TEST_ROOT/resume_target" && pwd -P)
scenario_b=$(cd "$TEST_ROOT/secret_mask" && pwd -P)
printf '==> project_state_isolation\n'
setup_stub_env
mkdir -p "$STUB_HOME/.codex"
printf '{"access_token":"test"}\n' >"$STUB_HOME/.codex/auth.json"
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_a" </dev/null >/dev/null 2>&1; then
record_failure "project_state_isolation: first project run failed"
teardown_stub_env
return
fi
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \
"$SLOPTRAP_BIN" "$scenario_b" </dev/null >/dev/null 2>&1; then
record_failure "project_state_isolation: second project run failed"
teardown_stub_env
return
fi
local -a codex_mounts=()
mapfile -t codex_mounts < <(
{
grep "FAKE PODMAN: run " "$STUB_LOG" \
| grep -oE -- "-v [^ ]+:/codex:Z" \
| sed -e 's/^-v //' -e 's/:\/codex:Z$//'
} || true
)
if [[ ${#codex_mounts[@]} -lt 2 ]]; then
record_failure "project_state_isolation: missing /codex state mounts"
teardown_stub_env
return
fi
if [[ ${codex_mounts[0]} == "${codex_mounts[1]}" ]]; then
record_failure "project_state_isolation: projects reused same Codex state mount"
fi
if [[ ${codex_mounts[0]} != */.codex/sloptrap/state/* || ${codex_mounts[1]} != */.codex/sloptrap/state/* ]]; then
record_failure "project_state_isolation: state mounts did not use sloptrap namespace"
fi
teardown_stub_env
}
run_auto_login_empty_auth() {
local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
printf '==> auto_login_empty_auth\n'
setup_stub_env
mkdir -p "$STUB_HOME/.codex"
: >"$STUB_HOME/.codex/auth.json"
if ! 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 "auto_login_empty_auth: sloptrap exited non-zero"
teardown_stub_env
return
fi
local first_run
first_run=$(grep "FAKE PODMAN: run " "$STUB_LOG" | head -n 1 || true)
if [[ -z $first_run || $first_run != *" login" ]]; then
record_failure "auto_login_empty_auth: expected login before primary run"
fi
if [[ -z $first_run || $first_run != *"-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z "* ]]; then
record_failure "auto_login_empty_auth: login target should keep auth file writable"
fi
if ! grep -q -- "-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z,ro" "$STUB_LOG"; then
record_failure "auto_login_empty_auth: post-login runtime should remount auth file read-only"
fi
teardown_stub_env
}
run_codex_symlink_home() { run_codex_symlink_home() {
local scenario_dir local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P) scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
@@ -407,6 +721,14 @@ run_invalid_manifest_packages() {
fi fi
} }
run_invalid_manifest_capabilities() {
local scenario_dir="$TEST_ROOT/invalid_manifest_capabilities"
printf '==> invalid_manifest_capabilities\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_manifest_capabilities: expected rejection for bad capabilities"
fi
}
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'
@@ -415,87 +737,464 @@ run_invalid_allow_host_network() {
fi fi
} }
run_wizzard_create_manifest() { run_host_network_packet_capture_ack_required() {
local scenario_dir="$TEST_ROOT/wizzard_empty" local scenario_dir="$TEST_ROOT/host_network_packet_capture"
printf '==> wizzard_create_manifest\n' printf '==> host_network_packet_capture_ack_required\n'
if ! command -v script >/dev/null 2>&1; then local output_log
printf 'skipping wizzard_create_manifest: script binary not found in PATH\n' output_log=$(mktemp)
setup_stub_env
if PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >"$output_log" 2>&1; then
record_failure "host_network_packet_capture_ack_required: expected failure without interactive acknowledgement"
fi
if grep -q -- "FAKE PODMAN: run " "$STUB_LOG"; then
record_failure "host_network_packet_capture_ack_required: runtime container should not start without acknowledgement"
fi
teardown_stub_env
rm -f "$output_log"
}
run_host_network_packet_capture_ack_prompt() {
local scenario_dir="$TEST_ROOT/host_network_packet_capture"
printf '==> host_network_packet_capture_ack_prompt\n'
if ! can_run_script_pty; then
printf 'skipping host_network_packet_capture_ack_prompt: script PTY support not available\n'
return
fi
local output_log
output_log=$(mktemp)
setup_stub_env
if ! printf 'y\n' | script -q -c "env PATH=\"$STUB_BIN:$PATH\" HOME=\"$STUB_HOME\" FAKE_PODMAN_LOG=\"$STUB_LOG\" FAKE_PODMAN_INSPECT_FAIL=1 \"$SLOPTRAP_BIN\" --trust-capabilities \"$scenario_dir\"" "$output_log" >/dev/null 2>&1; then
record_failure "host_network_packet_capture_ack_prompt: interactive acknowledgement should allow the run"
teardown_stub_env
rm -f "$output_log"
return
fi
if [[ $(grep -c -- 'Continue with host-network packet capture for this run' "$output_log" || true) -ne 1 ]]; then
record_failure "host_network_packet_capture_ack_prompt: expected a single runtime acknowledgement prompt"
fi
if ! grep -q -- 'capture host-network traffic' "$output_log" \
|| ! grep -q -- 'transmit spoofed packets' "$output_log"; then
record_failure "host_network_packet_capture_ack_prompt: warning should describe concrete consequences"
fi
if ! grep -q -- "--network host" "$STUB_LOG"; then
record_failure "host_network_packet_capture_ack_prompt: host networking run did not reach the container engine"
fi
teardown_stub_env
rm -f "$output_log"
}
run_wizard_create_manifest() {
local scenario_dir="$TEST_ROOT/wizard_empty"
printf '==> wizard_create_manifest\n'
if ! can_run_script_pty; then
printf 'skipping wizard_create_manifest: script PTY support not available\n'
return return
fi fi
rm -f "$scenario_dir/.sloptrap" rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n\n' local input=$'\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizzard" /dev/null >/dev/null 2>&1; then if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then
record_failure "wizzard_create_manifest: wizzard failed" record_failure "wizard_create_manifest: wizard failed"
return return
fi fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_create_manifest: manifest not created" record_failure "wizard_create_manifest: manifest not created"
return return
fi fi
if ! grep -qx "name=wizzard_empty" "$scenario_dir/.sloptrap"; then if ! grep -qx "name=wizard_empty" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: name default mismatch" record_failure "wizard_create_manifest: name default mismatch"
fi fi
if ! grep -qx "packages_extra=" "$scenario_dir/.sloptrap"; then if ! grep -qx "packages_extra=" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: packages_extra mismatch" record_failure "wizard_create_manifest: packages_extra mismatch"
fi fi
if ! grep -qx "codex_args=--sandbox danger-full-access --ask-for-approval never" "$scenario_dir/.sloptrap"; then if ! grep -qx "capabilities=" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: codex_args mismatch" record_failure "wizard_create_manifest: capabilities mismatch"
fi fi
if ! grep -qx "allow_host_network=false" "$scenario_dir/.sloptrap"; then if ! grep -qx "allow_host_network=false" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_create_manifest: allow_host_network mismatch" record_failure "wizard_create_manifest: allow_host_network mismatch"
fi fi
} }
run_wizzard_existing_defaults() { run_wizard_existing_defaults() {
local scenario_dir="$TEST_ROOT/wizzard_existing" local scenario_dir="$TEST_ROOT/wizard_existing"
printf '==> wizzard_existing_defaults\n' printf '==> wizard_existing_defaults\n'
if ! command -v script >/dev/null 2>&1; then if ! can_run_script_pty; then
printf 'skipping wizzard_existing_defaults: script binary not found in PATH\n' printf 'skipping wizard_existing_defaults: script PTY support not available\n'
return return
fi fi
local input=$'\n\n\n\n\n' local input=$'\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizzard" /dev/null >/dev/null 2>&1; then if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then
record_failure "wizzard_existing_defaults: wizzard failed" record_failure "wizard_existing_defaults: wizard failed"
return return
fi fi
if ! grep -qx "name=custom-wizzard" "$scenario_dir/.sloptrap"; then if ! grep -qx "name=custom-wizard" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: name not preserved" record_failure "wizard_existing_defaults: name not preserved"
fi fi
if ! grep -qx "packages_extra=make git" "$scenario_dir/.sloptrap"; then if ! grep -qx "packages_extra=make git" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: packages_extra not preserved" record_failure "wizard_existing_defaults: packages_extra not preserved"
fi fi
if ! grep -qx "codex_args=--sandbox workspace-write --ask-for-approval on-request" "$scenario_dir/.sloptrap"; then if ! grep -qx "capabilities=apt-install" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: codex_args not preserved" record_failure "wizard_existing_defaults: capabilities not preserved"
fi fi
if ! grep -qx "allow_host_network=true" "$scenario_dir/.sloptrap"; then if ! grep -qx "allow_host_network=true" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: allow_host_network not preserved" record_failure "wizard_existing_defaults: allow_host_network not preserved"
fi fi
} }
run_wizzard_build_trigger() { run_wizard_build_trigger() {
local scenario_dir="$TEST_ROOT/wizzard_build" local scenario_dir="$TEST_ROOT/wizard_build"
printf '==> wizzard_build_trigger\n' printf '==> wizard_build_trigger\n'
if ! command -v script >/dev/null 2>&1; then if ! can_run_script_pty; then
printf 'skipping wizzard_build_trigger: script binary not found in PATH\n' printf 'skipping wizard_build_trigger: script PTY support not available\n'
return return
fi fi
setup_stub_env setup_stub_env
rm -f "$scenario_dir/.sloptrap" rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n\n' local input=$'\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 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" record_failure "wizard_build_trigger: sloptrap failed"
teardown_stub_env teardown_stub_env
return return
fi fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_build_trigger: manifest not created" record_failure "wizard_build_trigger: manifest not created"
fi fi
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then
record_failure "wizzard_build_trigger: build not invoked after wizard" record_failure "wizard_build_trigger: build not invoked after wizard"
fi fi
teardown_stub_env teardown_stub_env
} }
run_capability_trust_required() {
local scenario_dir="$TEST_ROOT/capability_repo"
printf '==> capability_trust_required\n'
setup_stub_env
if 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 "capability_trust_required: expected failure without trusted capabilities"
fi
teardown_stub_env
}
run_capabilities_require_podman() {
local scenario_dir="$TEST_ROOT/capability_repo"
printf '==> capabilities_require_podman\n'
local output_log
output_log=$(mktemp)
setup_stub_env
if PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" SLOPTRAP_CONTAINER_ENGINE=docker \
"$SLOPTRAP_BIN" --dry-run "$scenario_dir" >"$output_log" 2>&1; then
record_failure "capabilities_require_podman: expected docker capability run to be rejected"
elif ! grep -q -- 'capability-enabled runs require podman' "$output_log"; then
record_failure "capabilities_require_podman: missing explicit podman requirement"
fi
teardown_stub_env
rm -f "$output_log"
}
run_capability_profiles() {
local scenario_dir="$TEST_ROOT/capability_repo"
printf '==> capability_profiles\n'
setup_stub_env
local main_lines capture_lines pod_lines
local expected_build_network="bridge"
if command -v slirp4netns >/dev/null 2>&1; then
expected_build_network="slirp4netns"
fi
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "capability_profiles: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "CAPABILITY_PACKAGES=tcpdump" "$STUB_LOG"; then
record_failure "capability_profiles: build arg for capability packages missing"
fi
if ! grep -q -- "FAKE PODMAN: build --quiet -t capability-repo-sloptrap-image -f .* --network $expected_build_network " "$STUB_LOG"; then
record_failure "capability_profiles: build should stay on isolated networking"
fi
main_lines=$(grep "FAKE PODMAN: run " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-container" || true)
capture_lines=$(grep "FAKE PODMAN: run " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-capture" || true)
pod_lines=$(grep "FAKE PODMAN: pod create " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-pod" || true)
if [[ -z $main_lines ]]; then
record_failure "capability_profiles: main runtime container did not reach the container engine"
fi
if [[ -z $pod_lines ]]; then
record_failure "capability_profiles: packet capture should create a dedicated pod"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add NET_RAW"* ]]; then
record_failure "capability_profiles: capture sidecar should receive NET_RAW"
fi
if [[ -n $main_lines && $main_lines == *"--cap-add NET_RAW"* ]]; then
record_failure "capability_profiles: main container should not receive NET_RAW"
fi
if grep -q -- "--cap-add NET_ADMIN" <<<"$capture_lines"; then
record_failure "capability_profiles: NET_ADMIN should not be granted"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add SETUID"* ]]; then
record_failure "capability_profiles: SETUID capability missing"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add SETGID"* ]]; then
record_failure "capability_profiles: SETGID capability missing"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add CHOWN"* ]]; then
record_failure "capability_profiles: CHOWN capability missing"
fi
if grep -q -- "--cap-add DAC_OVERRIDE" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: DAC_OVERRIDE should not be granted"
fi
if grep -q -- "--cap-add FOWNER" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: FOWNER should not be granted"
fi
if ! grep -q -- "--security-opt no-new-privileges" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: no-new-privileges missing"
fi
if grep -q -- "--read-only" <<<"$main_lines"; then
record_failure "capability_profiles: apt profile should disable read-only rootfs"
fi
if grep -q -- "--user " <<<"$main_lines"; then
record_failure "capability_profiles: capability-enabled run should not force --user"
fi
if ! grep -q -- "--userns=keep-id:uid=$(id -u),gid=$(id -g)" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: podman keep-id user namespace missing"
fi
if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=apt-install" <<<"$main_lines"; then
record_failure "capability_profiles: main helper capability environment missing"
fi
if ! grep -q -- "SLOPTRAP_PACKET_CAPTURE_ENABLED=1" <<<"$main_lines"; then
record_failure "capability_profiles: main container should advertise packet capture availability"
fi
if ! grep -q -- "SLOPTRAP_HELPER_DIR=/codex/state/capture-helper" <<<"$capture_lines"; then
record_failure "capability_profiles: capture sidecar helper dir missing"
fi
if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=packet-capture" <<<"$capture_lines"; then
record_failure "capability_profiles: capture sidecar capability environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_UID=$(id -u)" "$STUB_LOG"; then
record_failure "capability_profiles: host uid environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_GID=$(id -g)" "$STUB_LOG"; then
record_failure "capability_profiles: host gid environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_USER=$(id -un)" "$STUB_LOG"; then
record_failure "capability_profiles: host user environment missing"
fi
local state_root capability_dir
state_root="$STUB_HOME/.codex/sloptrap/state"
capability_dir=$(find "$state_root" -mindepth 2 -maxdepth 2 -type d -name capabilities | head -n 1 || true)
if [[ -z $capability_dir ]]; then
record_failure "capability_profiles: project capability state directory missing"
fi
teardown_stub_env
}
run_embedded_capability_helpers() {
printf '==> embedded_capability_helpers\n'
local temp_root helper_bin helper_dir workspace_dir capture_dir tool_log helper_pid
temp_root=$(mktemp -d)
helper_bin="$temp_root/bin"
helper_dir="$temp_root/helper"
workspace_dir="$temp_root/workspace"
capture_dir="$temp_root/captures"
tool_log="$temp_root/tool.log"
helper_pid=""
mkdir -p "$helper_bin" "$helper_dir/queue" "$workspace_dir/data" "$capture_dir"
: >"$tool_log"
if ! extract_embedded_helper "sloptrap-entrypoint" "$helper_bin/sloptrap-entrypoint" \
|| ! extract_embedded_helper "sloptrap-helperd" "$helper_bin/sloptrap-helperd" \
|| ! extract_embedded_helper "slop-apt" "$helper_bin/slop-apt" \
|| ! extract_embedded_helper "slopcap" "$helper_bin/slopcap"; then
record_failure "embedded_capability_helpers: failed to extract embedded helper scripts"
rm -rf "$temp_root"
return
fi
cat >"$helper_bin/apt-get" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'apt-get %s\n' "$*" >>"$TEST_TOOL_LOG"
exit 0
EOF
cat >"$helper_bin/tcpdump" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'tcpdump %s\n' "$*" >>"$TEST_TOOL_LOG"
output=""
prev=""
for arg in "$@"; do
if [[ $prev == "-w" ]]; then
output=$arg
break
fi
prev=$arg
done
if [[ -n $output ]]; then
mkdir -p "$(dirname "$output")"
: >"$output"
fi
exit 0
EOF
cat >"$helper_bin/setpriv" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'setpriv %s\n' "$*" >>"$TEST_TOOL_LOG"
while [[ $# -gt 0 ]]; do
case "$1" in
--reuid|--regid)
shift 2
;;
--clear-groups)
shift
;;
--)
shift
break
;;
*)
break
;;
esac
done
exec "$@"
EOF
chmod +x "$helper_bin/apt-get" "$helper_bin/tcpdump" "$helper_bin/setpriv"
if ! grep -q "chmod 711 \"\\\$helper_dir\"" "$helper_bin/sloptrap-entrypoint" \
|| ! grep -q "chmod 1733 \"\\\$queue_dir\"" "$helper_bin/sloptrap-entrypoint"; then
record_failure "embedded_capability_helpers: entrypoint did not expose helper queue to the dropped user"
fi
if grep -q -- "setpriv --reuid 0 --regid 0" "$helper_bin/slop-apt" \
|| grep -q -- "setpriv --reuid 0 --regid 0" "$helper_bin/slopcap"; then
record_failure "embedded_capability_helpers: helper clients should not attempt to regain root"
fi
if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$temp_root/helper-missing" \
SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
"$helper_bin/slop-apt" install jq >"$temp_root/missing-helper.out" 2>"$temp_root/missing-helper.err"; then
record_failure "embedded_capability_helpers: slop-apt should fail when the root helper is unavailable"
elif ! grep -q -- 'capability helper is unavailable' "$temp_root/missing-helper.err"; then
record_failure "embedded_capability_helpers: missing helper failure should explain the boundary"
fi
TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \
SLOPTRAP_APT_GET_BIN="$helper_bin/apt-get" \
SLOPTRAP_TCPDUMP_BIN="$helper_bin/tcpdump" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_AUDIT_LOG="$temp_root/audit.log" "$helper_bin/sloptrap-helperd" >/dev/null 2>&1 &
helper_pid=$!
if ! wait_for_path "$helper_dir/helperd.pid"; then
record_failure "embedded_capability_helpers: helper daemon did not publish its pid file"
fi
if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
"$helper_bin/slop-apt" install jq >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slop-apt failed against the embedded helper daemon"
fi
if ! grep -q -- 'apt-get install -y --no-install-recommends jq' "$tool_log"; then
record_failure "embedded_capability_helpers: slop-apt did not reach apt-get install"
fi
local bad_apt_request
bad_apt_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req")
printf 'apt-install\n' >"$bad_apt_request/op"
printf '%s\n' '--allow-unauthenticated' >"$bad_apt_request/packages"
if ! wait_for_path "$bad_apt_request/status"; then
record_failure "embedded_capability_helpers: helper daemon did not answer the invalid apt request"
elif [[ $(<"$bad_apt_request/status") != "2" ]]; then
record_failure "embedded_capability_helpers: invalid apt request returned the wrong status"
fi
if [[ -s "$bad_apt_request/stderr" ]] && ! grep -q -- 'invalid package name' "$bad_apt_request/stderr"; then
record_failure "embedded_capability_helpers: invalid apt request did not explain the rejection"
fi
if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_CAPTURE_HELPER_DIR="$helper_dir" SLOPTRAP_PACKET_CAPTURE_ENABLED=1 \
"$helper_bin/slopcap" capture --interface eth0 --output /tmp/escape.pcap >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slopcap accepted an out-of-bounds output path"
fi
if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_CAPTURE_HELPER_DIR="$helper_dir" SLOPTRAP_PACKET_CAPTURE_ENABLED=1 \
"$helper_bin/slopcap" capture --interface eth0 --filter 'tcp port 80' \
--output "$workspace_dir/capture.pcap" >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slopcap failed for a workspace-local capture file"
fi
if ! grep -q -- "tcpdump -p -i eth0 -w $workspace_dir/capture.pcap -- tcp port 80" "$tool_log"; then
record_failure "embedded_capability_helpers: slopcap did not invoke tcpdump with the expected guarded arguments"
fi
local bad_capture_request
bad_capture_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req")
printf 'packet-capture\n' >"$bad_capture_request/op"
printf 'eth0\n' >"$bad_capture_request/interface"
printf '\n' >"$bad_capture_request/filter"
printf '/tmp/escape.pcap\n' >"$bad_capture_request/output"
printf '0\n' >"$bad_capture_request/stdout_mode"
if ! wait_for_path "$bad_capture_request/status"; then
record_failure "embedded_capability_helpers: helper daemon did not answer the invalid capture request"
elif [[ $(<"$bad_capture_request/status") != "2" ]]; then
record_failure "embedded_capability_helpers: invalid capture request returned the wrong status"
fi
if [[ -s "$bad_capture_request/stderr" ]] && ! grep -q -- 'output path must stay within' "$bad_capture_request/stderr"; then
record_failure "embedded_capability_helpers: invalid capture request did not explain the rejection"
fi
if [[ -n $helper_pid ]]; then
kill "$helper_pid" >/dev/null 2>&1 || true
wait "$helper_pid" >/dev/null 2>&1 || true
fi
rm -rf "$temp_root"
}
run_make_install_single_file() {
local scenario_dir="$TEST_ROOT/resume_target"
printf '==> make_install_single_file\n'
if ! command -v make >/dev/null 2>&1; then
record_failure "make_install_single_file: make binary not found in PATH"
return
fi
setup_stub_env
local install_root install_dir installed_bin
install_root=$(mktemp -d)
install_dir="$install_root/bin"
installed_bin="$install_dir/sloptrap"
if ! make -C "$ROOT_DIR" install INSTALL_DIR="$install_dir" >/dev/null 2>&1; then
record_failure "make_install_single_file: make install failed"
teardown_stub_env
rm -rf "$install_root"
return
fi
if [[ ! -x $installed_bin ]]; then
record_failure "make_install_single_file: installed launcher missing"
fi
local helper
for helper in sloptrap-entrypoint sloptrap-helperd slop-apt slopcap; do
if [[ -e $install_dir/$helper ]]; then
record_failure "make_install_single_file: unexpected helper installed ($helper)"
fi
done
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$installed_bin" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "make_install_single_file: installed launcher failed"
fi
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then
record_failure "make_install_single_file: installed launcher did not reach build path"
fi
if ! make -C "$ROOT_DIR" uninstall INSTALL_DIR="$install_dir" >/dev/null 2>&1; then
record_failure "make_install_single_file: make uninstall failed"
fi
if [[ -e $installed_bin ]]; then
record_failure "make_install_single_file: installed launcher not removed by uninstall"
fi
teardown_stub_env
rm -rf "$install_root"
}
run_shellcheck run_shellcheck
run_mount_injection run_mount_injection
run_root_target run_root_target
@@ -503,7 +1202,15 @@ run_symlink_escape
run_manifest_injection run_manifest_injection
run_helper_symlink run_helper_symlink
run_secret_mask run_secret_mask
run_git_ignore_mask
run_resume_target run_resume_target
run_runtime_context_prompt
run_sh_reexec
run_resume_omits_runtime_context
run_auth_file_mount
run_codex_home_override
run_project_state_isolation
run_auto_login_empty_auth
run_codex_symlink_home run_codex_symlink_home
run_root_directory_project run_root_directory_project
run_shared_dir_override run_shared_dir_override
@@ -513,10 +1220,19 @@ run_dotdot_ignore
run_invalid_manifest_name run_invalid_manifest_name
run_invalid_manifest_sandbox run_invalid_manifest_sandbox
run_invalid_manifest_packages run_invalid_manifest_packages
run_invalid_manifest_capabilities
run_invalid_allow_host_network run_invalid_allow_host_network
run_wizzard_create_manifest run_host_network_packet_capture_ack_required
run_wizzard_existing_defaults run_host_network_packet_capture_ack_prompt
run_wizzard_build_trigger run_removed_nested_podman_manifest
run_wizard_create_manifest
run_wizard_existing_defaults
run_wizard_build_trigger
run_capability_trust_required
run_capabilities_require_podman
run_capability_profiles
run_embedded_capability_helpers
run_make_install_single_file
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=wizard_build
packages_extra=
capabilities=
allow_host_network=false

View File

@@ -0,0 +1,4 @@
name=wizard_empty
packages_extra=
capabilities=
allow_host_network=false

View File

@@ -0,0 +1,4 @@
name=custom-wizard
packages_extra=make git
capabilities=apt-install
allow_host_network=true

View File

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

View File

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

View File

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