Harden launcher overrides and fix opencode backend regressions
- remove codex auth mounts from opencode run/shell paths - reject opencode login and invalid backend values - harden opencode config writes against symlink clobbering - fix opencode build args and packages_extra handling - enforce cap-drop and read-only rootfs in runtime commands - reject dangerous runtime/build env overrides - update README and test docs to match actual behavior - extend regression coverage for backend safety and hardening
This commit is contained in:
28
README.md
28
README.md
@@ -34,17 +34,17 @@ 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`, prepares a per-project state directory, and runs `login` if `${HOME}/.codex/auth.json` is missing or empty.
|
||||
- verifies the selected backend CLI hash,
|
||||
- creates `${HOME}/.codex`, prepares a per-project state directory, and runs `login` if `${HOME}/.codex/auth.json` is missing or empty for the Codex backend.
|
||||
|
||||
> 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`; 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`.
|
||||
- The project directory mounts at `/workspace`; project-scoped state mounts at `/codex` from `${HOME}/.codex/sloptrap/state/<project-hash>`. Codex also mounts shared auth from `${HOME}/.codex/auth.json` to `/codex/auth.json`; opencode does not.
|
||||
- `.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`.
|
||||
- The helper Dockerfile is embedded inside `sloptrap`. 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 container user matches the host UID/GID (`--userns=keep-id` on Podman or `--user UID:GID` on Docker).
|
||||
- The runtime environment is fixed to HOME/XDG variables pointing at `/codex`; manifest-controlled environment injection is disabled.
|
||||
|
||||
@@ -74,7 +74,7 @@ Values containing `$`, `` ` ``, or newlines are rejected to prevent command inje
|
||||
|
||||
**Codex** (default): Uses OpenAI Codex CLI with state stored in `~/.codex/`. Supports login mode for credential sharing.
|
||||
|
||||
**opencode**: Uses Anomaly opencode CLI with state stored in `~/.opencode/`. sloptrap downloads the latest Linux CLI release artifact from Anomaly during image builds, verifies its digest from the GitHub release metadata, and copies it into the container image. Connects to any OpenAI-compatible inference server (llama.cpp, Ollama, vLLM, etc.). When `opencode_server` points at `localhost` under isolated networking, sloptrap rewrites it to `http://sloptrap.host:...` so host-local model servers remain reachable from inside the container. No authentication required for self-hosted models; API keys supported via manifest if needed.
|
||||
**opencode**: Uses Anomaly opencode CLI with project-scoped state and config stored under sloptrap's per-project state directory. sloptrap downloads the latest Linux CLI release artifact from Anomaly during image builds, verifies its digest from the GitHub release metadata, and copies it into the container image. Connects to any OpenAI-compatible inference server (llama.cpp, Ollama, vLLM, etc.). When `opencode_server` points at `localhost` under isolated networking, sloptrap rewrites it to `http://sloptrap.host:...` so host-local model servers remain reachable from inside the container. No Codex auth file is mounted for opencode sessions.
|
||||
|
||||
### `.sloptrapignore`
|
||||
|
||||
@@ -100,8 +100,12 @@ Environment variables override manifest values:
|
||||
- `SLOPTRAP_AGENT` — override `agent` key (codex or opencode)
|
||||
- `SLOPTRAP_OPENCODE_SERVER` — override `opencode_server` key
|
||||
- `SLOPTRAP_OPENCODE_MODEL` — override `opencode_model` key
|
||||
- `SLOPTRAP_OPENCODE_CONTEXT` — override `opencode_context` key
|
||||
- `SLOPTRAP_CONTAINER_ENGINE` — override container engine auto-detection
|
||||
|
||||
Security-sensitive runtime overrides such as `SLOPTRAP_SECURITY_OPTS_EXTRA`, `SLOPTRAP_ROOTFS_READONLY`, and `SLOPTRAP_NETWORK_NAME` are rejected.
|
||||
Build-path overrides such as `SLOPTRAP_DOCKERFILE_PATH`, `SLOPTRAP_CODEX_URL`, `SLOPTRAP_CODEX_ARCHIVE`, and `SLOPTRAP_CODEX_BIN` are also rejected.
|
||||
|
||||
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.
|
||||
@@ -123,12 +127,12 @@ Targets are supplied after the code directory. When omitted, sloptrap defaults t
|
||||
|
||||
| Target | Description |
|
||||
| --- | --- |
|
||||
| `build` | Download Codex (if missing), verify SHA-256, and build the container image. |
|
||||
| `build` | Download the selected backend CLI (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 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 shared `${HOME}/.codex/auth.json` credentials. |
|
||||
| `run` | Default goal. Runs the container with the selected backend. Codex uses sloptrap's built-in runtime flags; opencode relies on its generated config. |
|
||||
| `resume <session-id>` | Continues a backend session inside the container (Codex uses `codex resume`; opencode uses its session flag). |
|
||||
| `login` | Starts Codex in login mode to bootstrap shared `${HOME}/.codex/auth.json` credentials. Not supported for opencode. |
|
||||
| `shell` | Launches `/bin/bash` inside the container for debugging. |
|
||||
| `wizard` | Creates or updates `.sloptrap` interactively (no build); rerun `build` or `rebuild` afterward. |
|
||||
| `stop` | Best-effort stop of the running container (if any). |
|
||||
@@ -141,7 +145,7 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo
|
||||
- Container engine: Podman or Docker for standard runs. Podman uses `--userns=keep-id`; Docker receives the equivalent `--user UID:GID` for standard runs.
|
||||
- Filesystem view:
|
||||
- **Codex**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/<project-hash>` at `/codex`; auth at `/codex/auth.json`.
|
||||
- **opencode**: project directory at `/workspace`; `${HOME}/.opencode/sloptrap/state/<project-hash>` at `/codex/state/opencode`; state at `/codex/state`.
|
||||
- **opencode**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/<project-hash>` at `/codex`; generated config at `/codex/config/opencode/opencode.json`; runtime state at `/codex/state/opencode`; no shared auth mount.
|
||||
- Ignore filter: `.sloptrapignore` entries are overlaid with tmpfs directories or empty bind mounts so data remains unavailable to the agent.
|
||||
- Network: isolated networking is used by default; `allow_host_network=true` opts into `--network host`. For isolated runs, sloptrap injects `sloptrap.host` as a container-side hostname for the host gateway. On Podman `slirp4netns`, opencode runs also enable host loopback access so host-local servers bound to `localhost` remain reachable.
|
||||
- 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.
|
||||
@@ -152,12 +156,12 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo
|
||||
## 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`, 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.
|
||||
- **Shared storage**: `/workspace` and project-scoped `/codex` are host mounts. For Codex, `/codex/auth.json` is also mounted from the host; opencode sessions do not receive that shared credential file. Files written to mounted locations become visible on the host and may be surfaced to the configured 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**: standard runs keep a read-only root filesystem and no extra Linux capabilities.
|
||||
- **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.
|
||||
- **Codex cache hygiene**: per-project state mounts remain writable by the container and hold prompts/history/state, while `${HOME}/.codex/auth.json` holds shared Codex credentials when that backend is used. 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.
|
||||
|
||||
|
||||
79
sloptrap
79
sloptrap
@@ -174,7 +174,7 @@ usage() {
|
||||
info_line "Example manifest entries:\n"
|
||||
comment_line " name=my-project\n"
|
||||
comment_line " packages_extra=kubectl helm\n"
|
||||
comment_line " capabilities=apt-install packet-capture\n"
|
||||
comment_line " agent=codex\n"
|
||||
info_line "\n"
|
||||
info_line "Example targets:\n"
|
||||
comment_line " run Build if needed, then launch Codex\n"
|
||||
@@ -328,6 +328,7 @@ FROM ${BASE_IMAGE}
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 wget ripgrep xxd file procps util-linux binutils"
|
||||
ARG EXTRA_PACKAGES=""
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG CODEX_UID=1337
|
||||
@@ -1121,7 +1122,6 @@ SLOPTRAP_CODEX_BIN_NAME=""
|
||||
SLOPTRAP_CODEX_URL=""
|
||||
SLOPTRAP_CODEX_ARCHIVE=""
|
||||
SLOPTRAP_CODEX_HOME_CONT=""
|
||||
SLOPTRAP_SECURITY_OPTS_EXTRA=""
|
||||
SLOPTRAP_VOLUME_LABEL=""
|
||||
SLOPTRAP_WORKDIR=${SLOPTRAP_WORKDIR-}
|
||||
SLOPTRAP_NETWORK_NAME=""
|
||||
@@ -1131,8 +1131,6 @@ SLOPTRAP_LIMITS_SWP=""
|
||||
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
|
||||
@@ -1296,6 +1294,12 @@ ensure_opencode_config() {
|
||||
return 0
|
||||
fi
|
||||
ensure_codex_directory "$config_dir" "project Opencode config"
|
||||
if [[ -L $OPENCODE_CONFIG_HOST ]]; then
|
||||
error "Opencode config '$OPENCODE_CONFIG_HOST' must not be a symlink"
|
||||
fi
|
||||
if [[ -e $OPENCODE_CONFIG_HOST && ! -f $OPENCODE_CONFIG_HOST ]]; then
|
||||
error "expected Opencode config '$OPENCODE_CONFIG_HOST' to be a regular file"
|
||||
fi
|
||||
if ! jq -n \
|
||||
--arg schema "https://opencode.ai/config.json" \
|
||||
--arg provider_id "$provider_id" \
|
||||
@@ -1541,6 +1545,14 @@ normalize_package_list() {
|
||||
printf '%s' "${normalized[*]}"
|
||||
}
|
||||
|
||||
reject_removed_env_override() {
|
||||
local var=$1
|
||||
local reason=$2
|
||||
if printenv "$var" >/dev/null 2>&1; then
|
||||
error "environment override '$var' has been removed for security reasons (${reason})"
|
||||
fi
|
||||
}
|
||||
|
||||
targets_need_build() {
|
||||
local -a targets=("$@")
|
||||
if [[ ${#targets[@]} -eq 0 ]]; then
|
||||
@@ -1641,7 +1653,6 @@ prepare_container_runtime() {
|
||||
fi
|
||||
SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337")
|
||||
SLOPTRAP_CODEX_GID=$(get_env_default "SLOPTRAP_CODEX_GID" "1337")
|
||||
SLOPTRAP_SECURITY_OPTS_EXTRA=$(get_env_default "SLOPTRAP_SECURITY_OPTS_EXTRA" "")
|
||||
local default_network="bridge"
|
||||
if [[ $CONTAINER_ENGINE == "podman" ]]; then
|
||||
if command -v slirp4netns >/dev/null 2>&1; then
|
||||
@@ -1650,11 +1661,9 @@ prepare_container_runtime() {
|
||||
warn "podman detected but 'slirp4netns' is missing; falling back to 'bridge' networking"
|
||||
fi
|
||||
fi
|
||||
SLOPTRAP_NETWORK_NAME=$(get_env_default "SLOPTRAP_NETWORK_NAME" "$default_network")
|
||||
SLOPTRAP_NETWORK_NAME="$default_network"
|
||||
if $ALLOW_HOST_NETWORK; then
|
||||
SLOPTRAP_NETWORK_NAME="host"
|
||||
elif [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then
|
||||
error "$MANIFEST_PATH: host networking requires allow_host_network=true"
|
||||
fi
|
||||
if [[ "$BACKEND" == "opencode" && $ALLOW_HOST_NETWORK == false ]]; then
|
||||
ensure_host_loopback_network_access
|
||||
@@ -1665,8 +1674,6 @@ prepare_container_runtime() {
|
||||
SLOPTRAP_LIMITS_SHM=$(get_env_default "SLOPTRAP_LIMITS_SHM" "4096m")
|
||||
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_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")
|
||||
@@ -1677,11 +1684,6 @@ prepare_container_runtime() {
|
||||
ensure_opencode_storage_paths
|
||||
fi
|
||||
|
||||
if [[ -n $SLOPTRAP_SECURITY_OPTS_EXTRA ]]; then
|
||||
local -a extra_opts=()
|
||||
read -r -a extra_opts <<< "$SLOPTRAP_SECURITY_OPTS_EXTRA"
|
||||
security_opts+=("${extra_opts[@]}")
|
||||
fi
|
||||
local -a resource_opts=(
|
||||
--pids-limit "$SLOPTRAP_LIMITS_PID"
|
||||
--memory "$SLOPTRAP_LIMITS_RAM"
|
||||
@@ -1699,13 +1701,8 @@ prepare_container_runtime() {
|
||||
done
|
||||
fi
|
||||
|
||||
security_opts+=(--security-opt no-new-privileges)
|
||||
local rootfs_flag=()
|
||||
case "${SLOPTRAP_ROOTFS_READONLY,,}" in
|
||||
1|true|yes)
|
||||
rootfs_flag=(--read-only)
|
||||
;;
|
||||
esac
|
||||
security_opts+=(--cap-drop ALL --security-opt no-new-privileges)
|
||||
local -a rootfs_flag=(--read-only)
|
||||
|
||||
if [[ $CONTAINER_ENGINE == "podman" ]]; then
|
||||
SLOPTRAP_VOLUME_LABEL=":Z"
|
||||
@@ -1813,6 +1810,10 @@ build_image() {
|
||||
if ! $DRY_RUN; then
|
||||
status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME"
|
||||
fi
|
||||
local binary_build_arg_name="CODEX_BIN"
|
||||
if [[ "$BACKEND" == "opencode" ]]; then
|
||||
binary_build_arg_name="OPENCODE_BIN"
|
||||
fi
|
||||
local -a cmd=(
|
||||
"$CONTAINER_ENGINE" build --quiet
|
||||
-t "$SLOPTRAP_IMAGE_NAME"
|
||||
@@ -1820,7 +1821,7 @@ build_image() {
|
||||
--network "$SLOPTRAP_NETWORK_NAME"
|
||||
--label "$SLOPTRAP_IMAGE_LABEL"
|
||||
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
|
||||
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
|
||||
--build-arg "$binary_build_arg_name=$SLOPTRAP_CODEX_BIN_NAME"
|
||||
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
|
||||
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
|
||||
)
|
||||
@@ -1859,6 +1860,10 @@ rebuild_image() {
|
||||
fi
|
||||
local extra_packages_arg
|
||||
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
|
||||
local binary_build_arg_name="CODEX_BIN"
|
||||
if [[ "$BACKEND" == "opencode" ]]; then
|
||||
binary_build_arg_name="OPENCODE_BIN"
|
||||
fi
|
||||
local -a cmd=(
|
||||
"$CONTAINER_ENGINE" build --no-cache --quiet
|
||||
-t "$SLOPTRAP_IMAGE_NAME"
|
||||
@@ -1866,7 +1871,7 @@ rebuild_image() {
|
||||
--network "$SLOPTRAP_NETWORK_NAME"
|
||||
--label "$SLOPTRAP_IMAGE_LABEL"
|
||||
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
|
||||
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
|
||||
--build-arg "$binary_build_arg_name=$SLOPTRAP_CODEX_BIN_NAME"
|
||||
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
|
||||
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
|
||||
)
|
||||
@@ -1963,8 +1968,8 @@ run_codex_command() {
|
||||
ensure_opencode_config
|
||||
else
|
||||
ensure_codex_storage_paths
|
||||
append_auth_mount_arg false auth_mount
|
||||
fi
|
||||
append_auth_mount_arg false auth_mount
|
||||
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}")
|
||||
if [[ "$BACKEND" == "opencode" ]]; then
|
||||
true
|
||||
@@ -1993,6 +1998,9 @@ run_codex() {
|
||||
}
|
||||
|
||||
run_login_target() {
|
||||
if [[ "$BACKEND" == "opencode" ]]; then
|
||||
error "target 'login' is only supported for the codex backend"
|
||||
fi
|
||||
ensure_codex_storage_paths
|
||||
local -a source_args=("$SLOPTRAP_IMAGE_NAME")
|
||||
local -a auth_mount=()
|
||||
@@ -2015,7 +2023,9 @@ run_shell_target() {
|
||||
if ! $DRY_RUN; then
|
||||
status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME"
|
||||
fi
|
||||
append_auth_mount_arg false auth_mount
|
||||
if [[ "$BACKEND" != "opencode" ]]; then
|
||||
append_auth_mount_arg false auth_mount
|
||||
fi
|
||||
local -a cmd=("${BASE_CONTAINER_CMD[@]}" --entrypoint /bin/bash "${auth_mount[@]}" "${source_args[@]}")
|
||||
run_runtime_container_cmd "${cmd[@]}"
|
||||
}
|
||||
@@ -2227,16 +2237,18 @@ select_backend() {
|
||||
|
||||
case "${manifest_agent,,}" in
|
||||
opencode) BACKEND="opencode" ;;
|
||||
codex|*) BACKEND="codex" ;;
|
||||
codex) BACKEND="codex" ;;
|
||||
*) error "$MANIFEST_PATH: agent must be 'codex' or 'opencode' (got '$manifest_agent')" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
select_backend
|
||||
|
||||
if [[ "$BACKEND" == "opencode" ]]; then
|
||||
OPENCODE_SERVER="${MANIFEST[opencode_server]:-http://localhost:8080}"
|
||||
OPENCODE_MODEL="${MANIFEST[opencode_model]:-bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0}"
|
||||
OPENCODE_CONTEXT="${MANIFEST[opencode_context]:-256K}"
|
||||
OPENCODE_SERVER=$(get_env_default "SLOPTRAP_OPENCODE_SERVER" "${MANIFEST[opencode_server]:-http://localhost:8080}")
|
||||
OPENCODE_MODEL=$(get_env_default "SLOPTRAP_OPENCODE_MODEL" "${MANIFEST[opencode_model]:-bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0}")
|
||||
OPENCODE_CONTEXT=$(get_env_default "SLOPTRAP_OPENCODE_CONTEXT" "${MANIFEST[opencode_context]:-256K}")
|
||||
parse_opencode_context "$OPENCODE_CONTEXT" >/dev/null
|
||||
OPENCODE_STATE_HOME_HOST="$CODEX_STATE_HOME_HOST/opencode"
|
||||
OPENCODE_CONFIG_HOST="$CODEX_STATE_HOME_HOST/config/opencode/opencode.json"
|
||||
OPENCODE_CONFIG_CONT=""
|
||||
@@ -2255,6 +2267,13 @@ if [[ "$BACKEND" != "opencode" && ! -s "$CODEX_AUTH_FILE_HOST" ]]; then
|
||||
fi
|
||||
|
||||
CONTAINER_ENGINE="$(detect_container_engine)"
|
||||
reject_removed_env_override "SLOPTRAP_SECURITY_OPTS_EXTRA" "arbitrary runtime security flags must not bypass launcher hardening"
|
||||
reject_removed_env_override "SLOPTRAP_ROOTFS_READONLY" "the runtime root filesystem is always mounted read-only"
|
||||
reject_removed_env_override "SLOPTRAP_NETWORK_NAME" "network selection is derived by the launcher and manifest only"
|
||||
reject_removed_env_override "SLOPTRAP_DOCKERFILE_PATH" "custom image recipes bypass the launcher hardening model"
|
||||
reject_removed_env_override "SLOPTRAP_CODEX_URL" "backend download URLs are fixed to the verified release source"
|
||||
reject_removed_env_override "SLOPTRAP_CODEX_ARCHIVE" "backend archive selection is fixed by the launcher"
|
||||
reject_removed_env_override "SLOPTRAP_CODEX_BIN" "backend binary naming is fixed by the launcher"
|
||||
CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}")
|
||||
ensure_safe_sandbox "${CODEX_ARGS_ARRAY[@]}"
|
||||
CODEX_ARGS_DISPLAY=$DEFAULT_CODEX_ARGS_DISPLAY
|
||||
|
||||
@@ -12,6 +12,7 @@ Current scenarios:
|
||||
- `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`.
|
||||
- `runtime_hardening_flags` — verifies standard runs add `--cap-drop=ALL` and keep the root filesystem read-only.
|
||||
- `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`.
|
||||
- `opencode_*` — exercises opencode build/download, localhost rewriting, config generation, and backend-specific safety checks.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name=opencode-build
|
||||
packages_extra=
|
||||
packages_extra=htop
|
||||
agent=opencode
|
||||
allow_host_network=false
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name=opencode-localhost
|
||||
packages_extra=
|
||||
agent=opencode
|
||||
opencode_server=http://localhost:8080
|
||||
allow_host_network=false
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
name=opencode-print-config
|
||||
packages_extra=
|
||||
agent=opencode
|
||||
opencode_server=http://manifest:8080
|
||||
opencode_model=manifest-model
|
||||
opencode_context=128K
|
||||
allow_host_network=false
|
||||
|
||||
@@ -477,6 +477,9 @@ run_shell_target_uses_entrypoint() {
|
||||
if ! grep -q -- "--entrypoint /bin/bash" "$STUB_LOG"; then
|
||||
record_failure "shell_target_uses_entrypoint: missing entrypoint override"
|
||||
fi
|
||||
if grep -q -- "/codex/auth.json" "$STUB_LOG"; then
|
||||
record_failure "shell_target_uses_entrypoint: codex auth mount should not be present for opencode shell"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
@@ -502,6 +505,28 @@ run_auth_file_mount() {
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_runtime_hardening_flags() {
|
||||
local scenario_dir
|
||||
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
|
||||
printf '==> runtime_hardening_flags\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 "runtime_hardening_flags: sloptrap exited non-zero"
|
||||
teardown_stub_env
|
||||
return
|
||||
fi
|
||||
if ! grep -q -- "--cap-drop ALL" "$STUB_LOG"; then
|
||||
record_failure "runtime_hardening_flags: cap-drop flag missing"
|
||||
fi
|
||||
if ! grep -q -- "--read-only" "$STUB_LOG"; then
|
||||
record_failure "runtime_hardening_flags: read-only rootfs flag missing"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_codex_home_override() {
|
||||
local scenario_dir codex_root
|
||||
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
|
||||
@@ -652,6 +677,59 @@ run_invalid_manifest_packages() {
|
||||
fi
|
||||
}
|
||||
|
||||
run_invalid_manifest_agent() {
|
||||
printf '==> invalid_manifest_agent\n'
|
||||
local scenario_dir
|
||||
scenario_dir=$(mktemp -d)
|
||||
cat >"$scenario_dir/.sloptrap" <<'EOF'
|
||||
name=invalid-agent
|
||||
agent=bogus
|
||||
EOF
|
||||
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
|
||||
record_failure "invalid_manifest_agent: expected rejection for invalid agent"
|
||||
fi
|
||||
rm -rf "$scenario_dir"
|
||||
}
|
||||
|
||||
run_invalid_agent_env_override() {
|
||||
local scenario_dir="$TEST_ROOT/opencode_print_config"
|
||||
printf '==> invalid_agent_env_override\n'
|
||||
if SLOPTRAP_AGENT=bogus "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
|
||||
record_failure "invalid_agent_env_override: expected rejection for invalid SLOPTRAP_AGENT"
|
||||
fi
|
||||
}
|
||||
|
||||
run_removed_runtime_override_envs() {
|
||||
local scenario_dir="$TEST_ROOT/resume_target"
|
||||
printf '==> removed_runtime_override_envs\n'
|
||||
if SLOPTRAP_SECURITY_OPTS_EXTRA='--privileged' "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_runtime_override_envs: expected rejection for SLOPTRAP_SECURITY_OPTS_EXTRA"
|
||||
fi
|
||||
if SLOPTRAP_ROOTFS_READONLY=0 "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_runtime_override_envs: expected rejection for SLOPTRAP_ROOTFS_READONLY"
|
||||
fi
|
||||
if SLOPTRAP_NETWORK_NAME=host "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_runtime_override_envs: expected rejection for SLOPTRAP_NETWORK_NAME"
|
||||
fi
|
||||
}
|
||||
|
||||
run_removed_build_override_envs() {
|
||||
local scenario_dir="$TEST_ROOT/resume_target"
|
||||
printf '==> removed_build_override_envs\n'
|
||||
if SLOPTRAP_DOCKERFILE_PATH=/etc/passwd "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_DOCKERFILE_PATH"
|
||||
fi
|
||||
if SLOPTRAP_CODEX_URL=https://example.invalid/codex.tgz "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_URL"
|
||||
fi
|
||||
if SLOPTRAP_CODEX_ARCHIVE=codex-custom "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_ARCHIVE"
|
||||
fi
|
||||
if SLOPTRAP_CODEX_BIN=custom-codex "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
|
||||
record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_BIN"
|
||||
fi
|
||||
}
|
||||
|
||||
run_wizard_create_manifest() {
|
||||
local scenario_dir="$TEST_ROOT/wizard_empty"
|
||||
printf '==> wizard_create_manifest\n'
|
||||
@@ -695,7 +773,7 @@ run_wizard_existing_defaults() {
|
||||
cat > "$scenario_dir/.sloptrap" <<EOF
|
||||
name=custom-wizard
|
||||
packages_extra=make git
|
||||
capabilities=apt-install
|
||||
agent=codex
|
||||
allow_host_network=true
|
||||
EOF
|
||||
# Wizard now has: name, packages_extra, agent (codex), allow_host_network
|
||||
@@ -759,7 +837,7 @@ run_opencode_build_downloads_release_cli() {
|
||||
mkdir -p "$scenario_dir"
|
||||
cat > "$scenario_dir/.sloptrap" <<'EOF'
|
||||
name=opencode-build
|
||||
packages_extra=
|
||||
packages_extra=htop
|
||||
agent=opencode
|
||||
allow_host_network=false
|
||||
EOF
|
||||
@@ -772,6 +850,12 @@ EOF
|
||||
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then
|
||||
record_failure "opencode_build_downloads_release_cli: build not invoked"
|
||||
fi
|
||||
if ! grep -q -- "--build-arg OPENCODE_BIN=opencode" "$STUB_LOG"; then
|
||||
record_failure "opencode_build_downloads_release_cli: default OPENCODE_BIN build arg missing"
|
||||
fi
|
||||
if ! grep -q -- "--build-arg EXTRA_PACKAGES=htop" "$STUB_LOG"; then
|
||||
record_failure "opencode_build_downloads_release_cli: EXTRA_PACKAGES build arg missing"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
@@ -814,6 +898,9 @@ EOF
|
||||
if ! grep -q -- "OPENCODE_CONFIG=/codex/config/opencode/opencode.json" "$STUB_LOG"; then
|
||||
record_failure "opencode_localhost_rewrite: opencode config path not exported"
|
||||
fi
|
||||
if grep -q -- "/codex/auth.json" "$STUB_LOG"; then
|
||||
record_failure "opencode_localhost_rewrite: codex auth mount should not be present for opencode"
|
||||
fi
|
||||
if ! grep -q -- '"baseURL": "http://sloptrap.host:8080/v1"' "$config_path"; then
|
||||
record_failure "opencode_localhost_rewrite: localhost server not rewritten in config"
|
||||
fi
|
||||
@@ -871,6 +958,76 @@ EOF
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_opencode_env_overrides() {
|
||||
local scenario_dir="$TEST_ROOT/opencode_print_config"
|
||||
printf '==> opencode_env_overrides\n'
|
||||
setup_stub_env
|
||||
mkdir -p "$scenario_dir"
|
||||
cat > "$scenario_dir/.sloptrap" <<'EOF'
|
||||
name=opencode-print-config
|
||||
packages_extra=
|
||||
agent=opencode
|
||||
opencode_server=http://manifest:8080
|
||||
opencode_model=manifest-model
|
||||
opencode_context=128K
|
||||
allow_host_network=false
|
||||
EOF
|
||||
local output
|
||||
local plain_output
|
||||
if ! output=$(env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
|
||||
SLOPTRAP_OPENCODE_SERVER=http://env:8080 SLOPTRAP_OPENCODE_MODEL=env-model SLOPTRAP_OPENCODE_CONTEXT=64K \
|
||||
"$SLOPTRAP_BIN" --print-config "$scenario_dir" 2>/dev/null); then
|
||||
record_failure "opencode_env_overrides: print-config failed"
|
||||
teardown_stub_env
|
||||
return
|
||||
fi
|
||||
plain_output=$(printf '%s' "$output" | sed -E $'s/\x1B\\[[0-9;]*m//g')
|
||||
if ! grep -q 'opencode_server=http://env:8080' <<<"$plain_output"; then
|
||||
record_failure "opencode_env_overrides: server env override missing"
|
||||
fi
|
||||
if ! grep -q 'opencode_model=env-model' <<<"$plain_output"; then
|
||||
record_failure "opencode_env_overrides: model env override missing"
|
||||
fi
|
||||
if ! grep -q 'opencode_context=64K' <<<"$plain_output"; then
|
||||
record_failure "opencode_env_overrides: context env override missing"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_opencode_config_symlink_rejected() {
|
||||
local scenario_dir="$TEST_ROOT/opencode_localhost"
|
||||
printf '==> opencode_config_symlink_rejected\n'
|
||||
setup_stub_env
|
||||
mkdir -p "$scenario_dir"
|
||||
cat > "$scenario_dir/.sloptrap" <<'EOF'
|
||||
name=opencode-localhost
|
||||
packages_extra=
|
||||
agent=opencode
|
||||
allow_host_network=false
|
||||
EOF
|
||||
local state_key
|
||||
state_key=$(printf '%s' "$scenario_dir" | sha256sum | awk '{print $1}')
|
||||
local config_dir="$STUB_HOME/.codex/sloptrap/state/$state_key/config/opencode"
|
||||
mkdir -p "$config_dir"
|
||||
ln -s "$STUB_HOME/target.json" "$config_dir/opencode.json"
|
||||
if 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 "opencode_config_symlink_rejected: expected rejection for symlinked config"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_opencode_login_rejected() {
|
||||
local scenario_dir="$TEST_ROOT/opencode_localhost"
|
||||
printf '==> opencode_login_rejected\n'
|
||||
setup_stub_env
|
||||
if env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
|
||||
"$SLOPTRAP_BIN" "$scenario_dir" login </dev/null >/dev/null 2>&1; then
|
||||
record_failure "opencode_login_rejected: expected login rejection for opencode"
|
||||
fi
|
||||
teardown_stub_env
|
||||
}
|
||||
|
||||
run_symlink_escape
|
||||
run_manifest_injection
|
||||
run_helper_symlink
|
||||
@@ -881,6 +1038,7 @@ run_sh_reexec
|
||||
run_resume_omits_runtime_context
|
||||
run_shell_target_uses_entrypoint
|
||||
run_auth_file_mount
|
||||
run_runtime_hardening_flags
|
||||
run_codex_home_override
|
||||
run_project_state_isolation
|
||||
run_auto_login_empty_auth
|
||||
@@ -889,9 +1047,16 @@ run_root_directory_project
|
||||
run_wizard_create_manifest
|
||||
run_wizard_existing_defaults
|
||||
run_wizard_build_trigger
|
||||
run_invalid_manifest_agent
|
||||
run_invalid_agent_env_override
|
||||
run_removed_runtime_override_envs
|
||||
run_removed_build_override_envs
|
||||
run_opencode_build_downloads_release_cli
|
||||
run_opencode_localhost_rewrite
|
||||
run_opencode_print_config_runtime_flags
|
||||
run_opencode_env_overrides
|
||||
run_opencode_config_symlink_rejected
|
||||
run_opencode_login_rejected
|
||||
|
||||
if [[ ${#failures[@]} -gt 0 ]]; then
|
||||
printf '\nTest failures:\n'
|
||||
|
||||
Reference in New Issue
Block a user