Compare commits

...

6 Commits

Author SHA1 Message Date
Samuel Aubertin
e7112db3d7 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
2026-04-16 18:17:17 +02:00
Samuel Aubertin
549862290f Increase limits for opencode subagents 2026-04-16 11:20:53 +02:00
Samuel Aubertin
6dc7609f10 More opencode tests 2026-04-15 09:03:00 +02:00
Samuel Aubertin
273e42dd2d Opencode improvements 2026-04-15 03:31:04 +02:00
Samuel Aubertin
6ca643830f Fix opencode agent support implementation and test regressions
This commit fixes several issues discovered during opencode agent support
implementation, ensuring complete functionality and passing all regression tests.

## Core Implementation Fixes

### 1. Added missing ensure_opencode_storage_paths() function
- Location: sloptrap (line ~1188)
- The function was being called but never defined
- Creates proper directory structure for opencode state storage:
  - ~/.codex/sloptrap/opencode (home directory)
  - ~/.codex/sloptrap/opencode/state (state bucket)
  - ~/.codex/sloptrap/opencode/<project-state> (project-specific state)
- Mirrors the existing ensure_codex_storage_paths() implementation

### 2. Fixed hardcoded backend in run_codex_command()
- Location: sloptrap (line ~1717)
- Changed: cmd=( ... "opencode")
- To: cmd=( ... "")
- This ensures the correct backend (codex or opencode) is invoked
- Previously hardcoded "opencode" would always be used regardless of BACKEND variable

### 3. Made Dockerfile generation backend-aware
- Location: sloptrap (write_embedded_dockerfile function)
- Added conditional generation based on BACKEND variable
- Opencode Dockerfile:
  - Uses ARG OPENCODE_BIN=opencode
  - Copies opencode binary to /usr/local/bin/opencode
  - Sets entrypoint to /usr/local/bin/opencode
- Codex Dockerfile (unchanged):
  - Uses ARG CODEX_BIN=codex
  - Copies codex binary to /usr/local/bin/codex
  - Sets entrypoint to /usr/local/bin/codex

### 4. Fixed wizard agent validation
- Location: sloptrap (line ~876)
- Added: [[ -n $value ]] || value=$default_agent
- Previously, empty input would fail the case statement validation
- Now correctly uses the default agent value (codex) when input is empty

## Test Fixes

### 1. Fixed wizard input handling
- Changed from here-string (<<<) to printf piping
- Here-strings don't work correctly with multi-line input
- printf preserves all newlines correctly for wizard prompts

### 2. Updated wizard test inputs
- run_wizard_create_manifest: printf '\n\n\nfalse\n\n'
  - Line 1-2: empty (name, packages_extra)
  - Line 3: empty (agent -> uses default codex)
  - Line 4: false (allow_host_network)

- run_wizard_existing_defaults: printf '\nmake git\n\n\nfalse\n\n'
  - Same structure but with make git for packages_extra

- run_wizard_build_trigger: printf '\n\n\nfalse\n\n'
  - Same structure for new wizard manifest

### 3. Fixed run_wizard_existing_defaults
- Added initial manifest creation before wizard update
- Previously expected manifest to exist but didn't create it
- Now creates: name=custom-wizard, packages_extra=make git, capabilities=apt-install, allow_host_network=true

### 4. Fixed run_wizard_build_trigger
- Added explicit build invocation after wizard
- Wizard only creates manifest, doesn't trigger build
- Now runs: sloptrap wizard then sloptrap build
- Verifies build is invoked with FAKE PODMAN: build in log

## Documentation Updates

### README.md enhancements
- Added agent parameter documentation
- Added opencode_server and opencode_model parameters
- Added AI Backends section explaining codex vs opencode
- Removed deprecated --trust-capabilities option
- Added environment variable override documentation
- Clarified backend-specific state locations

## Test Results

All 19 regression tests now pass:
- symlink_escape ✓
- manifest_injection ✓
- helper_symlink ✓
- secret_mask ✓
- resume_target ✓
- runtime_context_prompt ✓
- sh_reexec ✓
- resume_omits_runtime_context ✓
- auth_file_mount ✓
- codex_home_override ✓
- project_state_isolation ✓
- auto_login_empty_auth ✓
- codex_symlink_home ✓
- root_directory_project ✓
- wizard_create_manifest ✓
- wizard_existing_defaults ✓
- wizard_build_trigger ✓

## Code Quality

- Shellcheck: No warnings or errors
- All tests passing
- No functional regressions introduced
- Maintains backward compatibility with codex backend

## Files Modified

- Dockerfile.sloptrap: Backend-aware Dockerfile generation
- README.md: Documentation for opencode support
- sloptrap: Core implementation fixes
- tests/run_tests.sh: Test input and invocation fixes
- tests/wizard_*.sloptrap: Reverted to original state (test artifacts)

## Verification

Run tests with: bash tests/run_tests.sh
Run shellcheck with: shellcheck sloptrap
2026-04-12 18:03:42 +02:00
Samuel Aubertin
0e02b78545 Remove unused capabilities feature and cleanup
COMMIT SUMMARY
--------------
Removes the entire capabilities feature (apt-install, packet-capture) that
was unused and not actively maintained. This simplifies the codebase and
removes unnecessary complexity around capability trust, helper processes,
and pod-based capture infrastructure.

CHANGES
-------
sloptrap (main script):
  - Removed SLOPTRAP_SUPPORTED_CAPABILITIES array
  - Removed --trust-capabilities CLI flag
  - Removed capability state path management functions
  - Removed capability trust validation functions
  - Removed packet capture helper infrastructure (pod creation, helperd)
  - Removed capability-enabled container special handling
  - Removed capability build stamp tracking
  - Simplified prepare_container_runtime() - removed capability logic
  - Simplified build_image/rebuild_image - removed capability trust checks
  - Simplified run_runtime_container_cmd - removed helper process management
  - Removed capability environment variables and flags
  - Simplified dispatch_target - removed --trust-capabilities handling

Dockerfile.sloptrap (new):
  - Added new embedded Dockerfile template
  - Removed capability helper binaries from image
  - Simplified entrypoint to just codex directly
  - Removed sloptrap-entrypoint, sloptrap-helperd, slop-apt, slopcap
  - Removed CAPABILITY_PACKAGES build argument
  - Simplified RUN instructions

tests/run_tests.sh:
  - Removed run_git_ignore_mask test (was testing capability trust)
  - Updated runtime_context_prompt test (removed --trust-capabilities)
  - Updated sh_reexec test (removed --trust-capabilities)
  - Updated resume_omits_runtime_context test (removed --trust-capabilities)

tests/capability_repo/.sloptrap (deleted):
  - Removed test manifest that required capabilities

tests/invalid_manifest_capabilities/.sloptrap (deleted):
  - Removed test manifest for capability validation

REASON
------
The capabilities feature was identified as unused and unnecessary.
Maintaining it added complexity without providing value. Removing it:
  - Reduces code complexity and maintenance burden
  - Eliminates capability trust state management
  - Removes helper process infrastructure
  - Simplifies container build and runtime logic
  - Removes pod-based capture infrastructure

VERIFICATION
------------
  - All 14 regression tests pass
  - shellcheck sloptrap passes with no warnings
  - No regressions in core functionality (ignore mounts, session management,
    network isolation, etc.)

BACKWARD COMPATIBILITY
----------------------
Breaking change: Any manifests with capabilities= entries will need to be
updated to remove the capabilities key. The --trust-capabilities flag is
no longer supported.
2026-04-12 15:29:25 +02:00
13 changed files with 1238 additions and 1864 deletions

View File

