diff --git a/.sloptrap b/.sloptrap index 4a1001a..2a748e4 100644 --- a/.sloptrap +++ b/.sloptrap @@ -1,4 +1,4 @@ name=skz-sloptrap packages_extra=bash make shellcheck jq podman iproute2 strace -capabilities= +agent=codex allow_host_network=false diff --git a/AGENTS.md b/AGENTS.md index 8e8ffa0..c1ce051 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,4 @@ Do not remove existing instructions unless they are outdated or wrong. - 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. +- `opencode` TUI accepts `--prompt`, so sloptrap can pass startup/runtime guidance without switching to non-interactive `opencode run`. diff --git a/README.md b/README.md index f6332a7..011ff3b 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,12 @@ brew install coreutils gnu-tar jq - 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. +> Use `./sloptrap path/to/project shell` to enter a troubleshooting shell inside the container or `./sloptrap path/to/project clean` to remove cached images, state, and the project tools volume. ## How It Works - The project directory mounts at `/workspace`; project-scoped state mounts at `/codex` from `${HOME}/.codex/sloptrap/state/`. Codex also mounts shared auth from `${HOME}/.codex/auth.json` to `/codex/auth.json`; opencode does not. +- Each project also gets a dedicated engine-managed volume mounted at `/sloptrap-tools`, with `/sloptrap-tools/bin` prepended to `PATH`. This is the supported writable install prefix for third-party agent tools and is kept outside the mounted home/state tree. - `.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`. 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`. @@ -146,6 +147,7 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo - Filesystem view: - **Codex**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/` at `/codex`; auth at `/codex/auth.json`. - **opencode**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/` at `/codex`; generated config at `/codex/config/opencode/opencode.json`; runtime state at `/codex/state/opencode`; no shared auth mount. + - **Both backends**: per-project tools volume at `/sloptrap-tools`; `/sloptrap-tools/bin` is on `PATH`; `SLOPTRAP_TOOLS_HOME=/sloptrap-tools`. - 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. @@ -157,6 +159,7 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo - **Outbound disclosure**: prompts and referenced data travel from the container to the configured LLM endpoint. Any file content within `/workspace` or environment data exposed to the process can appear in that traffic. - **Shared storage**: `/workspace` and 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. +- **Tools persistence**: `/sloptrap-tools` is a per-project container-engine volume, not a bind mount into `${HOME}`. Anything installed there persists across runs for that project until `clean` removes the volume. - **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. diff --git a/sloptrap b/sloptrap index 5fb07e8..42ecf2c 100755 --- a/sloptrap +++ b/sloptrap @@ -236,6 +236,9 @@ CODEX_AUTH_FILE_HOST="" CODEX_STATE_KEY="" CODEX_HOME_BOOTSTRAP=false NEED_LOGIN=false +SLOPTRAP_TOOLS_HOME_CONT="/sloptrap-tools" +SLOPTRAP_TOOLS_BIN_CONT="/sloptrap-tools/bin" +SLOPTRAP_TOOLS_VOLUME="" IGNORE_STUB_BASE="" IGNORE_HELPER_ROOT="" ALLOW_HOST_NETWORK=false @@ -341,11 +344,15 @@ ARG OPENCODE_BIN=opencode ARG OPENCODE_CONF=config/config.toml COPY ${OPENCODE_BIN} /usr/local/bin/opencode RUN chmod 0755 /usr/local/bin/opencode \ + && mkdir -p /sloptrap-tools/bin \ + && chmod 0777 /sloptrap-tools /sloptrap-tools/bin \ && chown -R sloptrap:sloptrap /home/sloptrap WORKDIR /workspace -ENV SHELL=/bin/bash HOME=/home/sloptrap +ENV SHELL=/bin/bash HOME=/home/sloptrap \ + SLOPTRAP_TOOLS_HOME=/sloptrap-tools \ + PATH=/sloptrap-tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENTRYPOINT ["opencode"] DOCKERFILE_EOF ) @@ -376,11 +383,15 @@ ARG CODEX_BIN=codex ARG CODEX_CONF=config/config.toml COPY ${CODEX_BIN} /usr/local/bin/codex RUN chmod 0755 /usr/local/bin/codex \ + && mkdir -p /sloptrap-tools/bin \ + && chmod 0777 /sloptrap-tools /sloptrap-tools/bin \ && chown -R sloptrap:sloptrap /home/sloptrap WORKDIR /workspace -ENV SHELL=/bin/bash HOME=/home/sloptrap +ENV SHELL=/bin/bash HOME=/home/sloptrap \ + SLOPTRAP_TOOLS_HOME=/sloptrap-tools \ + PATH=/sloptrap-tools/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENTRYPOINT ["/usr/local/bin/codex"] EOF fi @@ -1036,6 +1047,9 @@ print_config() { info_line "runtime_flags=%s\n" "$CODEX_ARGS_DISPLAY" fi info_line "host_alias=%s\n" "${SLOPTRAP_HOST_ALIAS:-}" + info_line "tools_volume=%s\n" "$SLOPTRAP_TOOLS_VOLUME" + info_line "tools_home=%s\n" "$SLOPTRAP_TOOLS_HOME_CONT" + info_line "tools_bin=%s\n" "$SLOPTRAP_TOOLS_BIN_CONT" info_line "needs_login=%s\n" "$NEED_LOGIN" info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE" if [[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]]; then @@ -1098,6 +1112,8 @@ Current resolved sloptrap state: - name=$PROJECT_NAME (project/image/container label) - packages_extra=${PACKAGES_EXTRA:-none} (Debian packages added at build time) - network_mode=$network_mode (host when host networking is enabled; otherwise isolated) +- tools_home=$SLOPTRAP_TOOLS_HOME_CONT (writable install prefix for third-party tools) +- tools_bin=$SLOPTRAP_TOOLS_BIN_CONT (already on the default PATH) EOF ) if [[ -n $SLOPTRAP_HOST_ALIAS ]]; then @@ -1122,6 +1138,7 @@ SLOPTRAP_CODEX_BIN_NAME="" SLOPTRAP_CODEX_URL="" SLOPTRAP_CODEX_ARCHIVE="" SLOPTRAP_CODEX_HOME_CONT="" +SLOPTRAP_RUNTIME_PATH="" SLOPTRAP_VOLUME_LABEL="" SLOPTRAP_WORKDIR=${SLOPTRAP_WORKDIR-} SLOPTRAP_NETWORK_NAME="" @@ -1314,6 +1331,7 @@ ensure_opencode_config() { enabled_providers: [$provider_id], share: "disabled", autoupdate: false, + lsp: true, provider: { ($provider_id): { npm: "@ai-sdk/openai-compatible", @@ -1328,7 +1346,7 @@ ensure_opencode_config() { name: $model_id, limit: { context: $context_limit, - output: 32768 + output: 16384 }, cost: { input: 0.0221, @@ -1344,9 +1362,10 @@ ensure_opencode_config() { }, model: $model_ref, compaction: { + threshold: 0.95, + strategy: "summarize", auto: true, - prune: true, - reserved: 12000 + prune: true }, watcher: { ignore: [ @@ -1363,14 +1382,14 @@ ensure_opencode_config() { grep: "allow", list: "allow", bash: "allow", - task: "allow", + task: "deny", question: "allow", webfetch: "allow", websearch: "allow", codesearch: "allow", external_directory: "allow", - doom_loop: "ask" - } + doom_loop: "deny" + }, "instructions": [ "AGENTS.md" ] @@ -1654,6 +1673,10 @@ prepare_container_runtime() { if [[ "$BACKEND" == "opencode" ]]; then OPENCODE_CONFIG_CONT="$SLOPTRAP_CODEX_HOME_CONT/config/opencode/opencode.json" fi + SLOPTRAP_TOOLS_HOME_CONT="/sloptrap-tools" + SLOPTRAP_TOOLS_BIN_CONT="$SLOPTRAP_TOOLS_HOME_CONT/bin" + SLOPTRAP_RUNTIME_PATH="$SLOPTRAP_TOOLS_BIN_CONT:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + SLOPTRAP_TOOLS_VOLUME=$(sanitize_engine_name "${PROJECT_NAME}-sloptrap-tools-${CODEX_STATE_KEY:0:12}") SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337") SLOPTRAP_CODEX_GID=$(get_env_default "SLOPTRAP_CODEX_GID" "1337") local default_network="bridge" @@ -1672,8 +1695,8 @@ prepare_container_runtime() { ensure_host_loopback_network_access fi SLOPTRAP_LIMITS_PID=$(get_env_default "SLOPTRAP_LIMITS_PID" "4096") - SLOPTRAP_LIMITS_RAM=$(get_env_default "SLOPTRAP_LIMITS_RAM" "4096m") - SLOPTRAP_LIMITS_SWP=$(get_env_default "SLOPTRAP_LIMITS_SWP" "4096m") + SLOPTRAP_LIMITS_RAM=$(get_env_default "SLOPTRAP_LIMITS_RAM" "16384m") + SLOPTRAP_LIMITS_SWP=$(get_env_default "SLOPTRAP_LIMITS_SWP" "16384m") 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") @@ -1717,6 +1740,9 @@ prepare_container_runtime() { -v "$SLOPTRAP_SHARED_DIR_ABS:$SLOPTRAP_WORKDIR$SLOPTRAP_VOLUME_LABEL" -v "$CODEX_STATE_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL" ) + local -a mount_opts=( + --mount "type=volume,source=$SLOPTRAP_TOOLS_VOLUME,target=$SLOPTRAP_TOOLS_HOME_CONT" + ) # Add opencode state mount if using opencode backend if [[ "$BACKEND" == "opencode" ]]; then @@ -1729,6 +1755,8 @@ prepare_container_runtime() { -e "XDG_CACHE_HOME=$SLOPTRAP_CODEX_HOME_CONT/cache" -e "XDG_STATE_HOME=$SLOPTRAP_CODEX_HOME_CONT/state" -e "CODEX_HOME=$SLOPTRAP_CODEX_HOME_CONT" + -e "PATH=$SLOPTRAP_RUNTIME_PATH" + -e "SLOPTRAP_TOOLS_HOME=$SLOPTRAP_TOOLS_HOME_CONT" -e "SLOPTRAP_WORKDIR=$SLOPTRAP_WORKDIR" -e "SLOPTRAP_HELPER_DIR=/tmp/sloptrap-helper" ) @@ -1783,6 +1811,7 @@ prepare_container_runtime() { "${resource_opts[@]}" "${rootfs_flag[@]}" "${tmpfs_opts[@]}" + "${mount_opts[@]}" "${volume_opts[@]}" "${env_args[@]}" "${IGNORE_MOUNT_ARGS[@]}" @@ -1943,11 +1972,13 @@ clean_environment() { if $DRY_RUN; then print_command "$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME" print_command "$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME" + print_command "$CONTAINER_ENGINE" volume rm -f "$SLOPTRAP_TOOLS_VOLUME" print_command rm -rf "$helper_root" return 0 fi "$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME" >/dev/null 2>&1 || true "$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1 || true + "$CONTAINER_ENGINE" volume rm -f "$SLOPTRAP_TOOLS_VOLUME" >/dev/null 2>&1 || true rm -rf "$helper_root" } @@ -1991,11 +2022,11 @@ run_codex() { if ! $DRY_RUN; then status_line "Running %s\n" "$SLOPTRAP_IMAGE_NAME" fi + local runtime_prompt + runtime_prompt=$(build_runtime_context_prompt) if [[ "$BACKEND" == "opencode" ]]; then - run_codex_command + run_codex_command --prompt "$runtime_prompt" else - local runtime_prompt - runtime_prompt=$(build_runtime_context_prompt) run_codex_command "$runtime_prompt" fi } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 22456d3..c3a538b 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -418,6 +418,9 @@ run_runtime_context_prompt() { if [[ -z $run_line || $run_line != *"You are running inside sloptrap"* ]]; then record_failure "runtime_context_prompt: startup prompt missing from fresh run" fi + if ! grep -q -- "tools_bin=/sloptrap-tools/bin" "$STUB_LOG"; then + record_failure "runtime_context_prompt: tools path missing from startup prompt" + fi if ! grep -q -- "name=host-network-repo" "$STUB_LOG" \ || ! grep -q -- "network_mode=host" "$STUB_LOG"; then record_failure "runtime_context_prompt: runtime summary missing manifest state" @@ -480,6 +483,12 @@ run_shell_target_uses_entrypoint() { 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 + if ! grep -q -- "-e PATH=/sloptrap-tools/bin:" "$STUB_LOG"; then + record_failure "shell_target_uses_entrypoint: tools PATH export missing" + fi + if ! grep -q -- "-e SLOPTRAP_TOOLS_HOME=/sloptrap-tools" "$STUB_LOG"; then + record_failure "shell_target_uses_entrypoint: tools home export missing" + fi teardown_stub_env } @@ -502,6 +511,9 @@ run_auth_file_mount() { if ! grep -q -- "-v ${STUB_HOME}/.codex/sloptrap/state/" "$STUB_LOG"; then record_failure "auth_file_mount: missing project state bind mount" fi + if ! grep -q -- "--mount type=volume,source=resume_target-sloptrap-tools-" "$STUB_LOG"; then + record_failure "auth_file_mount: missing project tools volume mount" + fi teardown_stub_env } @@ -898,6 +910,12 @@ 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 -- "--prompt You are running inside sloptrap" "$STUB_LOG"; then + record_failure "opencode_localhost_rewrite: startup prompt not passed to opencode" + fi + if ! grep -q -- "tools_home=/sloptrap-tools" "$STUB_LOG"; then + record_failure "opencode_localhost_rewrite: tools path missing from opencode prompt" + 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 @@ -922,8 +940,8 @@ EOF 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" + if ! grep -q -- "-e PATH=/sloptrap-tools/bin:" "$STUB_LOG"; then + record_failure "opencode_localhost_rewrite: tools PATH export missing" fi teardown_stub_env } @@ -940,24 +958,51 @@ agent=opencode 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_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 + plain_output=$(printf '%s' "$output" | sed -E $'s/\x1B\\[[0-9;]*m//g') + if ! grep -q 'runtime_flags=' <<<"$plain_output"; then record_failure "opencode_print_config_runtime_flags: runtime_flags line missing for opencode" fi - if ! grep -q 'opencode_config=' <<<"$output"; then + if ! grep -q 'opencode_config=' <<<"$plain_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 + if ! grep -q 'tools_volume=' <<<"$plain_output"; then + record_failure "opencode_print_config_runtime_flags: tools volume missing" + fi + if ! grep -q 'tools_home=/sloptrap-tools' <<<"$plain_output"; then + record_failure "opencode_print_config_runtime_flags: tools home missing" + fi + if ! grep -q 'tools_bin=/sloptrap-tools/bin' <<<"$plain_output"; then + record_failure "opencode_print_config_runtime_flags: tools bin missing" + fi + if grep -q -- '--sandbox danger-full-access --ask-for-approval never' <<<"$plain_output"; then record_failure "opencode_print_config_runtime_flags: codex runtime flags leaked into opencode config" fi teardown_stub_env } +run_clean_removes_tools_volume() { + local scenario_dir="$TEST_ROOT/resume_target" + printf '==> clean_removes_tools_volume\n' + setup_stub_env + if ! env PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" \ + "$SLOPTRAP_BIN" "$scenario_dir" clean /dev/null 2>&1; then + record_failure "clean_removes_tools_volume: clean failed" + teardown_stub_env + return + fi + if ! grep -q -- "FAKE PODMAN: volume rm -f resume_target-sloptrap-tools-" "$STUB_LOG"; then + record_failure "clean_removes_tools_volume: tools volume not removed" + fi + teardown_stub_env +} + run_opencode_env_overrides() { local scenario_dir="$TEST_ROOT/opencode_print_config" printf '==> opencode_env_overrides\n' @@ -1057,6 +1102,7 @@ run_opencode_print_config_runtime_flags run_opencode_env_overrides run_opencode_config_symlink_rejected run_opencode_login_rejected +run_clean_removes_tools_volume if [[ ${#failures[@]} -gt 0 ]]; then printf '\nTest failures:\n'