Compare commits

..

3 Commits

Author SHA1 Message Date
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
19 changed files with 1147 additions and 185 deletions

View File

@@ -1,4 +1,4 @@
name=sloptrap
packages_extra=make shellcheck jq
codex_args=--sandbox workspace-write
name=skz-sloptrap
packages_extra=make shellcheck jq podman
capabilities=apt-install nested-podman packet-capture
allow_host_network=false

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%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%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:
@printf '%b%bRemoving%b %b%s%b\n' '$(PREFIX_COMMENT)' '$(COLOR_TEXT)' '$(COLOR_TEXT)' '$(COLOR_COMMENT)' '$(INSTALL_PATH)' '$(RESET)'

View File

@@ -25,7 +25,6 @@ brew install coreutils gnu-tar jq
cat > path/to/project/.sloptrap <<'EOF'
name=path/to/project
packages_extra=make
codex_args=--sandbox danger-full-access --ask-for-approval never
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:
- builds `path/to/project-sloptrap-image` if missing,
- 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.
## 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.
- 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`.
@@ -54,7 +53,7 @@ brew install coreutils gnu-tar jq
The manifest is optional. When absent, sloptrap derives:
- `name = basename(project directory)`
- `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.
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. |
| `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`, `packet-capture`, and `nested-podman`. |
| `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.
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`. Trusted capabilities can then be activated per run with `--enable-capability <name>`.
### `.sloptrapignore`
@@ -79,13 +80,15 @@ Values containing `$`, `` ` ``, or newlines are rejected to prevent command inje
## CLI Reference
```
./sloptrap [--dry-run] [--print-config] <code-directory> [target ...]
./sloptrap [--dry-run] [--print-config] [--trust-capabilities] [--enable-capability <name> ...] <code-directory> [target ...]
```
Options:
- `--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.
- `--trust-capabilities` &mdash; trust the manifest's requested capabilities for the current build flow.
- `--enable-capability <name>` &mdash; enable a trusted runtime capability for this invocation. Repeat for multiple capabilities.
- `-h, --help` &mdash; display usage.
- `--` &mdash; stop option parsing; remaining arguments are treated as targets.
@@ -93,10 +96,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.
- `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.
`--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
@@ -112,34 +116,42 @@ 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-if-missing` | No-op when the image already exists; otherwise delegates to `build`. |
| `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). |
| `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. |
| `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). |
| `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.
### Capability Helpers
When a trusted capability is enabled for a run, the container includes helper commands:
- `slop-apt install <package...>` for session-scoped package installation.
- `slopcap capture --interface <iface> [--filter <expr>] [--output <path>] [--stdout]` for packet capture.
- `sloppodman <pull|build|tag|run|ps|logs|stop|rm|inspect> ...` for nested Podman workflows. `build` contexts and Dockerfiles must remain inside `/workspace`, and pushes are not supported.
## Execution Environment
- Container engine: Podman or podman with identical command lines. Podman uses `--userns=keep-id`; Docker receives the equivalent `--user UID:GID`.
- Filesystem view: the project directory mounts at `/workspace`; `${HOME}/.codex` mounts at `/codex`.
- Container engine: Podman or Docker with identical command lines. 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/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.
- Network: the container always runs with `--network host`. sloptrap does not filter or proxy outbound traffic.
- 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.
- Codex configuration: runtime flags come from `codex_args`. Persistent Codex state is stored under `${HOME}/.codex`.
- Network: isolated networking is used by default; `allow_host_network=true` opts into `--network host`.
- 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 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`.
## 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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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; if you require an offline or firewalled workflow, sloptrap is not an appropriate launcher.
- **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**: 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.
- **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.