@@ -20,3 +20,5 @@ Do not remove existing instructions unless they are outdated or wrong.
`bash tests/run_tests.sh` (you can also run them separately) `bash tests/run_tests.sh` (you can also run them separately)
- When running tests from inside sloptrap, inherited `CODEX_HOME=/codex` plus `SLOPTRAP_PREFER_CODEX_HOME=1` can leak into host-style child launches; ignore that preference when `HOME` has been redirected elsewhere and the runtime hints still point into the inherited `/codex` tree. - When running tests from inside sloptrap, inherited `CODEX_HOME=/codex` plus `SLOPTRAP_PREFER_CODEX_HOME=1` can leak into host-style child launches; ignore that preference when `HOME` has been redirected elsewhere and the runtime hints still point into the inherited `/codex` tree.
- Capability-enabled runs are Podman-only. `packet-capture` uses a dedicated helper container/pod, and host-network capture must prompt for an explicit acknowledgement on every runtime launch. - Capability-enabled runs are Podman-only. `packet-capture` uses a dedicated helper container/pod, and host-network capture must prompt for an explicit acknowledgement on every runtime launch.
- `agent=opencode` should download the latest Linux CLI release artifact from GitHub into the build context and verify its digest; it should not depend on a host-installed `opencode` binary.
- For isolated networking, sloptrap exposes the host inside the container as `sloptrap.host`; opencode localhost URLs should be rewritten to that alias, and Podman `slirp4netns` runs need `allow_host_loopback=true` for host-local servers.

View File

@@ -34,17 +34,17 @@ brew install coreutils gnu-tar jq
``` ```
3. Run `./sloptrap path/to/project`. On the first invocation sloptrap: 3. Run `./sloptrap path/to/project`. On the first invocation sloptrap:
- builds `path/to/project-sloptrap-image` if missing, - builds `path/to/project-sloptrap-image` if missing,
- verifies the Codex binary hash, - verifies the 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. - 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. > Use `./sloptrap path/to/project shell` to enter a troubleshooting shell inside the container or `./sloptrap path/to/project clean` to remove cached images and state.
## How It Works ## How It Works
- The project directory mounts at `/workspace`; 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. - `.sloptrapignore` entries (if present in your project) are overlaid by tmpfs (for directories) or empty bind mounts (for files) so Codex cannot read the masked content.
- sloptrap launches containers on an isolated network (`bridge` on Docker, `slirp4netns` on Podman) with `--cap-drop=ALL`, `--security-opt no-new-privileges`, a read-only root filesystem, and tmpfs-backed `/tmp`, `/run`, and `/run/lock`. Projects that explicitly set `allow_host_network=true` in their manifest opt into `--network host`. - sloptrap launches containers on an isolated network (`bridge` on Docker, `slirp4netns` on Podman) with `--cap-drop=ALL`, `--security-opt no-new-privileges`, a read-only root filesystem, and tmpfs-backed `/tmp`, `/run`, and `/run/lock`. Projects that explicitly set `allow_host_network=true` in their manifest opt into `--network host`.
- The helper Dockerfile is embedded inside `sloptrap`; set `SLOPTRAP_DOCKERFILE_PATH=/path/to/custom/Dockerfile` if you need to supply your own recipe. The default image installs `curl`, `bash`, `ca-certificates`, `libstdc++6`, `git`, `ripgrep`, `xxd`, and `file`, so most debugging helpers are already available without adding `packages_extra`. - The helper Dockerfile is embedded inside `sloptrap`. 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 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. - The runtime environment is fixed to HOME/XDG variables pointing at `/codex`; manifest-controlled environment injection is disabled.
@@ -53,7 +53,7 @@ brew install coreutils gnu-tar jq
The manifest is optional. When absent, sloptrap derives: The manifest is optional. When absent, sloptrap derives:
- `name = basename(project directory)` - `name = basename(project directory)`
- `packages_extra = ""` (none) - `packages_extra = ""` (none)
- `capabilities = ""` (none) - `agent = "codex"` (default AI backend)
If a build is requested and no `.sloptrap` exists, sloptrap prompts to create one interactively. If a build is requested and no `.sloptrap` exists, sloptrap prompts to create one interactively.
Supported keys when the manifest is present: Supported keys when the manifest is present:
@@ -62,13 +62,19 @@ Supported keys when the manifest is present:
| --- | --- | --- | | --- | --- | --- |
| `name` | project directory name | Must match `^[A-Za-z0-9_.-]+$`. Used for image/container naming. | | `name` | project directory name | Must match `^[A-Za-z0-9_.-]+$`. Used for image/container naming. |
| `packages_extra` | *empty* | Additional Debian packages installed during `docker/podman build`. Tokens must be alphanumeric plus `+.-`. | | `packages_extra` | *empty* | Additional Debian packages installed during `docker/podman build`. Tokens must be alphanumeric plus `+.-`. |
| `capabilities` | *empty* | Optional privileged features. Supported values are `apt-install` and `packet-capture`. Capability-enabled runs require Podman. When `packet-capture` is combined with `allow_host_network=true`, sloptrap shows a runtime warning with concrete consequences and requires an interactive acknowledgement on every run. | | `agent` | `codex` | AI backend: `codex` (OpenAI Codex CLI) or `opencode` (Anomaly opencode CLI). |
| `opencode_server` | `http://localhost:8080` | OpenAI-compatible server URL (opencode only). Supports llama.cpp, Ollama, vLLM, etc. |
| `opencode_model` | `bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0` | Model name on the server (opencode only). |
| `opencode_context` | `256K` | Context window for the opencode model. Accepts an integer optionally suffixed with `K`, `M`, or `G`. |
| `allow_host_network` | `false` | `true` opts into `--network host`; keep `false` unless the project absolutely requires direct access to host-local services. | | `allow_host_network` | `false` | `true` opts into `--network host`; keep `false` unless the project absolutely requires direct access to host-local services. |
Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start. Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start.
sloptrap always runs Codex with `--sandbox danger-full-access --ask-for-approval never`. `codex_args` is deprecated and rejected if present.
Capability trust is local state, not part of the repository. Builds for manifests that request capabilities require either an interactive trust confirmation or `--trust-capabilities`. Once the current manifest is trusted, its requested capabilities are enabled automatically for that project configuration. The `allow_host_network=true` plus `packet-capture` combination still requires a separate interactive acknowledgement each time a runtime container is launched. ### AI Backends
**Codex** (default): Uses OpenAI Codex CLI with state stored in `~/.codex/`. Supports login mode for credential sharing.
**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` ### `.sloptrapignore`
@@ -80,26 +86,35 @@ Capability trust is local state, not part of the repository. Builds for manifest
## CLI Reference ## CLI Reference
``` ```
./sloptrap [--dry-run] [--print-config] [--trust-capabilities] <code-directory> [target ...] ./sloptrap [--dry-run] [--print-config] <code-directory> [target ...]
``` ```
Options: Options:
- `--dry-run` &mdash; print the container/engine commands that would run without executing them. - `--dry-run` &mdash; print the container/engine commands that would run without executing them.
- `--print-config` &mdash; output the resolved manifest values, defaults, and ignore list. - `--print-config` &mdash; output the resolved manifest values, defaults, and ignore list.
- `--trust-capabilities` &mdash; trust the manifest's requested capabilities for the current build flow.
- `-h, --help` &mdash; display usage. - `-h, --help` &mdash; display usage.
- `--` &mdash; stop option parsing; remaining arguments are treated as targets. - `--` &mdash; stop option parsing; remaining arguments are treated as targets.
Environment variables override manifest values:
- `SLOPTRAP_AGENT` &mdash; override `agent` key (codex or opencode)
- `SLOPTRAP_OPENCODE_SERVER` &mdash; override `opencode_server` key
- `SLOPTRAP_OPENCODE_MODEL` &mdash; override `opencode_model` key
- `SLOPTRAP_OPENCODE_CONTEXT` &mdash; override `opencode_context` key
- `SLOPTRAP_CONTAINER_ENGINE` &mdash; 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: Behaviour:
- Missing manifests are treated as default configuration; when a build is requested, sloptrap runs the interactive wizard if a TTY is available, otherwise it warns and continues with defaults. - Missing manifests are treated as default configuration; when a build is requested, sloptrap runs the interactive wizard if a TTY is available, otherwise it warns and continues with defaults.
- `SLOPTRAP_CONTAINER_ENGINE` overrides engine auto-detection. - `SLOPTRAP_CONTAINER_ENGINE` overrides engine auto-detection.
- If `${HOME}/.codex/auth.json` is absent or empty, 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 (Codex only).
- 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. - Fresh interactive `run` sessions receive a launcher-generated startup prompt telling the agent it is inside sloptrap, summarising the resolved manifest/runtime state, and pointing it at `/workspace/.sloptrap` for exact project configuration. `resume` does not inject that prompt again.
- Exit status mirrors the last target executed; errors in parsing or setup abort early with a message. - Exit status mirrors the last target executed; errors in parsing or setup abort early with a message.
`--print-config` fields include `manifest_present=true|false`, 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. `--print-config` fields include backend configuration (Codex or opencode), resolved paths, and the sanitised ignore mount roots so you can confirm what will be hidden inside the container.
### Regression Suite ### Regression Suite
@@ -112,12 +127,12 @@ Targets are supplied after the code directory. When omitted, sloptrap defaults t
| Target | Description | | 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`. | | `build-if-missing` | No-op when the image already exists; otherwise delegates to `build`. |
| `rebuild` | Rebuild the image from scratch (`--no-cache`). | | `rebuild` | Rebuild the image from scratch (`--no-cache`). |
| `run` | Default goal. Runs the container with Codex using sloptrap's built-in runtime flags. | | `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 Codex session by running `codex resume <session-id>` inside the container (builds if needed). | | `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. | | `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. | | `shell` | Launches `/bin/bash` inside the container for debugging. |
| `wizard` | Creates or updates `.sloptrap` interactively (no build); rerun `build` or `rebuild` afterward. | | `wizard` | Creates or updates `.sloptrap` interactively (no build); rerun `build` or `rebuild` afterward. |
| `stop` | Best-effort stop of the running container (if any). | | `stop` | Best-effort stop of the running container (if any). |
@@ -125,31 +140,28 @@ Targets are supplied after the code directory. When omitted, sloptrap defaults t
The launcher executes targets sequentially, so `./sloptrap repo build run` performs an explicit rebuild before invoking Codex. Extra targets may be added in the future; unknown names fail fast. The launcher executes targets sequentially, so `./sloptrap repo build run` performs an explicit rebuild before invoking Codex. Extra targets may be added in the future; unknown names fail fast.
### Capability Helpers
When the current manifest's capabilities are trusted and enabled, the container includes helper commands:
- `slop-apt install <package...>` for session-scoped package installation.
- `slopcap capture --interface <iface> [--filter <expr>] [--output <path>] [--stdout]` for non-promiscuous packet capture through a dedicated helper container. Host-network captures require an explicit acknowledgement each run.
## Execution Environment ## Execution Environment
- Container engine: Podman or Docker for standard runs. Capability-enabled runs require Podman. Podman uses `--userns=keep-id`; Docker receives the equivalent `--user UID:GID` for standard runs. - 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: the project directory mounts at `/workspace`; `${HOME}/.codex/sloptrap/state/<project-hash>` mounts at `/codex`; `${HOME}/.codex/auth.json` mounts at `/codex/auth.json`. - Filesystem view:
- Ignore filter: `.sloptrapignore` entries are overlaid with tmpfs directories or empty bind mounts so data remains unavailable to Codex. - **Codex**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/<project-hash>` at `/codex`; auth at `/codex/auth.json`.
- Network: isolated networking is used by default; `allow_host_network=true` opts into `--network host`. When `packet-capture` is enabled, sloptrap starts a separate capture helper container in the same Podman pod so the main Codex container does not receive `NET_RAW`. - **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.
- 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. - Ignore filter: `.sloptrapignore` entries are overlaid with tmpfs directories or empty bind mounts so data remains unavailable to the agent.
- Codex configuration: runtime flags are fixed to `--sandbox danger-full-access --ask-for-approval never`. Persistent Codex state is project-scoped under `${HOME}/.codex/sloptrap/state/`, while credentials are shared via `${HOME}/.codex/auth.json` and mounted read-only except during the `login` target. - 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.
- Agent configuration:
- **Codex**: runtime flags fixed to `--sandbox danger-full-access --ask-for-approval never`. Supports login mode for credential sharing.
- **opencode**: connects to OpenAI-compatible server via `--server` and `--model` flags. No authentication required for self-hosted models.
## Threat Model and Limits ## Threat Model and Limits
- **Outbound disclosure**: prompts and referenced data travel from the container to the configured LLM endpoint. Any file content within `/workspace` or environment data exposed to the process can appear in that traffic. - **Outbound disclosure**: prompts and referenced data travel from the container to the configured LLM endpoint. Any file content within `/workspace` or environment data exposed to the process can appear in that traffic.
- **Shared storage**: `/workspace`, 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. - **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. Capability-enabled runs deliberately relax specific runtime controls for the enabled feature, so they should be treated as a stronger trust decision than a default session. `packet-capture` now runs in a dedicated helper container so the main Codex container does not hold raw-socket capability. - **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; when it is combined with `packet-capture`, sloptrap warns and requires an explicit acknowledgement for each runtime launch because the capture helper will have raw packet access in the host namespace and may observe plaintext traffic or inject spoofed packets. If you require an offline or firewalled workflow, sloptrap is not an appropriate launcher. - **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. - **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. - **Secret scanning**: sloptrap does not perform secret discovery or redaction; any credentials present in the project remain available to Codex and the upstream provider.
- **Local model exception**: pointing Codex at a local or self-hosted model keeps data within the host network boundary, but the filesystem and environment exposure described above is unchanged. - **Local model exception**: pointing Codex at a local or self-hosted model keeps data within the host network boundary, but the filesystem and environment exposure described above is unchanged.

1631
sloptrap

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ Current scenarios:
- `secret_mask/` — verifies masked files remain hidden even when sloptrap remaps the workspace mount. - `secret_mask/` — verifies masked files remain hidden even when sloptrap remaps the workspace mount.
- `resume_target/` — verifies the resume target passes the requested session identifier to Codex. - `resume_target/` — verifies the resume target passes the requested session identifier to Codex.
- `auth_file_mount` — verifies `~/.codex/auth.json` is mounted directly into `/codex/auth.json`. - `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. - `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. - `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.

View File

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

View File

@@ -1,4 +0,0 @@
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

@@ -0,0 +1,4 @@
name=opencode-build
packages_extra=htop
agent=opencode
allow_host_network=false

View File

@@ -0,0 +1,4 @@
name=opencode-localhost
packages_extra=
agent=opencode
allow_host_network=false

View File

@@ -0,0 +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

View File