43
slop-apt Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
mkdir -p "$queue_dir"
if [[ ${1-} != "install" ]]; then
printf 'usage: slop-apt install <package...>\n' >&2
exit 2
fi
shift
if [[ $# -eq 0 ]]; then
printf 'slop-apt: at least one package is required\n' >&2
exit 2
fi
for package in "$@"; do
if [[ ! $package =~ ^[A-Za-z0-9+.-]+$ ]]; then
printf 'slop-apt: invalid package name %s\n' "$package" >&2
exit 2
fi
done
request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req")
trap 'rm -rf "$request_dir"' EXIT INT TERM HUP
printf 'apt-install\n' >"$request_dir/op"
printf '%s\n' "$@" >"$request_dir/packages"
while [[ ! -f "$request_dir/status" ]]; do
sleep 1
done
if [[ -s "$request_dir/stdout" ]]; then
cat "$request_dir/stdout"
fi
if [[ -s "$request_dir/stderr" ]]; then
cat "$request_dir/stderr" >&2
fi
status=$(<"$request_dir/status")
exit "$status"

85
slopcap Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -euo pipefail
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
default_output=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}
mkdir -p "$queue_dir" "$default_output"
if [[ ${1-} != "capture" ]]; then
printf 'usage: slopcap capture --interface <iface> [--filter <expr>] [--output <path>] [--stdout]\n' >&2
exit 2
fi
shift
iface=""
filter=""
output=""
stdout_mode=0
while [[ $# -gt 0 ]]; do
case "$1" in
--interface)
shift
[[ $# -gt 0 ]] || { printf 'slopcap: --interface requires a value\n' >&2; exit 2; }
iface=$1
;;
--filter)
shift
[[ $# -gt 0 ]] || { printf 'slopcap: --filter requires a value\n' >&2; exit 2; }
filter=$1
;;
--output)
shift
[[ $# -gt 0 ]] || { printf 'slopcap: --output requires a value\n' >&2; exit 2; }
output=$1
;;
--stdout)
stdout_mode=1
;;
*)
printf 'slopcap: unsupported argument %s\n' "$1" >&2
exit 2
;;
esac
shift
done
[[ -n $iface ]] || { printf 'slopcap: --interface is required\n' >&2; exit 2; }
if [[ -z $output && $stdout_mode -eq 0 ]]; then
output="$default_output/capture-$(date +%s).pcap"
fi
request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req")
trap 'rm -rf "$request_dir"' EXIT INT TERM HUP
printf 'packet-capture\n' >"$request_dir/op"
printf '%s\n' "$iface" >"$request_dir/interface"
printf '%s\n' "$filter" >"$request_dir/filter"
printf '%s\n' "$output" >"$request_dir/output"
printf '%s\n' "$stdout_mode" >"$request_dir/stdout_mode"
stream_pid=""
if [[ $stdout_mode -eq 1 ]]; then
touch "$request_dir/stdout"
tail -f "$request_dir/stdout" &
stream_pid=$!
fi
while [[ ! -f "$request_dir/status" ]]; do
sleep 1
done
if [[ -n $stream_pid ]]; then
kill "$stream_pid" >/dev/null 2>&1 || true
wait "$stream_pid" >/dev/null 2>&1 || true
fi
if [[ $stdout_mode -eq 0 && -s "$request_dir/stdout" ]]; then
cat "$request_dir/stdout"
fi
if [[ -s "$request_dir/stderr" ]]; then
cat "$request_dir/stderr" >&2
fi
status=$(<"$request_dir/status")
exit "$status"

97
sloppodman Normal file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -eq 0 ]]; then
printf 'usage: sloppodman <pull|build|tag|run|ps|logs|stop|rm|inspect> ...\n' >&2
exit 2
fi
subcommand=$1
shift
case "$subcommand" in
pull|build|tag|run|ps|logs|stop|rm|inspect)
;;
*)
printf 'sloppodman: unsupported subcommand %s\n' "$subcommand" >&2
exit 2
;;
esac
workspace_root=${SLOPTRAP_WORKDIR:-/workspace}
podman_root=${SLOPTRAP_INNER_PODMAN_ROOT:-/codex/capabilities/podman/storage}
podman_runroot=${SLOPTRAP_INNER_PODMAN_RUNROOT:-/codex/capabilities/podman/run}
runtime_dir=${XDG_RUNTIME_DIR:-/codex/capabilities/podman/runtime}
mkdir -p "$podman_root" "$podman_runroot" "$runtime_dir"
resolve_inner_path() {
local raw=$1
if command -v realpath >/dev/null 2>&1; then
realpath -m "$raw"
return
fi
case "$raw" in
/*) printf '%s\n' "$raw" ;;
*) printf '%s/%s\n' "$(pwd -P)" "$raw" ;;
esac
}
validate_workspace_path() {
local path=$1
path=$(resolve_inner_path "$path")
case "$path" in
"$workspace_root"|"${workspace_root}/"*) ;;
*)
printf 'sloppodman: path must stay within %s (%s)\n' "$workspace_root" "$path" >&2
exit 2
;;
esac
}
if [[ $subcommand == "build" ]]; then
args=("$@")
context=""
idx=0
while (( idx < ${#args[@]} )); do
arg=${args[$idx]}
case "$arg" in
-f|--file)
((idx+=1))
(( idx < ${#args[@]} )) || { printf 'sloppodman: %s requires a path\n' "$arg" >&2; exit 2; }
validate_workspace_path "${args[$idx]}"
;;
--network)
((idx+=1))
(( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; }
if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then
printf 'sloppodman: host networking is not available in this session\n' >&2
exit 2
fi
;;
esac
((idx+=1))
done
if [[ ${#args[@]} -gt 0 ]]; then
context=${args[$(( ${#args[@]} - 1 ))]}
validate_workspace_path "$context"
fi
fi
if [[ $subcommand == "run" ]]; then
args=("$@")
idx=0
while (( idx < ${#args[@]} )); do
arg=${args[$idx]}
if [[ $arg == "--network" ]]; then
((idx+=1))
(( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; }
if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then
printf 'sloppodman: host networking is not available in this session\n' >&2
exit 2
fi
fi
((idx+=1))
done
fi
exec podman --root "$podman_root" --runroot "$podman_runroot" "$subcommand" "$@"

521
sloptrap
View File

@@ -1,5 +1,8 @@
#!/usr/bin/env bash
# sloptrap
if [ -z "${BASH_VERSION:-}" ]; then
exec bash "$0" "$@"
fi
set -euo pipefail
IS_MAC=false
@@ -158,6 +161,7 @@ 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"
SLOPTRAP_SUPPORTED_CAPABILITIES=(apt-install packet-capture nested-podman)
usage() {
print_banner
@@ -165,19 +169,20 @@ usage() {
info_line "Options:\n"
comment_line " --dry-run Show planned container command(s) and exit\n"
comment_line " --print-config Display resolved manifest values\n"
comment_line " --trust-capabilities Trust the manifest's requested capabilities for this build\n"
comment_line " -h, --help Show this message\n"
info_line "\n"
comment_line "Each project supplies configuration via a %s file in its root.\n" "$MANIFEST_BASENAME"
info_line "Example manifest entries:\n"
comment_line " name=my-project\n"
comment_line " packages_extra=kubectl helm\n"
comment_line " codex_args=--sandbox danger-full-access --ask-for-approval never\n"
comment_line " capabilities=apt-install packet-capture\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 " wizard 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"
}
@@ -221,13 +226,25 @@ resolve_path_strict() {
declare -A MANIFEST=()
declare -a SLOPTRAP_IGNORE_ENTRIES=()
declare -a IGNORE_MOUNT_ARGS=()
declare -a CODEX_ARGS_ARRAY=()
declare -a DEFAULT_TARGETS=()
MANIFEST_PRESENT=false
CURRENT_IGNORE_FILE=""
CONTAINER_ENGINE=""
CODEX_HOME_HOST=""
CODEX_ROOT_HOST=""
CODEX_STATE_HOME_HOST=""
CODEX_AUTH_FILE_HOST=""
CODEX_STATE_KEY=""
CODEX_HOME_BOOTSTRAP=false
NEED_LOGIN=false
REQUESTED_CAPABILITIES=""
ENABLED_CAPABILITIES=""
CAPABILITY_MANIFEST_DIGEST=""
CAPABILITY_TRUST_ROOT_HOST=""
CAPABILITY_TRUST_FILE_HOST=""
CAPABILITY_BUILD_STAMP_HOST=""
CAPABILITY_STATE_HOST=""
IGNORE_STUB_BASE=""
IGNORE_HELPER_ROOT=""
ALLOW_HOST_NETWORK=false
@@ -296,10 +313,11 @@ FROM ${BASE_IMAGE}
ENV DEBIAN_FRONTEND=noninteractive
ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps"
ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps util-linux"
ARG EXTRA_PACKAGES=""
ARG CAPABILITY_PACKAGES=""
RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} \
&& apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} ${CAPABILITY_PACKAGES} \
&& rm -rf /var/lib/apt/lists/*
ARG CODEX_UID=1337
@@ -311,14 +329,20 @@ RUN groupadd --gid ${CODEX_GID} sloptrap \
ARG CODEX_BIN=codex
ARG CODEX_CONF=config/config.toml
COPY ${CODEX_BIN} /usr/local/bin/codex
COPY sloptrap-entrypoint /usr/local/bin/sloptrap-entrypoint
COPY sloptrap-helperd /usr/local/bin/sloptrap-helperd
COPY slop-apt /usr/local/bin/slop-apt
COPY slopcap /usr/local/bin/slopcap
COPY sloppodman /usr/local/bin/sloppodman
RUN chown -R sloptrap:sloptrap /home/sloptrap
USER sloptrap
RUN chmod 0755 /usr/local/bin/sloptrap-entrypoint /usr/local/bin/sloptrap-helperd \
/usr/local/bin/slop-apt /usr/local/bin/slopcap /usr/local/bin/sloppodman \
&& chown -R sloptrap:sloptrap /home/sloptrap
WORKDIR /workspace
ENV SHELL=/bin/bash HOME=/home/sloptrap
ENTRYPOINT ["codex"]
ENTRYPOINT ["/usr/local/bin/sloptrap-entrypoint"]
EOF
}
@@ -361,6 +385,13 @@ prepare_build_context() {
populate_dockerfile "$SLOPTRAP_DOCKERFILE_PATH"
validate_basename "$SLOPTRAP_CODEX_BIN_NAME"
CODEX_BIN_PATH="$SLOPTRAP_BUILD_CONTEXT/$SLOPTRAP_CODEX_BIN_NAME"
local helper
for helper in sloptrap-entrypoint sloptrap-helperd slop-apt slopcap sloppodman; do
if [[ ! -f "$SCRIPT_DIR/$helper" ]]; then
error "required helper '$SCRIPT_DIR/$helper' not found"
fi
cp "$SCRIPT_DIR/$helper" "$SLOPTRAP_BUILD_CONTEXT/$helper"
done
}
select_codex_home() {
@@ -372,13 +403,118 @@ select_codex_home() {
error "expected Codex home '$preferred' to be a directory"
fi
CODEX_HOME_HOST="$preferred"
if [[ -d $CODEX_HOME_HOST ]]; then
CODEX_HOME_HOST="$(cd "$CODEX_HOME_HOST" && pwd -P)"
CODEX_ROOT_HOST="$preferred"
if [[ -d $CODEX_ROOT_HOST ]]; then
CODEX_ROOT_HOST="$(cd "$CODEX_ROOT_HOST" && pwd -P)"
CODEX_HOME_BOOTSTRAP=false
else
CODEX_HOME_BOOTSTRAP=true
fi
CODEX_STATE_KEY=$(printf '%s' "$CODE_DIR" | sha256sum)
CODEX_STATE_KEY=${CODEX_STATE_KEY%% *}
CODEX_STATE_HOME_HOST="$CODEX_ROOT_HOST/sloptrap/state/$CODEX_STATE_KEY"
CODEX_AUTH_FILE_HOST="$CODEX_ROOT_HOST/auth.json"
if [[ -L $CODEX_AUTH_FILE_HOST ]]; then
error "Codex auth file '$CODEX_AUTH_FILE_HOST' must not be a symlink"
fi
if [[ -e $CODEX_AUTH_FILE_HOST && ! -f $CODEX_AUTH_FILE_HOST ]]; then
error "expected Codex auth file '$CODEX_AUTH_FILE_HOST' to be a regular file"
fi
}
compute_manifest_digest() {
if [[ -f $MANIFEST_PATH ]]; then
local digest
digest=$(sha256sum "$MANIFEST_PATH")
printf '%s' "${digest%% *}"
return 0
fi
printf 'no-manifest'
}
select_capability_state_paths() {
local capability_root="$CODEX_ROOT_HOST/sloptrap/capabilities"
CAPABILITY_TRUST_ROOT_HOST="$capability_root/trust"
CAPABILITY_TRUST_FILE_HOST="$CAPABILITY_TRUST_ROOT_HOST/$CODEX_STATE_KEY.trust"
CAPABILITY_BUILD_STAMP_HOST="$capability_root/builds/$CODEX_STATE_KEY.stamp"
CAPABILITY_STATE_HOST="$CODEX_STATE_HOME_HOST/capabilities"
}
ensure_capability_state_paths() {
local capability_root="$CODEX_ROOT_HOST/sloptrap/capabilities"
ensure_codex_directory "$capability_root" "sloptrap capability namespace"
ensure_codex_directory "$CAPABILITY_TRUST_ROOT_HOST" "sloptrap capability trust root"
ensure_codex_directory "$(dirname "$CAPABILITY_BUILD_STAMP_HOST")" "sloptrap capability build stamp root"
ensure_codex_directory "$CAPABILITY_STATE_HOST" "project capability state"
}
capability_trust_matches_current() {
[[ -f $CAPABILITY_TRUST_FILE_HOST ]] || return 1
local trusted_digest trusted_caps
trusted_digest=$(sed -n '1p' "$CAPABILITY_TRUST_FILE_HOST" 2>/dev/null || true)
trusted_caps=$(sed -n '2p' "$CAPABILITY_TRUST_FILE_HOST" 2>/dev/null || true)
[[ $trusted_digest == "$CAPABILITY_MANIFEST_DIGEST" && $trusted_caps == "$REQUESTED_CAPABILITIES" ]]
}
record_capability_trust() {
ensure_capability_state_paths
if $DRY_RUN; then
print_command mkdir -p "$(dirname "$CAPABILITY_TRUST_FILE_HOST")"
print_command sh -c "printf '%s\\n%s\\n' '$CAPABILITY_MANIFEST_DIGEST' '$REQUESTED_CAPABILITIES' > '$CAPABILITY_TRUST_FILE_HOST'"
return 0
fi
printf '%s\n%s\n' "$CAPABILITY_MANIFEST_DIGEST" "$REQUESTED_CAPABILITIES" >"$CAPABILITY_TRUST_FILE_HOST"
}
prompt_capability_trust() {
local tty_path="/dev/tty"
info_line "Manifest requests privileged capabilities: %s\n" "$REQUESTED_CAPABILITIES"
printf '%s' "$PREFIX_TEXT" >"$tty_path"
printf '%b' "$COLOR_TEXT" >"$tty_path"
printf 'Trust these capabilities for this project build? [y/N]: ' >"$tty_path"
printf '%b' "$RESET" >"$tty_path"
local input
if ! IFS= read -r input <"$tty_path"; then
error "capability trust requires an interactive terminal or --trust-capabilities"
fi
case "${input,,}" in
y|yes)
record_capability_trust
;;
*)
error "capability trust not granted"
;;
esac
}
ensure_capability_trust() {
[[ -n $REQUESTED_CAPABILITIES ]] || return 0
capability_trust_matches_current && return 0
if $TRUST_CAPABILITIES; then
record_capability_trust
return 0
fi
if [[ ! -t 0 ]]; then
error "requested capabilities require prior trust or --trust-capabilities"
fi
prompt_capability_trust
}
write_capability_build_stamp() {
ensure_capability_state_paths
if $DRY_RUN; then
print_command sh -c "printf '%s\\n%s\\n' '$CAPABILITY_MANIFEST_DIGEST' '$REQUESTED_CAPABILITIES' > '$CAPABILITY_BUILD_STAMP_HOST'"
return 0
fi
printf '%s\n%s\n' "$CAPABILITY_MANIFEST_DIGEST" "$REQUESTED_CAPABILITIES" >"$CAPABILITY_BUILD_STAMP_HOST"
}
capability_build_stamp_matches_current() {
[[ -f $CAPABILITY_BUILD_STAMP_HOST ]] || return 1
local stamp_digest stamp_caps
stamp_digest=$(sed -n '1p' "$CAPABILITY_BUILD_STAMP_HOST" 2>/dev/null || true)
stamp_caps=$(sed -n '2p' "$CAPABILITY_BUILD_STAMP_HOST" 2>/dev/null || true)
[[ $stamp_digest == "$CAPABILITY_MANIFEST_DIGEST" && $stamp_caps == "$REQUESTED_CAPABILITIES" ]]
}
assert_path_within_code_dir() {
@@ -625,6 +761,47 @@ validate_package_list() {
done
}
validate_capability_list() {
local key=$1
local raw=$2
local source=${3:-$MANIFEST_PATH}
[[ -z $raw ]] && return 0
local token supported capability
for token in $raw; do
supported=false
for capability in "${SLOPTRAP_SUPPORTED_CAPABILITIES[@]}"; do
if [[ $token == "$capability" ]]; then
supported=true
break
fi
done
if [[ $supported != true ]]; then
error "$source: invalid capability '$token' in '$key'"
fi
done
}
normalize_capability_list() {
local raw=$1
[[ -z $raw ]] && return 0
local token
for token in $raw; do
printf '%s\n' "$token"
done | sort -u | tr '\n' ' ' | sed 's/ $//'
}
capability_list_contains() {
local list=$1
local needle=$2
local token
for token in $list; do
if [[ $token == "$needle" ]]; then
return 0
fi
done
return 1
}
detect_container_engine() {
local override=${SLOPTRAP_CONTAINER_ENGINE-}
if [[ -n $override ]]; then
@@ -691,12 +868,12 @@ prompt_manifest_value() {
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"
error "wizard requires an interactive terminal"
fi
printf '%s' "$input"
}
validate_wizzard_name() {
validate_wizard_name() {
local value=$1
[[ -n $value ]] || error "$MANIFEST_PATH: name must not be empty"
if [[ ! $value =~ $VALID_NAME_REGEX ]]; then
@@ -704,7 +881,7 @@ validate_wizzard_name() {
fi
}
normalize_wizzard_allow_host_network() {
normalize_wizard_allow_host_network() {
local value=${1,,}
case "$value" in
1|true|yes) printf 'true' ;;
@@ -713,25 +890,13 @@ normalize_wizzard_allow_host_network() {
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() {
run_wizard() {
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"
error "wizard requires an interactive terminal"
fi
if [[ ! -f $manifest_path ]]; then
print_banner
@@ -739,12 +904,12 @@ run_wizzard() {
local default_name
local default_packages_extra
local default_codex_args
local default_capabilities
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_capabilities=$(manifest_default_value "capabilities" "")
default_allow_host_network=$(manifest_default_value "allow_host_network" "false")
local action="Creating"
@@ -759,7 +924,7 @@ run_wizzard() {
value=$(prompt_manifest_value "name" "$default_name")
value=$(trim "$value")
[[ -n $value ]] || value=$default_name
validate_wizzard_name "$value"
validate_wizard_name "$value"
default_name=$value
break
done
@@ -777,12 +942,15 @@ run_wizzard() {
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")
info_line "capabilities: Optional privileged features (%s).\n" "${SLOPTRAP_SUPPORTED_CAPABILITIES[*]}"
value=$(prompt_manifest_value "capabilities" "$default_capabilities")
value=$(trim "$value")
[[ -n $value ]] || value=$default_codex_args
validate_wizzard_codex_args "$value"
default_codex_args=$value
[[ -n $value ]] || value=$default_capabilities
value=$(normalize_capability_list "$value")
if [[ -n $value ]]; then
validate_capability_list "capabilities" "$value" "$manifest_path"
fi
default_capabilities=$value
break
done
@@ -791,7 +959,7 @@ run_wizzard() {
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")
default_allow_host_network=$(normalize_wizard_allow_host_network "$value")
break
done
@@ -799,7 +967,7 @@ run_wizzard() {
cat > "$manifest_path" <<EOF
name=$default_name
packages_extra=$default_packages_extra
codex_args=$default_codex_args
capabilities=$default_capabilities
allow_host_network=$default_allow_host_network
EOF
info_line "Wrote %s\n" "$manifest_path"
@@ -820,12 +988,19 @@ print_config() {
info_line "container_engine=%s\n" "$CONTAINER_ENGINE"
info_line "image_name=%s\n" "$SLOPTRAP_IMAGE_NAME"
info_line "container_name=%s\n" "$SLOPTRAP_CONTAINER_NAME"
info_line "codex_home=%s\n" "$CODEX_HOME_HOST"
info_line "codex_home=%s\n" "$CODEX_STATE_HOME_HOST"
info_line "codex_root=%s\n" "$CODEX_ROOT_HOST"
info_line "codex_state_home=%s\n" "$CODEX_STATE_HOME_HOST"
info_line "codex_auth_file=%s\n" "$CODEX_AUTH_FILE_HOST"
info_line "codex_state_key=%s\n" "$CODEX_STATE_KEY"
info_line "codex_home_bootstrap=%s\n" "$CODEX_HOME_BOOTSTRAP"
info_line "codex_archive=%s\n" "$SLOPTRAP_CODEX_ARCHIVE"
info_line "codex_url=%s\n" "$SLOPTRAP_CODEX_URL"
info_line "needs_login=%s\n" "$NEED_LOGIN"
info_line "codex_args=%s\n" "$CODEX_ARGS_DISPLAY"
info_line "runtime_flags=%s\n" "$CODEX_ARGS_DISPLAY"
info_line "requested_capabilities=%s\n" "$REQUESTED_CAPABILITIES"
info_line "enabled_capabilities=%s\n" "$ENABLED_CAPABILITIES"
info_line "capability_trust=%s\n" "$(capability_trust_matches_current && printf true || printf false)"
info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE"
if [[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]]; then
local ignore_paths
@@ -850,10 +1025,58 @@ print_manifest_summary() {
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 " capabilities=%s\n" "$REQUESTED_CAPABILITIES"
comment_line " runtime_flags=%s\n" "$CODEX_ARGS_DISPLAY"
comment_line " allow_host_network=%s\n" "$ALLOW_HOST_NETWORK"
}
build_runtime_context_prompt() {
local manifest_present prompt manifest_capabilities trusted enabled network_mode
manifest_present="false"
if [[ -f $MANIFEST_PATH ]]; then
manifest_present="true"
fi
manifest_capabilities=${REQUESTED_CAPABILITIES:-none}
trusted="none"
if [[ -n $REQUESTED_CAPABILITIES ]] && capability_trust_matches_current; then
trusted=$REQUESTED_CAPABILITIES
fi
enabled=${ENABLED_CAPABILITIES:-none}
network_mode="isolated"
if [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then
network_mode="host"
fi
prompt=$(cat <<EOF
You are running inside sloptrap, which confines Codex inside a container.
This startup note describes the sloptrap runtime only; it does not replace higher-priority instructions from AGENTS.md or the system.
Container layout:
- /workspace is the project mount.
- /codex is persistent Codex state for this project.
- The project manifest path is /workspace/.sloptrap and it may be absent.
Manifest key meanings:
- name: labels the sloptrap project/image/container names.
- packages_extra: Debian packages added when the image was built.
- capabilities: privileged features requested by the manifest; only capabilities enabled for this run are currently usable.
- allow_host_network: enables host networking when true; otherwise networking is isolated.
Current resolved sloptrap state:
- manifest_present=$manifest_present
- project_name=$PROJECT_NAME
- packages_extra=${PACKAGES_EXTRA:-none}
- manifest_capabilities=$manifest_capabilities
- trusted_capabilities=$trusted
- enabled_capabilities=$enabled
- network_mode=$network_mode
- runtime_flags=$CODEX_ARGS_DISPLAY
If you need exact project configuration, inspect /workspace/.sloptrap directly.
EOF
)
printf '%s' "$prompt"
}
declare -a CONTAINER_SHARED_OPTS=()
declare -a BASE_CONTAINER_CMD=()
SLOPTRAP_IMAGE_NAME=""
@@ -865,6 +1088,7 @@ CODEX_BIN_PATH=""
SLOPTRAP_SHARED_DIR_ABS=""
SLOPTRAP_PACKAGES_BASE=""
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=""
SLOPTRAP_PACKAGES_CAPABILITY=""
SLOPTRAP_CODEX_BIN_NAME=""
SLOPTRAP_CODEX_URL=""
SLOPTRAP_CODEX_ARCHIVE=""
@@ -880,6 +1104,8 @@ SLOPTRAP_LIMITS_SHM=""
SLOPTRAP_LIMITS_CPU=""
SLOPTRAP_TMPFS_PATHS=""
SLOPTRAP_ROOTFS_READONLY=""
SLOPTRAP_ROOTFS_READONLY_DEFAULT=""
SLOPTRAP_RUN_AS_ROOT=false
get_env_default() {
local var=$1
@@ -942,15 +1168,47 @@ run_or_print() {
"$@"
}
ensure_codex_home_dir() {
if [[ -d $CODEX_HOME_HOST ]]; then
ensure_codex_directory() {
local path=$1
local label=$2
if [[ -L $path ]]; then
error "$label '$path' must not be a symlink"
fi
if [[ -e $path && ! -d $path ]]; then
error "expected $label '$path' to be a directory"
fi
if [[ -d $path ]]; then
return 0
fi
if $DRY_RUN; then
print_command mkdir -p "$CODEX_HOME_HOST"
print_command mkdir -p "$path"
return 0
fi
mkdir -p "$CODEX_HOME_HOST"
mkdir -p "$path"
}
ensure_codex_storage_paths() {
local state_root="$CODEX_ROOT_HOST/sloptrap"
local state_bucket="$state_root/state"
ensure_codex_directory "$CODEX_ROOT_HOST" "Codex home"
ensure_codex_directory "$state_root" "sloptrap Codex namespace"
ensure_codex_directory "$state_bucket" "sloptrap Codex state root"
ensure_codex_directory "$CODEX_STATE_HOME_HOST" "project Codex state"
ensure_capability_state_paths
if [[ -L $CODEX_AUTH_FILE_HOST ]]; then
error "Codex auth file '$CODEX_AUTH_FILE_HOST' must not be a symlink"
fi
if [[ -e $CODEX_AUTH_FILE_HOST && ! -f $CODEX_AUTH_FILE_HOST ]]; then
error "expected Codex auth file '$CODEX_AUTH_FILE_HOST' to be a regular file"
fi
if [[ -f $CODEX_AUTH_FILE_HOST ]]; then
return 0
fi
if $DRY_RUN; then
print_command touch "$CODEX_AUTH_FILE_HOST"
return 0
fi
: > "$CODEX_AUTH_FILE_HOST"
}
fetch_latest_codex_digest() {
@@ -1014,20 +1272,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, workspace-read-only, or danger-full-access)"
error "runtime '--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, workspace-read-only, or danger-full-access)"
error "runtime flags must include '--sandbox <mode>' (workspace-write, workspace-read-only, or danger-full-access)"
fi
case "$sandbox_mode" in
workspace-write|workspace-read-only|danger-full-access)
;;
*)
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write, workspace-read-only, or danger-full-access)"
error "sandbox mode '$sandbox_mode' is not allowed (expected workspace-write, workspace-read-only, or danger-full-access)"
;;
esac
}
@@ -1060,6 +1318,8 @@ targets_need_build() {
prepare_container_runtime() {
resolve_container_workdir
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA
SLOPTRAP_PACKAGES_CAPABILITY=""
SLOPTRAP_RUN_AS_ROOT=false
SLOPTRAP_SHARED_DIR_ABS="$CODE_DIR"
if [[ ! -d $SLOPTRAP_SHARED_DIR_ABS ]]; then
error "shared directory '$SLOPTRAP_SHARED_DIR_ABS' does not exist"
@@ -1081,9 +1341,19 @@ prepare_container_runtime() {
else
SLOPTRAP_DOCKERFILE_SOURCE=""
fi
if [[ -n $REQUESTED_CAPABILITIES && -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then
error "capabilities require the embedded Dockerfile; custom Dockerfile overrides are not supported"
fi
SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps")
SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps util-linux")
validate_package_list "SLOPTRAP_PACKAGES" "$SLOPTRAP_PACKAGES_BASE" "SLOPTRAP_PACKAGES"
if capability_list_contains "$REQUESTED_CAPABILITIES" "packet-capture"; then
SLOPTRAP_PACKAGES_CAPABILITY+=" tcpdump"
fi
if capability_list_contains "$REQUESTED_CAPABILITIES" "nested-podman"; then
SLOPTRAP_PACKAGES_CAPABILITY+=" podman fuse-overlayfs slirp4netns"
fi
SLOPTRAP_PACKAGES_CAPABILITY=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY")
local default_codex_archive
default_codex_archive=$(detect_codex_archive_name)
local env_codex_archive
@@ -1130,7 +1400,8 @@ prepare_container_runtime() {
SLOPTRAP_LIMITS_SHM=$(get_env_default "SLOPTRAP_LIMITS_SHM" "1024m")
SLOPTRAP_LIMITS_CPU=$(get_env_default "SLOPTRAP_LIMITS_CPU" "8")
SLOPTRAP_TMPFS_PATHS=$(get_env_default "SLOPTRAP_TMPFS_PATHS" "/tmp:exec /run /run/lock")
SLOPTRAP_ROOTFS_READONLY=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1")
SLOPTRAP_ROOTFS_READONLY_DEFAULT=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1")
SLOPTRAP_ROOTFS_READONLY=$SLOPTRAP_ROOTFS_READONLY_DEFAULT
SLOPTRAP_IMAGE_NAME=$(get_env_default "SLOPTRAP_IMAGE_NAME" "${PROJECT_NAME}-sloptrap-image")
SLOPTRAP_CONTAINER_NAME=$(get_env_default "SLOPTRAP_CONTAINER_NAME" "${PROJECT_NAME}-sloptrap-container")
SLOPTRAP_IMAGE_NAME=$(sanitize_engine_name "$SLOPTRAP_IMAGE_NAME")
@@ -1138,6 +1409,7 @@ prepare_container_runtime() {
local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME" --init)
local -a security_opts=(--cap-drop=ALL --security-opt no-new-privileges)
local -a capability_opts=()
if [[ -n $SLOPTRAP_SECURITY_OPTS_EXTRA ]]; then
local -a extra_opts=()
read -r -a extra_opts <<< "$SLOPTRAP_SECURITY_OPTS_EXTRA"
@@ -1160,6 +1432,18 @@ prepare_container_runtime() {
done
fi
if capability_list_contains "$ENABLED_CAPABILITIES" "apt-install"; then
SLOPTRAP_ROOTFS_READONLY=0
SLOPTRAP_RUN_AS_ROOT=true
fi
if capability_list_contains "$ENABLED_CAPABILITIES" "packet-capture"; then
capability_opts+=(--cap-add NET_RAW --cap-add NET_ADMIN)
SLOPTRAP_RUN_AS_ROOT=true
fi
if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then
capability_opts+=(--device /dev/fuse)
fi
local rootfs_flag=()
case "${SLOPTRAP_ROOTFS_READONLY,,}" in
1|true|yes)
@@ -1175,8 +1459,16 @@ prepare_container_runtime() {
local -a volume_opts=(
-v "$SLOPTRAP_SHARED_DIR_ABS:$SLOPTRAP_WORKDIR$SLOPTRAP_VOLUME_LABEL"
-v "$CODEX_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL"
-v "$CODEX_STATE_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL"
-v "$CODEX_AUTH_FILE_HOST:$SLOPTRAP_CODEX_HOME_CONT/auth.json$SLOPTRAP_VOLUME_LABEL"
)
if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then
volume_opts+=(
-v "$CAPABILITY_STATE_HOST/podman-storage:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/storage$SLOPTRAP_VOLUME_LABEL"
-v "$CAPABILITY_STATE_HOST/podman-run:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/run$SLOPTRAP_VOLUME_LABEL"
-v "$CAPABILITY_STATE_HOST/podman-runtime:$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/runtime$SLOPTRAP_VOLUME_LABEL"
)
fi
local -a env_args=(
-e "HOME=$SLOPTRAP_CODEX_HOME_CONT"
@@ -1184,7 +1476,18 @@ prepare_container_runtime() {
-e "XDG_CACHE_HOME=$SLOPTRAP_CODEX_HOME_CONT/cache"
-e "XDG_STATE_HOME=$SLOPTRAP_CODEX_HOME_CONT/state"
-e "CODEX_HOME=$SLOPTRAP_CODEX_HOME_CONT"
-e "SLOPTRAP_WORKDIR=$SLOPTRAP_WORKDIR"
-e "SLOPTRAP_HELPER_DIR=/run/sloptrap-helper"
-e "SLOPTRAP_ACTIVE_CAPABILITIES=$ENABLED_CAPABILITIES"
-e "SLOPTRAP_CAPTURE_DIR=$SLOPTRAP_CODEX_HOME_CONT/state/captures"
-e "SLOPTRAP_AUDIT_LOG=$SLOPTRAP_CODEX_HOME_CONT/state/capabilities.log"
-e "SLOPTRAP_INNER_PODMAN_ROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/storage"
-e "SLOPTRAP_INNER_PODMAN_RUNROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/run"
-e "XDG_RUNTIME_DIR=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/runtime"
)
if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman" && [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then
env_args+=(-e "SLOPTRAP_INNER_PODMAN_HOST_NETWORK=1")
fi
local uid gid
uid=$(id -u)
@@ -1193,10 +1496,14 @@ prepare_container_runtime() {
if [[ $CONTAINER_ENGINE == "podman" ]]; then
user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}")
fi
if $SLOPTRAP_RUN_AS_ROOT; then
user_opts=()
fi
CONTAINER_SHARED_OPTS=(
"${network_opts[@]}"
"${security_opts[@]}"
"${capability_opts[@]}"
"${resource_opts[@]}"
"${rootfs_flag[@]}"
"${tmpfs_opts[@]}"
@@ -1215,13 +1522,16 @@ prepare_container_runtime() {
}
build_image() {
ensure_capability_trust
ensure_codex_binary
if [[ $SKIP_BUILD_BANNER != true ]]; then
print_banner
fi
print_manifest_summary
local extra_packages_arg
local capability_packages_arg
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
capability_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY")
if ! $DRY_RUN; then
status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME"
fi
@@ -1231,6 +1541,7 @@ build_image() {
-f "$SLOPTRAP_DOCKERFILE_PATH"
--label "$SLOPTRAP_IMAGE_LABEL"
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
--build-arg "CAPABILITY_PACKAGES=$capability_packages_arg"
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
@@ -1252,9 +1563,11 @@ build_image() {
if [[ -n $build_output ]]; then
comment_line "Image %s\n" "$build_output"
fi
write_capability_build_stamp
}
rebuild_image() {
ensure_capability_trust
ensure_codex_binary
if [[ $SKIP_BUILD_BANNER != true ]]; then
print_banner
@@ -1264,13 +1577,16 @@ rebuild_image() {
status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME"
fi
local extra_packages_arg
local capability_packages_arg
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
capability_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY")
local -a cmd=(
"$CONTAINER_ENGINE" build --no-cache --quiet
-t "$SLOPTRAP_IMAGE_NAME"
-f "$SLOPTRAP_DOCKERFILE_PATH"
--label "$SLOPTRAP_IMAGE_LABEL"
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
--build-arg "CAPABILITY_PACKAGES=$capability_packages_arg"
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
@@ -1292,14 +1608,21 @@ rebuild_image() {
if [[ -n $build_output ]]; then
comment_line "Image %s\n" "$build_output"
fi
write_capability_build_stamp
}
build_if_missing() {
ensure_capability_trust
if $DRY_RUN; then
print_command "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME"
return 0
fi
if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then
if ! capability_build_stamp_matches_current; then
warn "image '$SLOPTRAP_IMAGE_NAME' capability stamp mismatch; rebuilding"
rebuild_image
return 0
fi
local created
if created=$("$CONTAINER_ENGINE" image inspect --format '{{.Created}}' "$SLOPTRAP_IMAGE_NAME" 2>/dev/null); then
local created_epoch
@@ -1361,7 +1684,8 @@ prune_sloptrap_images() {
run_codex_command() {
local -a extra_args=("$@")
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME")
ensure_codex_storage_paths
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" "codex")
if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then
cmd+=("${CODEX_ARGS_ARRAY[@]}")
fi
@@ -1375,22 +1699,26 @@ run_codex() {
if ! $DRY_RUN; then
status_line "Running %s\n" "$SLOPTRAP_IMAGE_NAME"
fi
run_codex_command
local runtime_prompt
runtime_prompt=$(build_runtime_context_prompt)
run_codex_command "$runtime_prompt"
}
run_login_target() {
ensure_codex_storage_paths
if ! $DRY_RUN; then
status_line "Login %s\n" "$SLOPTRAP_IMAGE_NAME"
fi
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" login)
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" "codex" login)
run_or_print "${cmd[@]}"
}
run_shell_target() {
ensure_codex_storage_paths
if ! $DRY_RUN; then
status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME"
fi
local -a cmd=("${BASE_CONTAINER_CMD[@]}" --entrypoint /bin/bash "$SLOPTRAP_IMAGE_NAME")
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" /bin/bash)
run_or_print "${cmd[@]}"
}
@@ -1429,15 +1757,14 @@ dispatch_target() {
;;
login)
build_if_missing
ensure_codex_home_dir
run_login_target
;;
shell)
build_if_missing
run_shell_target
;;
wizzard)
run_wizzard "$MANIFEST_PATH"
wizard)
run_wizard "$MANIFEST_PATH"
exit 0
;;
stop)
@@ -1458,6 +1785,7 @@ dispatch_target() {
DRY_RUN=false
PRINT_CONFIG=false
SKIP_BUILD_BANNER=false
TRUST_CAPABILITIES=false
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -1469,6 +1797,10 @@ while [[ $# -gt 0 ]]; do
PRINT_CONFIG=true
shift
;;
--trust-capabilities)
TRUST_CAPABILITIES=true
shift
;;
-h|--help)
usage
exit 0
@@ -1514,11 +1846,11 @@ 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[$target_index]} == "wizard" ]]; then
if (( ${#TARGETS_INPUT[@]} > 1 )); then
warn "wizzard runs standalone; ignoring other targets"
warn "wizard runs standalone; ignoring other targets"
fi
run_wizzard "$MANIFEST_PATH"
run_wizard "$MANIFEST_PATH"
exit 0
fi
((target_index+=1))
@@ -1528,13 +1860,13 @@ fi
if [[ ! -f $MANIFEST_PATH ]]; then
if targets_need_build "${TARGETS_INPUT[@]}"; then
if [[ -t 0 ]]; then
run_wizzard "$MANIFEST_PATH"
run_wizard "$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)"
warn "missing $MANIFEST_BASENAME; proceeding with defaults (run '$0 $CODE_DIR wizard' to create one)"
fi
fi
fi
@@ -1548,18 +1880,18 @@ if [[ ! $PROJECT_NAME =~ $VALID_NAME_REGEX ]]; then
fi
select_codex_home "$CODE_DIR"
select_capability_state_paths
CAPABILITY_MANIFEST_DIGEST=$(compute_manifest_digest)
ensure_ignore_helper_root
IGNORE_STUB_BASE="$IGNORE_HELPER_ROOT/session-${BASHPID:-$$}"
resolve_sloptrap_ignore "$CODE_DIR"
resolve_container_workdir
NEED_LOGIN=false
if [[ $CODEX_HOME_BOOTSTRAP == true ]]; then
NEED_LOGIN=true
elif [[ ! -f "$CODEX_HOME_HOST/auth.json" ]]; then
if [[ ! -s "$CODEX_AUTH_FILE_HOST" ]]; then
NEED_LOGIN=true
fi
TARGETS=("$@")
TARGETS=("${TARGETS_INPUT[@]}")
if [[ ${#TARGETS[@]} -eq 0 ]]; then
TARGETS=("run")
fi
@@ -1567,6 +1899,16 @@ fi
DEFAULT_TARGETS=("${TARGETS[@]}")
PACKAGES_EXTRA=${MANIFEST[packages_extra]-}
REQUESTED_CAPABILITIES=$(normalize_capability_list "${MANIFEST[capabilities]-}")
if [[ -n $REQUESTED_CAPABILITIES ]]; then
ensure_safe_for_make "capabilities" "$REQUESTED_CAPABILITIES"
fi
validate_capability_list "capabilities" "$REQUESTED_CAPABILITIES"
if [[ -n $REQUESTED_CAPABILITIES ]] && { $TRUST_CAPABILITIES || capability_trust_matches_current; }; then
ENABLED_CAPABILITIES="$REQUESTED_CAPABILITIES"
else
ENABLED_CAPABILITIES=""
fi
if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
case "${MANIFEST[allow_host_network],,}" in
1|true|yes)
@@ -1581,9 +1923,12 @@ if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
esac
fi
forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough default_targets default_target)
forbidden_keys=(codex_args 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
if [[ $forbidden_key == "codex_args" ]]; then
error "$MANIFEST_PATH: key 'codex_args' has been deprecated; sloptrap now always uses '$DEFAULT_CODEX_ARGS_DISPLAY'"
fi
error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons"
fi
done
@@ -1594,43 +1939,8 @@ if [[ -n $PACKAGES_EXTRA ]]; then
fi
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]}"
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=()
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, workspace-read-only, or danger-full-access)"
fi
sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}")
((codex_args_index+=2))
continue
fi
sanitized_codex_args+=("${CODEX_ARGS_ARRAY[$codex_args_index]}")
((codex_args_index+=1))
done
if [[ ${#sandbox_pair[@]} -gt 0 ]]; then
sanitized_codex_args+=("${sandbox_pair[@]}")
fi
CODEX_ARGS_ARRAY=("${sanitized_codex_args[@]}")
unset -v sanitized_codex_args sandbox_pair codex_args_index
ensure_safe_sandbox "${CODEX_ARGS_ARRAY[@]}"
if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then
CODEX_ARGS_DISPLAY=$(printf '%s ' "${CODEX_ARGS_ARRAY[@]}")
CODEX_ARGS_DISPLAY=${CODEX_ARGS_DISPLAY% }
else
CODEX_ARGS_DISPLAY=""
fi
CODEX_ARGS_DISPLAY=$DEFAULT_CODEX_ARGS_DISPLAY
prepare_ignore_mounts "$CODE_DIR"
prepare_container_runtime
@@ -1653,9 +1963,8 @@ fi
if $AUTO_LOGIN; then
if ! $DRY_RUN; then
status_line "Codex login required (%s)\n" "$CODEX_HOME_HOST"
status_line "Codex login required (%s)\n" "$CODEX_AUTH_FILE_HOST"
fi
ensure_codex_home_dir
dispatch_target login
fi

30
sloptrap-entrypoint Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
helper_pid=""
cleanup() {
if [[ -n $helper_pid ]]; then
kill "$helper_pid" >/dev/null 2>&1 || true
wait "$helper_pid" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT INT TERM HUP
if [[ $# -eq 0 ]]; then
set -- codex
fi
if [[ $(id -u) -eq 0 ]]; then
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
mkdir -p "$helper_dir/queue"
chmod 700 "$helper_dir"
if [[ -n ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then
/usr/local/bin/sloptrap-helperd &
helper_pid=$!
fi
exec runuser -u sloptrap --preserve-environment -- "$@"
fi
exec "$@"

146
sloptrap-helperd Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper}
queue_dir="$helper_dir/queue"
caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-}
audit_log=${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}
mkdir -p "$queue_dir" "$(dirname "$audit_log")"
has_capability() {
local needle=$1
local token
for token in $caps; do
if [[ $token == "$needle" ]]; then
return 0
fi
done
return 1
}
log_action() {
local op=$1
local details=$2
local status=$3
printf '%s op=%s status=%s %s\n' "$(date -u +%FT%TZ)" "$op" "$status" "$details" >>"$audit_log"
}
write_status() {
local request_dir=$1
local status=$2
printf '%s\n' "$status" >"$request_dir/status"
}
run_apt_install() {
local request_dir=$1
has_capability "apt-install" || {
printf 'capability apt-install is not active\n' >"$request_dir/stderr"
write_status "$request_dir" 126
log_action "apt-install" "packages=denied" 126
return
}
local packages_file="$request_dir/packages"
if [[ ! -f $packages_file ]]; then
printf 'missing package list\n' >"$request_dir/stderr"
write_status "$request_dir" 2
log_action "apt-install" "packages=missing" 2
return
fi
mapfile -t packages <"$packages_file"
if [[ ${#packages[@]} -eq 0 ]]; then
printf 'package list is empty\n' >"$request_dir/stderr"
write_status "$request_dir" 2
log_action "apt-install" "packages=empty" 2
return
fi
if apt-get update >"$request_dir/stdout" 2>"$request_dir/stderr" \
&& apt-get install -y --no-install-recommends "${packages[@]}" >>"$request_dir/stdout" 2>>"$request_dir/stderr"; then
write_status "$request_dir" 0
log_action "apt-install" "packages=${packages[*]}" 0
return
fi
write_status "$request_dir" 1
log_action "apt-install" "packages=${packages[*]}" 1
}
run_packet_capture() {
local request_dir=$1
has_capability "packet-capture" || {
printf 'capability packet-capture is not active\n' >"$request_dir/stderr"
write_status "$request_dir" 126
log_action "packet-capture" "interface=denied" 126
return
}
local iface_file="$request_dir/interface"
[[ -f $iface_file ]] || {
printf 'missing interface\n' >"$request_dir/stderr"
write_status "$request_dir" 2
log_action "packet-capture" "interface=missing" 2
return
}
local iface filter_file output_file stdout_mode
iface=$(<"$iface_file")
filter_file="$request_dir/filter"
output_file="$request_dir/output"
stdout_mode=0
[[ -f "$request_dir/stdout_mode" ]] && stdout_mode=$(<"$request_dir/stdout_mode")
local -a cmd=(tcpdump -i "$iface")
if [[ -s $filter_file ]]; then
local filter
filter=$(<"$filter_file")
local -a filter_tokens=()
read -r -a filter_tokens <<< "$filter"
cmd+=("${filter_tokens[@]}")
fi
if [[ -s $output_file ]]; then
local capture_path
capture_path=$(<"$output_file")
mkdir -p "$(dirname "$capture_path")"
cmd+=(-w "$capture_path")
fi
if [[ $stdout_mode == "1" ]]; then
"${cmd[@]}" >"$request_dir/stdout" 2>"$request_dir/stderr" || {
write_status "$request_dir" 1
log_action "packet-capture" "interface=$iface stdout=1" 1
return
}
else
"${cmd[@]}" >"$request_dir/stdout" 2>"$request_dir/stderr" || {
write_status "$request_dir" 1
log_action "packet-capture" "interface=$iface stdout=0" 1
return
}
fi
write_status "$request_dir" 0
log_action "packet-capture" "interface=$iface stdout=$stdout_mode" 0
}
while true; do
shopt -s nullglob
request_dirs=("$queue_dir"/*.req)
shopt -u nullglob
if [[ ${#request_dirs[@]} -eq 0 ]]; then
sleep 1
continue
fi
for request_dir in "${request_dirs[@]}"; do
[[ -d $request_dir ]] || continue
[[ ! -f "$request_dir/status" ]] || continue
op=$(<"$request_dir/op")
: >"$request_dir/stdout"
: >"$request_dir/stderr"
case "$op" in
apt-install)
run_apt_install "$request_dir"
;;
packet-capture)
run_packet_capture "$request_dir"
;;
*)
printf 'unknown operation %s\n' "$op" >"$request_dir/stderr"
write_status "$request_dir" 2
log_action "$op" "unknown=1" 2
;;
esac
done
done

View File

@@ -11,3 +11,6 @@ Current scenarios:
- `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.
- `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.

View File

@@ -0,0 +1,3 @@
name=capability-repo
capabilities=apt-install packet-capture nested-podman
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=()
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() {
printf '==> shellcheck\n'
if ! command -v shellcheck >/dev/null 2>&1; then
@@ -154,6 +164,9 @@ if [[ ${1-} == "-c" ]]; then
exit 0
fi
fi
if [[ -x /usr/bin/sha256sum ]]; then
exec /usr/bin/sha256sum "$@"
fi
printf 'sha256sum stub encountered unsupported args: %s\n' "$*" >&2
exit 1
EOF
@@ -306,6 +319,158 @@ run_resume_target() {
teardown_stub_env
}
run_runtime_context_prompt() {
local scenario_dir="$TEST_ROOT/capability_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 -- "manifest_present=true" "$STUB_LOG" \
|| ! grep -q -- "manifest_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG" \
|| ! grep -q -- "trusted_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG" \
|| ! grep -q -- "enabled_capabilities=apt-install nested-podman packet-capture" "$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/capability_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/capability_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" "$STUB_LOG"; then
record_failure "auth_file_mount: missing auth file bind mount"
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_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 ! grep -q -- "-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then
record_failure "auto_login_empty_auth: missing auth file bind mount"
fi
teardown_stub_env
}
run_codex_symlink_home() {
local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
@@ -407,6 +572,14 @@ run_invalid_manifest_packages() {
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() {
local scenario_dir="$TEST_ROOT/invalid_allow_host_network"
printf '==> invalid_allow_host_network\n'
@@ -415,83 +588,131 @@ run_invalid_allow_host_network() {
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'
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
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"
local input=$'\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then
record_failure "wizard_create_manifest: wizard failed"
return
fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_create_manifest: manifest not created"
record_failure "wizard_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"
if ! grep -qx "name=wizard_empty" "$scenario_dir/.sloptrap"; then
record_failure "wizard_create_manifest: name default mismatch"
fi
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
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"
if ! grep -qx "capabilities=" "$scenario_dir/.sloptrap"; then
record_failure "wizard_create_manifest: capabilities mismatch"
fi
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
}
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'
run_wizard_existing_defaults() {
local scenario_dir="$TEST_ROOT/wizard_existing"
printf '==> wizard_existing_defaults\n'
if ! can_run_script_pty; then
printf 'skipping wizard_existing_defaults: script PTY support not available\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"
local input=$'\n\n\n\n'
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then
record_failure "wizard_existing_defaults: wizard failed"
return
fi
if ! grep -qx "name=custom-wizzard" "$scenario_dir/.sloptrap"; then
record_failure "wizzard_existing_defaults: name not preserved"
if ! grep -qx "name=custom-wizard" "$scenario_dir/.sloptrap"; then
record_failure "wizard_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"
record_failure "wizard_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"
if ! grep -qx "capabilities=apt-install packet-capture" "$scenario_dir/.sloptrap"; then
record_failure "wizard_existing_defaults: capabilities not preserved"
fi
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
}
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'
run_wizard_build_trigger() {
local scenario_dir="$TEST_ROOT/wizard_build"
printf '==> wizard_build_trigger\n'
if ! can_run_script_pty; then
printf 'skipping wizard_build_trigger: script PTY support not available\n'
return
fi
setup_stub_env
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
record_failure "wizzard_build_trigger: sloptrap failed"
record_failure "wizard_build_trigger: sloptrap failed"
teardown_stub_env
return
fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizzard_build_trigger: manifest not created"
record_failure "wizard_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"
record_failure "wizard_build_trigger: build not invoked after wizard"
fi
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_capability_profiles() {
local scenario_dir="$TEST_ROOT/capability_repo"
printf '==> capability_profiles\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 "capability_profiles: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "CAPABILITY_PACKAGES=tcpdump podman fuse-overlayfs slirp4netns" "$STUB_LOG"; then
record_failure "capability_profiles: build arg for capability packages missing"
fi
if ! grep -q -- "--cap-add NET_RAW" "$STUB_LOG"; then
record_failure "capability_profiles: NET_RAW capability missing"
fi
if ! grep -q -- "--cap-add NET_ADMIN" "$STUB_LOG"; then
record_failure "capability_profiles: NET_ADMIN capability missing"
fi
if ! grep -q -- "--device /dev/fuse" "$STUB_LOG"; then
record_failure "capability_profiles: /dev/fuse device missing"
fi
if grep -q -- "--read-only" "$STUB_LOG"; then
record_failure "capability_profiles: apt profile should disable read-only rootfs"
fi
if grep -q -- "--user " "$STUB_LOG"; then
record_failure "capability_profiles: capability-enabled run should not force --user"
fi
if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=apt-install nested-podman packet-capture" "$STUB_LOG"; then
record_failure "capability_profiles: active capability environment missing"
fi
if ! grep -q -- "SLOPTRAP_INNER_PODMAN_HOST_NETWORK=1" "$STUB_LOG"; then
record_failure "capability_profiles: inner podman host-network mirror flag missing"
fi
teardown_stub_env
}
@@ -504,6 +725,12 @@ run_manifest_injection
run_helper_symlink
run_secret_mask
run_resume_target
run_runtime_context_prompt
run_sh_reexec
run_resume_omits_runtime_context
run_auth_file_mount
run_project_state_isolation
run_auto_login_empty_auth
run_codex_symlink_home
run_root_directory_project
run_shared_dir_override
@@ -513,10 +740,13 @@ run_dotdot_ignore
run_invalid_manifest_name
run_invalid_manifest_sandbox
run_invalid_manifest_packages
run_invalid_manifest_capabilities
run_invalid_allow_host_network
run_wizzard_create_manifest
run_wizzard_existing_defaults
run_wizzard_build_trigger
run_wizard_create_manifest
run_wizard_existing_defaults
run_wizard_build_trigger
run_capability_trust_required
run_capability_profiles
if [[ ${#failures[@]} -gt 0 ]]; then
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 packet-capture
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