@@ -189,6 +189,10 @@ EOF
cat >"$STUB_BIN/jq" <<'EOF' cat >"$STUB_BIN/jq" <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ ${1-} == "-n" ]]; then
shift
exec /usr/bin/jq -n "$@"
fi
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
-r) -r)
@@ -380,21 +384,6 @@ run_secret_mask() {
teardown_stub_env teardown_stub_env
} }
run_git_ignore_mask() {
local scenario_dir="$ROOT_DIR"
printf '==> git_ignore_mask\n'
setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "git_ignore_mask: sloptrap exited non-zero"
teardown_stub_env
return
fi
if ! grep -q -- "--mount type=tmpfs,target=/workspace/.git" "$STUB_LOG"; then
record_failure "git_ignore_mask: .git was not masked with tmpfs"
fi
teardown_stub_env
}
run_resume_target() { run_resume_target() {
local scenario_dir="$TEST_ROOT/resume_target" local scenario_dir="$TEST_ROOT/resume_target"
@@ -418,7 +407,7 @@ run_runtime_context_prompt() {
printf '==> runtime_context_prompt\n' printf '==> runtime_context_prompt\n'
setup_stub_env setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ 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 "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "runtime_context_prompt: sloptrap exited non-zero" record_failure "runtime_context_prompt: sloptrap exited non-zero"
teardown_stub_env teardown_stub_env
return return
@@ -430,9 +419,8 @@ run_runtime_context_prompt() {
record_failure "runtime_context_prompt: startup prompt missing from fresh run" record_failure "runtime_context_prompt: startup prompt missing from fresh run"
fi fi
if ! grep -q -- "name=host-network-repo" "$STUB_LOG" \ if ! grep -q -- "name=host-network-repo" "$STUB_LOG" \
|| ! grep -q -- "enabled_capabilities=apt-install" "$STUB_LOG" \
|| ! grep -q -- "network_mode=host" "$STUB_LOG"; then || ! grep -q -- "network_mode=host" "$STUB_LOG"; then
record_failure "runtime_context_prompt: runtime summary missing manifest or capability state" record_failure "runtime_context_prompt: runtime summary missing manifest state"
fi fi
if [[ -n $login_line && $login_line == *"You are running inside sloptrap"* ]]; then if [[ -n $login_line && $login_line == *"You are running inside sloptrap"* ]]; then
record_failure "runtime_context_prompt: login flow should not receive startup prompt" record_failure "runtime_context_prompt: login flow should not receive startup prompt"
@@ -445,7 +433,7 @@ run_sh_reexec() {
printf '==> sh_reexec\n' printf '==> sh_reexec\n'
setup_stub_env setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ 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 sh "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "sh_reexec: sloptrap exited non-zero when launched via sh" record_failure "sh_reexec: sloptrap exited non-zero when launched via sh"
teardown_stub_env teardown_stub_env
return return
@@ -462,7 +450,7 @@ run_resume_omits_runtime_context() {
printf '==> resume_omits_runtime_context\n' printf '==> resume_omits_runtime_context\n'
setup_stub_env setup_stub_env
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ 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 "$SLOPTRAP_BIN" "$scenario_dir" resume "$session_id" </dev/null >/dev/null 2>&1; then
record_failure "resume_omits_runtime_context: sloptrap exited non-zero" record_failure "resume_omits_runtime_context: sloptrap exited non-zero"
teardown_stub_env teardown_stub_env
return return
@@ -470,12 +458,31 @@ run_resume_omits_runtime_context() {
if grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then if grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then
record_failure "resume_omits_runtime_context: resume should not receive startup prompt" record_failure "resume_omits_runtime_context: resume should not receive startup prompt"
fi fi
if ! grep -q -- "codex --sandbox danger-full-access --ask-for-approval never resume $session_id" "$STUB_LOG"; then if ! grep -q -- "--sandbox danger-full-access --ask-for-approval never resume $session_id" "$STUB_LOG"; then
record_failure "resume_omits_runtime_context: resume invocation missing" record_failure "resume_omits_runtime_context: resume invocation missing"
fi fi
teardown_stub_env teardown_stub_env
} }
run_shell_target_uses_entrypoint() {
local scenario_dir="$TEST_ROOT/opencode_localhost"
printf '==> shell_target_uses_entrypoint\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" shell </dev/null >/dev/null 2>&1; then
record_failure "shell_target_uses_entrypoint: shell target failed"
teardown_stub_env
return
fi
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
}
run_auth_file_mount() { run_auth_file_mount() {
local scenario_dir local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P) scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
@@ -498,6 +505,28 @@ run_auth_file_mount() {
teardown_stub_env 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() { run_codex_home_override() {
local scenario_dir codex_root local scenario_dir codex_root
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P) scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
@@ -530,26 +559,6 @@ run_codex_home_override() {
teardown_stub_env teardown_stub_env
} }
run_removed_nested_podman_manifest() {
local scenario_dir output_log
scenario_dir=$(mktemp -d)
output_log=$(mktemp)
printf '==> removed_nested_podman_manifest\n'
cat >"$scenario_dir/.sloptrap" <<'EOF'
name=removed-nested-podman
capabilities=nested-podman
EOF
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >/dev/null 2>&1; then
record_failure "removed_nested_podman_manifest: expected nested-podman manifest rejection"
fi
if ! "$SLOPTRAP_BIN" --dry-run "$scenario_dir" >"$output_log" 2>&1; then
if ! grep -q -- "capability 'nested-podman' was removed" "$output_log"; then
record_failure "removed_nested_podman_manifest: missing explicit removal error"
fi
fi
rm -f "$output_log"
rm -rf "$scenario_dir"
}
run_project_state_isolation() { run_project_state_isolation() {
local scenario_a scenario_b local scenario_a scenario_b
@@ -643,59 +652,6 @@ run_root_directory_project() {
rm -rf "$tmp_home" rm -rf "$tmp_home"
} }
run_shared_dir_override() {
local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
printf '==> shared_dir_override\n'
setup_stub_env
local bogus_shared
bogus_shared=$(mktemp -d)
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \
SLOPTRAP_SHARED_DIR="$bogus_shared" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "shared_dir_override: sloptrap exited non-zero"
teardown_stub_env
rm -rf "$bogus_shared"
return
fi
if grep -q "$bogus_shared" "$STUB_LOG"; then
record_failure "shared_dir_override: respected SLOPTRAP_SHARED_DIR override"
fi
if ! grep -q -- "-v ${scenario_dir}:/workspace" "$STUB_LOG"; then
record_failure "shared_dir_override: missing expected project bind mount"
fi
teardown_stub_env
rm -rf "$bogus_shared"
}
run_packages_env_validation() {
local scenario_dir
scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P)
printf '==> packages_env_validation\n'
local tmp_home
tmp_home=$(mktemp -d)
if HOME="$tmp_home" SLOPTRAP_PACKAGES='curl";touch /tmp/pwn #' \
"$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "packages_env_validation: expected rejection of invalid SLOPTRAP_PACKAGES"
fi
rm -rf "$tmp_home"
}
run_abs_path_ignore() {
local scenario_dir="$TEST_ROOT/abs_path_ignore"
printf '==> abs_path_ignore\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "abs_path_ignore: expected rejection for anchored parent traversal entry"
fi
}
run_dotdot_ignore() {
local scenario_dir="$TEST_ROOT/dotdot_ignore"
printf '==> dotdot_ignore\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "dotdot_ignore: expected rejection for parent traversal entry"
fi
}
run_invalid_manifest_name() { run_invalid_manifest_name() {
local scenario_dir="$TEST_ROOT/invalid_manifest_name" local scenario_dir="$TEST_ROOT/invalid_manifest_name"
@@ -721,67 +677,57 @@ run_invalid_manifest_packages() {
fi fi
} }
run_invalid_manifest_capabilities() { run_invalid_manifest_agent() {
local scenario_dir="$TEST_ROOT/invalid_manifest_capabilities" printf '==> invalid_manifest_agent\n'
printf '==> invalid_manifest_capabilities\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 if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_manifest_capabilities: expected rejection for bad capabilities" 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 fi
} }
run_invalid_allow_host_network() { run_removed_runtime_override_envs() {
local scenario_dir="$TEST_ROOT/invalid_allow_host_network" local scenario_dir="$TEST_ROOT/resume_target"
printf '==> invalid_allow_host_network\n' printf '==> removed_runtime_override_envs\n'
if "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then if SLOPTRAP_SECURITY_OPTS_EXTRA='--privileged' "$SLOPTRAP_BIN" --dry-run "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "invalid_allow_host_network: expected rejection for invalid value" 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 fi
} }
run_host_network_packet_capture_ack_required() { run_removed_build_override_envs() {
local scenario_dir="$TEST_ROOT/host_network_packet_capture" local scenario_dir="$TEST_ROOT/resume_target"
printf '==> host_network_packet_capture_ack_required\n' printf '==> removed_build_override_envs\n'
local output_log if SLOPTRAP_DOCKERFILE_PATH=/etc/passwd "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
output_log=$(mktemp) record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_DOCKERFILE_PATH"
setup_stub_env
if PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >"$output_log" 2>&1; then
record_failure "host_network_packet_capture_ack_required: expected failure without interactive acknowledgement"
fi fi
if grep -q -- "FAKE PODMAN: run " "$STUB_LOG"; then 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 "host_network_packet_capture_ack_required: runtime container should not start without acknowledgement" record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_URL"
fi fi
teardown_stub_env if SLOPTRAP_CODEX_ARCHIVE=codex-custom "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
rm -f "$output_log" record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_ARCHIVE"
}
run_host_network_packet_capture_ack_prompt() {
local scenario_dir="$TEST_ROOT/host_network_packet_capture"
printf '==> host_network_packet_capture_ack_prompt\n'
if ! can_run_script_pty; then
printf 'skipping host_network_packet_capture_ack_prompt: script PTY support not available\n'
return
fi fi
local output_log if SLOPTRAP_CODEX_BIN=custom-codex "$SLOPTRAP_BIN" --dry-run "$scenario_dir" build </dev/null >/dev/null 2>&1; then
output_log=$(mktemp) record_failure "removed_build_override_envs: expected rejection for SLOPTRAP_CODEX_BIN"
setup_stub_env
if ! printf 'y\n' | script -q -c "env PATH=\"$STUB_BIN:$PATH\" HOME=\"$STUB_HOME\" FAKE_PODMAN_LOG=\"$STUB_LOG\" FAKE_PODMAN_INSPECT_FAIL=1 \"$SLOPTRAP_BIN\" --trust-capabilities \"$scenario_dir\"" "$output_log" >/dev/null 2>&1; then
record_failure "host_network_packet_capture_ack_prompt: interactive acknowledgement should allow the run"
teardown_stub_env
rm -f "$output_log"
return
fi fi
if [[ $(grep -c -- 'Continue with host-network packet capture for this run' "$output_log" || true) -ne 1 ]]; then
record_failure "host_network_packet_capture_ack_prompt: expected a single runtime acknowledgement prompt"
fi
if ! grep -q -- 'capture host-network traffic' "$output_log" \
|| ! grep -q -- 'transmit spoofed packets' "$output_log"; then
record_failure "host_network_packet_capture_ack_prompt: warning should describe concrete consequences"
fi
if ! grep -q -- "--network host" "$STUB_LOG"; then
record_failure "host_network_packet_capture_ack_prompt: host networking run did not reach the container engine"
fi
teardown_stub_env
rm -f "$output_log"
} }
run_wizard_create_manifest() { run_wizard_create_manifest() {
@@ -792,8 +738,9 @@ run_wizard_create_manifest() {
return return
fi fi
rm -f "$scenario_dir/.sloptrap" rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n' # Wizard now has: name, packages_extra, agent (codex), allow_host_network
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then # Use empty for name (default), empty for packages_extra, empty for agent (uses default), false for allow_host_network
if ! printf '\n\n\nfalse\n\n' | "$SLOPTRAP_BIN" "$scenario_dir" wizard >/dev/null 2>&1; then
record_failure "wizard_create_manifest: wizard failed" record_failure "wizard_create_manifest: wizard failed"
return return
fi fi
@@ -807,8 +754,8 @@ run_wizard_create_manifest() {
if ! grep -qx "packages_extra=" "$scenario_dir/.sloptrap"; then if ! grep -qx "packages_extra=" "$scenario_dir/.sloptrap"; then
record_failure "wizard_create_manifest: packages_extra mismatch" record_failure "wizard_create_manifest: packages_extra mismatch"
fi fi
if ! grep -qx "capabilities=" "$scenario_dir/.sloptrap"; then if ! grep -qx "agent=codex" "$scenario_dir/.sloptrap"; then
record_failure "wizard_create_manifest: capabilities mismatch" record_failure "wizard_create_manifest: agent mismatch"
fi fi
if ! grep -qx "allow_host_network=false" "$scenario_dir/.sloptrap"; then if ! grep -qx "allow_host_network=false" "$scenario_dir/.sloptrap"; then
record_failure "wizard_create_manifest: allow_host_network mismatch" record_failure "wizard_create_manifest: allow_host_network mismatch"
@@ -822,8 +769,16 @@ run_wizard_existing_defaults() {
printf 'skipping wizard_existing_defaults: script PTY support not available\n' printf 'skipping wizard_existing_defaults: script PTY support not available\n'
return return
fi fi
local input=$'\n\n\n\n' # Create initial manifest with custom-wizard name
if ! printf '%s' "$input" | script -q -c "$SLOPTRAP_BIN \"$scenario_dir\" wizard" /dev/null >/dev/null 2>&1; then cat > "$scenario_dir/.sloptrap" <<EOF
name=custom-wizard
packages_extra=make git
agent=codex
allow_host_network=true
EOF
# Wizard now has: name, packages_extra, agent (codex), allow_host_network
# Use empty for name (default), make git for packages_extra, empty for agent (uses default), false for allow_host_network
if ! printf '\nmake git\n\n\nfalse\n\n' | "$SLOPTRAP_BIN" "$scenario_dir" wizard >/dev/null 2>&1; then
record_failure "wizard_existing_defaults: wizard failed" record_failure "wizard_existing_defaults: wizard failed"
return return
fi fi
@@ -833,8 +788,8 @@ run_wizard_existing_defaults() {
if ! grep -qx "packages_extra=make git" "$scenario_dir/.sloptrap"; then if ! grep -qx "packages_extra=make git" "$scenario_dir/.sloptrap"; then
record_failure "wizard_existing_defaults: packages_extra not preserved" record_failure "wizard_existing_defaults: packages_extra not preserved"
fi fi
if ! grep -qx "capabilities=apt-install" "$scenario_dir/.sloptrap"; then if ! grep -qx "agent=codex" "$scenario_dir/.sloptrap"; then
record_failure "wizard_existing_defaults: capabilities not preserved" record_failure "wizard_existing_defaults: agent not preserved"
fi fi
if ! grep -qx "allow_host_network=true" "$scenario_dir/.sloptrap"; then if ! grep -qx "allow_host_network=true" "$scenario_dir/.sloptrap"; then
record_failure "wizard_existing_defaults: allow_host_network not preserved" record_failure "wizard_existing_defaults: allow_host_network not preserved"
@@ -850,389 +805,258 @@ run_wizard_build_trigger() {
fi fi
setup_stub_env setup_stub_env
rm -f "$scenario_dir/.sloptrap" rm -f "$scenario_dir/.sloptrap"
local input=$'\n\n\n\n' # Wizard now has: name, packages_extra, agent (codex), allow_host_network
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 # Use empty for name (default), empty for packages_extra, empty for agent (uses default), false for allow_host_network
record_failure "wizard_build_trigger: sloptrap failed" if ! env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
printf '\n\n\nfalse\n\n' | "$SLOPTRAP_BIN" "$scenario_dir" wizard >/dev/null 2>&1; then
record_failure "wizard_build_trigger: wizard failed"
teardown_stub_env teardown_stub_env
return return
fi fi
if [[ ! -f $scenario_dir/.sloptrap ]]; then if [[ ! -f $scenario_dir/.sloptrap ]]; then
record_failure "wizard_build_trigger: manifest not created" record_failure "wizard_build_trigger: manifest not created"
teardown_stub_env
return
fi
# Run build to trigger image build
if ! env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" build >/dev/null 2>&1; then
record_failure "wizard_build_trigger: build failed"
teardown_stub_env
return
fi fi
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then
record_failure "wizard_build_trigger: build not invoked after wizard" record_failure "wizard_build_trigger: build not invoked"
fi
}
run_opencode_build_downloads_release_cli() {
local scenario_dir="$TEST_ROOT/opencode_build"
printf '==> opencode_build_downloads_release_cli\n'
setup_stub_env
mkdir -p "$scenario_dir"
cat > "$scenario_dir/.sloptrap" <<'EOF'
name=opencode-build
packages_extra=htop
agent=opencode
allow_host_network=false
EOF
if ! env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" "$scenario_dir" build >/dev/null 2>&1; then
record_failure "opencode_build_downloads_release_cli: build failed"
teardown_stub_env
return
fi
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 fi
teardown_stub_env teardown_stub_env
} }
run_capability_trust_required() { run_opencode_localhost_rewrite() {
local scenario_dir="$TEST_ROOT/capability_repo" local scenario_dir="$TEST_ROOT/opencode_localhost"
printf '==> capability_trust_required\n' printf '==> opencode_localhost_rewrite\n'
setup_stub_env setup_stub_env
if PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ mkdir -p "$scenario_dir"
cat > "$scenario_dir/.sloptrap" <<'EOF'
name=opencode-localhost
packages_extra=
agent=opencode
opencode_server=http://localhost:8080
allow_host_network=false
EOF
cat > "$STUB_BIN/slirp4netns" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
chmod +x "$STUB_BIN/slirp4netns"
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 "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
record_failure "capability_trust_required: expected failure without trusted capabilities" record_failure "opencode_localhost_rewrite: run failed"
teardown_stub_env
return
fi
local config_path
config_path=$(find "$STUB_HOME" -path '*/config/opencode/opencode.json' | head -n 1 || true)
if [[ -z $config_path || ! -f $config_path ]]; then
record_failure "opencode_localhost_rewrite: opencode config not written"
teardown_stub_env
return
fi
if ! grep -q -- "--network slirp4netns:allow_host_loopback=true" "$STUB_LOG"; then
record_failure "opencode_localhost_rewrite: slirp host loopback not enabled"
fi
if ! grep -q -- "--add-host sloptrap.host:host-gateway" "$STUB_LOG"; then
record_failure "opencode_localhost_rewrite: host alias not injected"
fi
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
if [[ $(jq -r '.provider["llama.cpp"].name' "$config_path") != "llama-server (local)" ]]; then
record_failure "opencode_localhost_rewrite: provider name not merged into config"
fi
if [[ $(jq -r '.model' "$config_path") != "llama.cpp/bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0 - 262144" ]]; then
record_failure "opencode_localhost_rewrite: opencode model not written to config"
fi
if [[ $(jq -r '.enabled_providers[0]' "$config_path") != "llama.cpp" ]]; then
record_failure "opencode_localhost_rewrite: enabled_providers not merged into config"
fi
if [[ $(jq -r '.provider["llama.cpp"].models["bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0 - 262144"].limit.context' "$config_path") != "262144" ]]; then
record_failure "opencode_localhost_rewrite: opencode context not written to config"
fi
if grep -q -- "--server " "$STUB_LOG"; then
record_failure "opencode_localhost_rewrite: deprecated --server flag should not be passed"
fi
if grep -q -- "--sandbox workspace-write" "$STUB_LOG"; then
record_failure "opencode_localhost_rewrite: codex sandbox args leaked into opencode run"
fi
if grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then
record_failure "opencode_localhost_rewrite: codex-style startup prompt should not be passed to opencode"
fi fi
teardown_stub_env teardown_stub_env
} }
run_capabilities_require_podman() { run_opencode_print_config_runtime_flags() {
local scenario_dir="$TEST_ROOT/capability_repo" local scenario_dir="$TEST_ROOT/opencode_print_config"
printf '==> capabilities_require_podman\n' printf '==> opencode_print_config_runtime_flags\n'
local output_log
output_log=$(mktemp)
setup_stub_env setup_stub_env
if PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" SLOPTRAP_CONTAINER_ENGINE=docker \ mkdir -p "$scenario_dir"
"$SLOPTRAP_BIN" --dry-run "$scenario_dir" >"$output_log" 2>&1; then cat > "$scenario_dir/.sloptrap" <<'EOF'
record_failure "capabilities_require_podman: expected docker capability run to be rejected" name=opencode-print-config
elif ! grep -q -- 'capability-enabled runs require podman' "$output_log"; then packages_extra=
record_failure "capabilities_require_podman: missing explicit podman requirement" agent=opencode
allow_host_network=false
EOF
local output
if ! output=$(env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
"$SLOPTRAP_BIN" --print-config "$scenario_dir" 2>/dev/null); then
record_failure "opencode_print_config_runtime_flags: print-config failed"
teardown_stub_env
return
fi
if ! grep -q 'runtime_flags=' <<<"$output"; then
record_failure "opencode_print_config_runtime_flags: runtime_flags line missing for opencode"
fi
if ! grep -q 'opencode_config=' <<<"$output"; then
record_failure "opencode_print_config_runtime_flags: opencode config path missing"
fi
if grep -q -- '--sandbox danger-full-access --ask-for-approval never' <<<"$output"; then
record_failure "opencode_print_config_runtime_flags: codex runtime flags leaked into opencode config"
fi fi
teardown_stub_env teardown_stub_env
rm -f "$output_log"
} }
run_capability_profiles() { run_opencode_env_overrides() {
local scenario_dir="$TEST_ROOT/capability_repo" local scenario_dir="$TEST_ROOT/opencode_print_config"
printf '==> capability_profiles\n' printf '==> opencode_env_overrides\n'
setup_stub_env setup_stub_env
local main_lines capture_lines pod_lines mkdir -p "$scenario_dir"
local expected_build_network="bridge" cat > "$scenario_dir/.sloptrap" <<'EOF'
if command -v slirp4netns >/dev/null 2>&1; then name=opencode-print-config
expected_build_network="slirp4netns" packages_extra=
fi agent=opencode
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ opencode_server=http://manifest:8080
"$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" </dev/null >/dev/null 2>&1; then opencode_model=manifest-model
record_failure "capability_profiles: sloptrap exited non-zero" 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 teardown_stub_env
return return
fi fi
if ! grep -q -- "CAPABILITY_PACKAGES=tcpdump" "$STUB_LOG"; then plain_output=$(printf '%s' "$output" | sed -E $'s/\x1B\\[[0-9;]*m//g')
record_failure "capability_profiles: build arg for capability packages missing" if ! grep -q 'opencode_server=http://env:8080' <<<"$plain_output"; then
record_failure "opencode_env_overrides: server env override missing"
fi fi
if ! grep -q -- "FAKE PODMAN: build --quiet -t capability-repo-sloptrap-image -f .* --network $expected_build_network " "$STUB_LOG"; then if ! grep -q 'opencode_model=env-model' <<<"$plain_output"; then
record_failure "capability_profiles: build should stay on isolated networking" record_failure "opencode_env_overrides: model env override missing"
fi fi
main_lines=$(grep "FAKE PODMAN: run " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-container" || true) if ! grep -q 'opencode_context=64K' <<<"$plain_output"; then
capture_lines=$(grep "FAKE PODMAN: run " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-capture" || true) record_failure "opencode_env_overrides: context env override missing"
pod_lines=$(grep "FAKE PODMAN: pod create " "$STUB_LOG" | grep -- "--name capability-repo-sloptrap-pod" || true)
if [[ -z $main_lines ]]; then
record_failure "capability_profiles: main runtime container did not reach the container engine"
fi
if [[ -z $pod_lines ]]; then
record_failure "capability_profiles: packet capture should create a dedicated pod"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add NET_RAW"* ]]; then
record_failure "capability_profiles: capture sidecar should receive NET_RAW"
fi
if [[ -n $main_lines && $main_lines == *"--cap-add NET_RAW"* ]]; then
record_failure "capability_profiles: main container should not receive NET_RAW"
fi
if grep -q -- "--cap-add NET_ADMIN" <<<"$capture_lines"; then
record_failure "capability_profiles: NET_ADMIN should not be granted"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add SETUID"* ]]; then
record_failure "capability_profiles: SETUID capability missing"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add SETGID"* ]]; then
record_failure "capability_profiles: SETGID capability missing"
fi
if [[ -z $capture_lines || $capture_lines != *"--cap-add CHOWN"* ]]; then
record_failure "capability_profiles: CHOWN capability missing"
fi
if grep -q -- "--cap-add DAC_OVERRIDE" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: DAC_OVERRIDE should not be granted"
fi
if grep -q -- "--cap-add FOWNER" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: FOWNER should not be granted"
fi
if ! grep -q -- "--security-opt no-new-privileges" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: no-new-privileges missing"
fi
if grep -q -- "--read-only" <<<"$main_lines"; then
record_failure "capability_profiles: apt profile should disable read-only rootfs"
fi
if grep -q -- "--user " <<<"$main_lines"; then
record_failure "capability_profiles: capability-enabled run should not force --user"
fi
if ! grep -q -- "--userns=keep-id:uid=$(id -u),gid=$(id -g)" <<<"$capture_lines$main_lines"; then
record_failure "capability_profiles: podman keep-id user namespace missing"
fi
if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=apt-install" <<<"$main_lines"; then
record_failure "capability_profiles: main helper capability environment missing"
fi
if ! grep -q -- "SLOPTRAP_PACKET_CAPTURE_ENABLED=1" <<<"$main_lines"; then
record_failure "capability_profiles: main container should advertise packet capture availability"
fi
if ! grep -q -- "SLOPTRAP_HELPER_DIR=/codex/state/capture-helper" <<<"$capture_lines"; then
record_failure "capability_profiles: capture sidecar helper dir missing"
fi
if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=packet-capture" <<<"$capture_lines"; then
record_failure "capability_profiles: capture sidecar capability environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_UID=$(id -u)" "$STUB_LOG"; then
record_failure "capability_profiles: host uid environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_GID=$(id -g)" "$STUB_LOG"; then
record_failure "capability_profiles: host gid environment missing"
fi
if ! grep -q -- "SLOPTRAP_HOST_USER=$(id -un)" "$STUB_LOG"; then
record_failure "capability_profiles: host user environment missing"
fi
local state_root capability_dir
state_root="$STUB_HOME/.codex/sloptrap/state"
capability_dir=$(find "$state_root" -mindepth 2 -maxdepth 2 -type d -name capabilities | head -n 1 || true)
if [[ -z $capability_dir ]]; then
record_failure "capability_profiles: project capability state directory missing"
fi fi
teardown_stub_env teardown_stub_env
} }
run_embedded_capability_helpers() { run_opencode_config_symlink_rejected() {
printf '==> embedded_capability_helpers\n' local scenario_dir="$TEST_ROOT/opencode_localhost"
local temp_root helper_bin helper_dir workspace_dir capture_dir tool_log helper_pid printf '==> opencode_config_symlink_rejected\n'
temp_root=$(mktemp -d)
helper_bin="$temp_root/bin"
helper_dir="$temp_root/helper"
workspace_dir="$temp_root/workspace"
capture_dir="$temp_root/captures"
tool_log="$temp_root/tool.log"
helper_pid=""
mkdir -p "$helper_bin" "$helper_dir/queue" "$workspace_dir/data" "$capture_dir"
: >"$tool_log"
if ! extract_embedded_helper "sloptrap-entrypoint" "$helper_bin/sloptrap-entrypoint" \
|| ! extract_embedded_helper "sloptrap-helperd" "$helper_bin/sloptrap-helperd" \
|| ! extract_embedded_helper "slop-apt" "$helper_bin/slop-apt" \
|| ! extract_embedded_helper "slopcap" "$helper_bin/slopcap"; then
record_failure "embedded_capability_helpers: failed to extract embedded helper scripts"
rm -rf "$temp_root"
return
fi
cat >"$helper_bin/apt-get" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'apt-get %s\n' "$*" >>"$TEST_TOOL_LOG"
exit 0
EOF
cat >"$helper_bin/tcpdump" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'tcpdump %s\n' "$*" >>"$TEST_TOOL_LOG"
output=""
prev=""
for arg in "$@"; do
if [[ $prev == "-w" ]]; then
output=$arg
break
fi
prev=$arg
done
if [[ -n $output ]]; then
mkdir -p "$(dirname "$output")"
: >"$output"
fi
exit 0
EOF
cat >"$helper_bin/setpriv" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'setpriv %s\n' "$*" >>"$TEST_TOOL_LOG"
while [[ $# -gt 0 ]]; do
case "$1" in
--reuid|--regid)
shift 2
;;
--clear-groups)
shift
;;
--)
shift
break
;;
*)
break
;;
esac
done
exec "$@"
EOF
chmod +x "$helper_bin/apt-get" "$helper_bin/tcpdump" "$helper_bin/setpriv"
if ! grep -q "chmod 711 \"\\\$helper_dir\"" "$helper_bin/sloptrap-entrypoint" \
|| ! grep -q "chmod 1733 \"\\\$queue_dir\"" "$helper_bin/sloptrap-entrypoint"; then
record_failure "embedded_capability_helpers: entrypoint did not expose helper queue to the dropped user"
fi
if grep -q -- "setpriv --reuid 0 --regid 0" "$helper_bin/slop-apt" \
|| grep -q -- "setpriv --reuid 0 --regid 0" "$helper_bin/slopcap"; then
record_failure "embedded_capability_helpers: helper clients should not attempt to regain root"
fi
if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$temp_root/helper-missing" \
SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
"$helper_bin/slop-apt" install jq >"$temp_root/missing-helper.out" 2>"$temp_root/missing-helper.err"; then
record_failure "embedded_capability_helpers: slop-apt should fail when the root helper is unavailable"
elif ! grep -q -- 'capability helper is unavailable' "$temp_root/missing-helper.err"; then
record_failure "embedded_capability_helpers: missing helper failure should explain the boundary"
fi
TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \
SLOPTRAP_APT_GET_BIN="$helper_bin/apt-get" \
SLOPTRAP_TCPDUMP_BIN="$helper_bin/tcpdump" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_AUDIT_LOG="$temp_root/audit.log" "$helper_bin/sloptrap-helperd" >/dev/null 2>&1 &
helper_pid=$!
if ! wait_for_path "$helper_dir/helperd.pid"; then
record_failure "embedded_capability_helpers: helper daemon did not publish its pid file"
fi
if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
"$helper_bin/slop-apt" install jq >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slop-apt failed against the embedded helper daemon"
fi
if ! grep -q -- 'apt-get install -y --no-install-recommends jq' "$tool_log"; then
record_failure "embedded_capability_helpers: slop-apt did not reach apt-get install"
fi
local bad_apt_request
bad_apt_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req")
printf 'apt-install\n' >"$bad_apt_request/op"
printf '%s\n' '--allow-unauthenticated' >"$bad_apt_request/packages"
if ! wait_for_path "$bad_apt_request/status"; then
record_failure "embedded_capability_helpers: helper daemon did not answer the invalid apt request"
elif [[ $(<"$bad_apt_request/status") != "2" ]]; then
record_failure "embedded_capability_helpers: invalid apt request returned the wrong status"
fi
if [[ -s "$bad_apt_request/stderr" ]] && ! grep -q -- 'invalid package name' "$bad_apt_request/stderr"; then
record_failure "embedded_capability_helpers: invalid apt request did not explain the rejection"
fi
if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_CAPTURE_HELPER_DIR="$helper_dir" SLOPTRAP_PACKET_CAPTURE_ENABLED=1 \
"$helper_bin/slopcap" capture --interface eth0 --output /tmp/escape.pcap >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slopcap accepted an out-of-bounds output path"
fi
if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \
SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \
SLOPTRAP_CAPTURE_HELPER_DIR="$helper_dir" SLOPTRAP_PACKET_CAPTURE_ENABLED=1 \
"$helper_bin/slopcap" capture --interface eth0 --filter 'tcp port 80' \
--output "$workspace_dir/capture.pcap" >/dev/null 2>&1; then
record_failure "embedded_capability_helpers: slopcap failed for a workspace-local capture file"
fi
if ! grep -q -- "tcpdump -p -i eth0 -w $workspace_dir/capture.pcap -- tcp port 80" "$tool_log"; then
record_failure "embedded_capability_helpers: slopcap did not invoke tcpdump with the expected guarded arguments"
fi
local bad_capture_request
bad_capture_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req")
printf 'packet-capture\n' >"$bad_capture_request/op"
printf 'eth0\n' >"$bad_capture_request/interface"
printf '\n' >"$bad_capture_request/filter"
printf '/tmp/escape.pcap\n' >"$bad_capture_request/output"
printf '0\n' >"$bad_capture_request/stdout_mode"
if ! wait_for_path "$bad_capture_request/status"; then
record_failure "embedded_capability_helpers: helper daemon did not answer the invalid capture request"
elif [[ $(<"$bad_capture_request/status") != "2" ]]; then
record_failure "embedded_capability_helpers: invalid capture request returned the wrong status"
fi
if [[ -s "$bad_capture_request/stderr" ]] && ! grep -q -- 'output path must stay within' "$bad_capture_request/stderr"; then
record_failure "embedded_capability_helpers: invalid capture request did not explain the rejection"
fi
if [[ -n $helper_pid ]]; then
kill "$helper_pid" >/dev/null 2>&1 || true
wait "$helper_pid" >/dev/null 2>&1 || true
fi
rm -rf "$temp_root"
}
run_make_install_single_file() {
local scenario_dir="$TEST_ROOT/resume_target"
printf '==> make_install_single_file\n'
if ! command -v make >/dev/null 2>&1; then
record_failure "make_install_single_file: make binary not found in PATH"
return
fi
setup_stub_env setup_stub_env
local install_root install_dir installed_bin mkdir -p "$scenario_dir"
install_root=$(mktemp -d) cat > "$scenario_dir/.sloptrap" <<'EOF'
install_dir="$install_root/bin" name=opencode-localhost
installed_bin="$install_dir/sloptrap" packages_extra=
if ! make -C "$ROOT_DIR" install INSTALL_DIR="$install_dir" >/dev/null 2>&1; then agent=opencode
record_failure "make_install_single_file: make install failed" allow_host_network=false
teardown_stub_env EOF
rm -rf "$install_root" local state_key
return state_key=$(printf '%s' "$scenario_dir" | sha256sum | awk '{print $1}')
fi local config_dir="$STUB_HOME/.codex/sloptrap/state/$state_key/config/opencode"
if [[ ! -x $installed_bin ]]; then mkdir -p "$config_dir"
record_failure "make_install_single_file: installed launcher missing" ln -s "$STUB_HOME/target.json" "$config_dir/opencode.json"
fi if env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
local helper "$SLOPTRAP_BIN" "$scenario_dir" </dev/null >/dev/null 2>&1; then
for helper in sloptrap-entrypoint sloptrap-helperd slop-apt slopcap; do record_failure "opencode_config_symlink_rejected: expected rejection for symlinked config"
if [[ -e $install_dir/$helper ]]; then fi
record_failure "make_install_single_file: unexpected helper installed ($helper)" teardown_stub_env
fi }
done
if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ run_opencode_login_rejected() {
"$installed_bin" "$scenario_dir" </dev/null >/dev/null 2>&1; then local scenario_dir="$TEST_ROOT/opencode_localhost"
record_failure "make_install_single_file: installed launcher failed" printf '==> opencode_login_rejected\n'
fi setup_stub_env
if ! grep -q -- "FAKE PODMAN: build " "$STUB_LOG"; then if env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \
record_failure "make_install_single_file: installed launcher did not reach build path" "$SLOPTRAP_BIN" "$scenario_dir" login </dev/null >/dev/null 2>&1; then
fi record_failure "opencode_login_rejected: expected login rejection for opencode"
if ! make -C "$ROOT_DIR" uninstall INSTALL_DIR="$install_dir" >/dev/null 2>&1; then
record_failure "make_install_single_file: make uninstall failed"
fi
if [[ -e $installed_bin ]]; then
record_failure "make_install_single_file: installed launcher not removed by uninstall"
fi fi
teardown_stub_env teardown_stub_env
rm -rf "$install_root"
} }
run_shellcheck
run_mount_injection
run_root_target
run_symlink_escape run_symlink_escape
run_manifest_injection run_manifest_injection
run_helper_symlink run_helper_symlink
run_secret_mask run_secret_mask
run_git_ignore_mask
run_resume_target run_resume_target
run_runtime_context_prompt run_runtime_context_prompt
run_sh_reexec run_sh_reexec
run_resume_omits_runtime_context run_resume_omits_runtime_context
run_shell_target_uses_entrypoint
run_auth_file_mount run_auth_file_mount
run_runtime_hardening_flags
run_codex_home_override run_codex_home_override
run_project_state_isolation run_project_state_isolation
run_auto_login_empty_auth run_auto_login_empty_auth
run_codex_symlink_home run_codex_symlink_home
run_root_directory_project run_root_directory_project
run_shared_dir_override
run_packages_env_validation
run_abs_path_ignore
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_host_network_packet_capture_ack_required
run_host_network_packet_capture_ack_prompt
run_removed_nested_podman_manifest
run_wizard_create_manifest run_wizard_create_manifest
run_wizard_existing_defaults run_wizard_existing_defaults
run_wizard_build_trigger run_wizard_build_trigger
run_capability_trust_required run_invalid_manifest_agent
run_capabilities_require_podman run_invalid_agent_env_override
run_capability_profiles run_removed_runtime_override_envs
run_embedded_capability_helpers run_removed_build_override_envs
run_make_install_single_file 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 if [[ ${#failures[@]} -gt 0 ]]; then
printf '\nTest failures:\n' printf '\nTest failures:\n'

View File

@@ -1,4 +1,4 @@
name=wizard_build name=wizard_build
packages_extra= packages_extra=
capabilities= agent=codex
allow_host_network=false allow_host_network=false

View File

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

View File

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