diff --git a/AGENTS.md b/AGENTS.md index a5d794e..8e8ffa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) - 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. +- `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. diff --git a/Dockerfile.sloptrap b/Dockerfile.sloptrap deleted file mode 100644 index 2f79961..0000000 --- a/Dockerfile.sloptrap +++ /dev/null @@ -1,28 +0,0 @@ -# Dockerfile.sloptrap -ARG BASE_IMAGE=debian:trixie-slim -FROM ${BASE_IMAGE} - -ENV DEBIAN_FRONTEND=noninteractive - -ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps util-linux" -ARG EXTRA_PACKAGES="" -RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} \ - && rm -rf /var/lib/apt/lists/* - -ARG CODEX_UID=1337 -ARG CODEX_GID=1337 -RUN groupadd --gid ${CODEX_GID} sloptrap \ - && useradd --create-home --home-dir /home/sloptrap \ - --gid sloptrap --uid ${CODEX_UID} --shell /bin/bash sloptrap - -ARG CODEX_BIN=codex -ARG CODEX_BIN_PATH=/usr/local/bin/codex -COPY ${CODEX_BIN} ${CODEX_BIN_PATH} -RUN chmod 0755 ${CODEX_BIN_PATH} \ - && chown -R sloptrap:sloptrap /home/sloptrap - -WORKDIR /workspace - -ENV SHELL=/bin/bash HOME=/home/sloptrap -ENTRYPOINT ["${CODEX_BIN_PATH}"] diff --git a/README.md b/README.md index c674432..8d9a379 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,9 @@ Supported keys when the manifest is present: | `name` | project directory name | Must match `^[A-Za-z0-9_.-]+$`. Used for image/container naming. | | `packages_extra` | *empty* | Additional Debian packages installed during `docker/podman build`. Tokens must be alphanumeric plus `+.-`. | | `agent` | `codex` | AI backend: `codex` (OpenAI Codex CLI) or `opencode` (Anomaly opencode CLI). | -| `opencode_server` | `http://localhost:11434` | OpenAI-compatible server URL (opencode only). Supports llama.cpp, Ollama, vLLM, etc. | -| `opencode_model` | `llama3` | Model name on the server (opencode only). | +| `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. | Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start. @@ -73,7 +74,7 @@ Values containing `$`, `` ` ``, or newlines are rejected to prevent command inje **Codex** (default): Uses OpenAI Codex CLI with state stored in `~/.codex/`. Supports login mode for credential sharing. -**opencode**: Uses Anomaly opencode CLI with state stored in `~/.opencode/`. Connects to any OpenAI-compatible inference server (llama.cpp, Ollama, vLLM, etc.). No authentication required for self-hosted models; API keys supported via manifest if needed. +**opencode**: Uses Anomaly opencode CLI with state stored in `~/.opencode/`. sloptrap downloads the latest Linux CLI release artifact from Anomaly during image builds, verifies its digest from the GitHub release metadata, and copies it into the container image. Connects to any OpenAI-compatible inference server (llama.cpp, Ollama, vLLM, etc.). When `opencode_server` points at `localhost` under isolated networking, sloptrap rewrites it to `http://sloptrap.host:...` so host-local model servers remain reachable from inside the container. No authentication required for self-hosted models; API keys supported via manifest if needed. ### `.sloptrapignore` @@ -142,7 +143,7 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo - **Codex**: project directory at `/workspace`; `${HOME}/.codex/sloptrap/state/` at `/codex`; auth at `/codex/auth.json`. - **opencode**: project directory at `/workspace`; `${HOME}/.opencode/sloptrap/state/` at `/codex/state/opencode`; state at `/codex/state`. - 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`. +- 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. diff --git a/sloptrap b/sloptrap index 9c3b779..e270cbd 100755 --- a/sloptrap +++ b/sloptrap @@ -242,7 +242,11 @@ ALLOW_HOST_NETWORK=false BACKEND="codex" OPENCODE_SERVER="" OPENCODE_MODEL="" +OPENCODE_CONTEXT="" OPENCODE_STATE_HOME_HOST="" +OPENCODE_CONFIG_HOST="" +OPENCODE_CONFIG_CONT="" +SLOPTRAP_HOST_ALIAS="" declare -a SLOPTRAP_TEMP_PATHS=() @@ -302,24 +306,35 @@ create_temp_dir() { write_embedded_dockerfile() { if [[ "$BACKEND" == "opencode" ]]; then - cat <<'EOF' + local opencode_pkg + case "$(uname -m)" in + x86_64|amd64) + opencode_pkg="opencode-desktop-linux-amd64.deb" + ;; + arm64|aarch64) + opencode_pkg="opencode-desktop-linux-arm64.deb" + ;; + *) + error "unsupported architecture for opencode" + ;; + esac + +local dockerfile_content + dockerfile_content=$(cat <<'DOCKERFILE_EOF' # Dockerfile.sloptrap ARG BASE_IMAGE=debian:trixie-slim FROM ${BASE_IMAGE} ENV DEBIAN_FRONTEND=noninteractive -ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps util-linux" -ARG EXTRA_PACKAGES="" -RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} \ - && rm -rf /var/lib/apt/lists/* +ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 wget ripgrep xxd file procps util-linux binutils" +RUN apt-get update && apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} && rm -rf /var/lib/apt/lists/* ARG CODEX_UID=1337 ARG CODEX_GID=1337 RUN groupadd --gid ${CODEX_GID} sloptrap \ && useradd --create-home --home-dir /home/sloptrap \ - --gid sloptrap --uid ${CODEX_UID} --shell /bin/bash sloptrap + --gid sloptrap --uid ${CODEX_UID} --shell /bin/bash sloptrap ARG OPENCODE_BIN=opencode ARG OPENCODE_CONF=config/config.toml @@ -330,8 +345,12 @@ RUN chmod 0755 /usr/local/bin/opencode \ WORKDIR /workspace ENV SHELL=/bin/bash HOME=/home/sloptrap -ENTRYPOINT ["/usr/local/bin/opencode"] -EOF +ENTRYPOINT ["opencode"] +DOCKERFILE_EOF +) + # Replace placeholder with actual package name + dockerfile_content=$(printf '%s' "$dockerfile_content" | sed "s|PLACEHOLDER_OPENCODE_PKG|${opencode_pkg}|g") + printf '%s' "$dockerfile_content" else cat <<'EOF' # Dockerfile.sloptrap @@ -725,6 +744,24 @@ validate_package_list() { done } +# Common package name aliases for user convenience +expand_package_alias() { + local pkg=$1 + case "$pkg" in + rg) printf 'ripgrep' ;; + git) printf 'git' ;; + curl) printf 'curl' ;; + bash) printf 'bash' ;; + ca-certificates) printf 'ca-certificates' ;; + libstdc++6) printf 'libstdc++6' ;; + xxd) printf 'xxd' ;; + file) printf 'file' ;; + procps) printf 'procps' ;; + util-linux) printf 'util-linux' ;; + *) printf '%s' "$pkg" ;; + esac +} + detect_container_engine() { local override=${SLOPTRAP_CONTAINER_ENGINE-} if [[ -n $override ]]; then @@ -815,6 +852,26 @@ normalize_wizard_allow_host_network() { esac } +parse_opencode_context() { + local raw=$1 + local upper=${raw^^} + local number suffix multiplier=1 + [[ -n $upper ]] || error "$MANIFEST_PATH: opencode_context must not be empty" + if [[ $upper =~ ^([0-9]+)([KMG]?)$ ]]; then + number=${BASH_REMATCH[1]} + suffix=${BASH_REMATCH[2]} + else + error "$MANIFEST_PATH: opencode_context must be an integer optionally suffixed with K, M, or G (got '$raw')" + fi + case "$suffix" in + "") multiplier=1 ;; + K) multiplier=1024 ;; + M) multiplier=$((1024 * 1024)) ;; + G) multiplier=$((1024 * 1024 * 1024)) ;; + esac + printf '%s' $(( number * multiplier )) +} + run_wizard() { local manifest_path=$1 if [[ -L $manifest_path ]]; then @@ -832,13 +889,15 @@ run_wizard() { local default_allow_host_network local default_opencode_server local default_opencode_model + local default_opencode_context default_name=$(manifest_default_value "name" "$(basename "$CODE_DIR")") default_packages_extra=$(manifest_default_value "packages_extra" "") default_agent=$(manifest_default_value "agent" "codex") default_allow_host_network=$(manifest_default_value "allow_host_network" "false") - default_opencode_server=$(manifest_default_value "opencode_server" "http://localhost:11434") - default_opencode_model=$(manifest_default_value "opencode_model" "llama3") + default_opencode_server=$(manifest_default_value "opencode_server" "http://localhost:8080") + default_opencode_model=$(manifest_default_value "opencode_model" "bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0") + default_opencode_context=$(manifest_default_value "opencode_context" "256K") local action="Creating" if [[ -f $manifest_path ]]; then @@ -886,22 +945,32 @@ run_wizard() { # If opencode, prompt for server and model if [[ "$default_agent" == "opencode" ]]; then while true; do - info_line "opencode_server: OpenAI-compatible server URL (e.g., http://localhost:11434).\n" + info_line "opencode_server: OpenAI-compatible server URL (e.g., http://localhost:8080).\n" value=$(prompt_manifest_value "opencode_server" "$default_opencode_server") value=$(trim "$value") - [[ -n $value ]] || value="http://localhost:11434" + [[ -n $value ]] || value="http://localhost:8080" default_opencode_server=$value break done while true; do - info_line "opencode_model: Model name on the server (e.g., llama3).\n" + info_line "opencode_model: Model name on the server (e.g., bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0).\n" value=$(prompt_manifest_value "opencode_model" "$default_opencode_model") value=$(trim "$value") - [[ -n $value ]] || value="llama3" + [[ -n $value ]] || value="bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0" default_opencode_model=$value break done + + while true; do + info_line "opencode_context: Context window for the model (e.g., 256K).\n" + value=$(prompt_manifest_value "opencode_context" "$default_opencode_context") + value=$(trim "$value") + [[ -n $value ]] || value="256K" + parse_opencode_context "$value" >/dev/null + default_opencode_context=$value + break + done fi while true; do @@ -924,6 +993,7 @@ EOF cat >> "$manifest_path" <"$OPENCODE_CONFIG_HOST"; then + error "failed to write opencode config '$OPENCODE_CONFIG_HOST'" + fi } fetch_latest_codex_digest() { @@ -1208,42 +1392,43 @@ fetch_latest_codex_digest() { printf '%s' "$digest_line" } -detect_opencode_archive_name() { - local os arch opencode_os opencode_arch - os=$(uname -s 2>/dev/null || true) - arch=$(uname -m 2>/dev/null || true) - [[ -n $os ]] || error "failed to detect host OS for opencode download" - [[ -n $arch ]] || error "failed to detect host architecture for opencode download" - case "$os" in - Linux|Darwin) opencode_os="unknown-linux-gnu" ;; - *) error "unsupported host OS '$os' for opencode download" ;; - esac - case "$arch" in - x86_64|amd64) opencode_arch="x86_64" ;; - arm64|aarch64) opencode_arch="arm64" ;; - *) error "unsupported host architecture '$arch' for opencode download" ;; - esac - printf 'opencode-%s-%s' "$opencode_arch" "$opencode_os" +detect_opencode_asset_name() { + local arch + arch=$(uname -m 2>/dev/null || true) + [[ -n $arch ]] || error "failed to detect host architecture for opencode download" + + case "$arch" in + x86_64|amd64) + printf 'opencode-linux-x64.tar.gz' + ;; + arm64|aarch64) + printf 'opencode-linux-arm64.tar.gz' + ;; + *) + error "unsupported host architecture '$arch' for opencode download" + ;; + esac } fetch_latest_opencode_digest() { - local api_url="https://api.github.com/repos/anomalyco/opencode/releases/latest" - local target_asset="${SLOPTRAP_CODEX_BIN_NAME}.tar.gz" - [[ -n $SLOPTRAP_CODEX_BIN_NAME ]] || error "opencode binary name is not set" - if ! command -v jq >/dev/null 2>&1; then - error "jq is required to verify the opencode binary digest" - fi - local response - if ! response=$(curl -fsSL "$api_url"); then - error "failed to download opencode release metadata from GitHub" - fi - local digest_line - digest_line=$(jq -r --arg name "$target_asset" '.assets[] | select(.name == $name) | .digest' <<<"$response" | head -n 1) - if [[ -z $digest_line || $digest_line == "null" ]]; then - error "failed to resolve opencode digest from GitHub response" - fi - digest_line=${digest_line#sha256:} - printf '%s' "$digest_line" + local api_url="https://api.github.com/repos/anomalyco/opencode/releases/latest" + local target_asset + target_asset=$(detect_opencode_asset_name) + [[ -n $target_asset ]] || error "opencode binary name is not set" + if ! command -v jq >/dev/null 2>&1; then + error "jq is required to verify the opencode binary digest" + fi + local response + if ! response=$(curl -fsSL "$api_url"); then + error "failed to download opencode release metadata from GitHub" + fi + local digest_line + digest_line=$(jq -r --arg name "$target_asset" '.assets[] | select(.name == $name) | .digest' <<<"$response" | head -n 1) + if [[ -z $digest_line || $digest_line == "null" ]]; then + error "failed to resolve opencode digest from GitHub response (looking for $target_asset)" + fi + digest_line=${digest_line#sha256:} + printf '%s' "$digest_line" } ensure_codex_binary() { @@ -1285,28 +1470,30 @@ ensure_opencode_binary() { if [[ -x $CODEX_BIN_PATH ]]; then return 0 fi - local tar_transform="s/${SLOPTRAP_CODEX_BIN_NAME}/${SLOPTRAP_CODEX_BIN_NAME}/" + local target_asset + target_asset=$(detect_opencode_asset_name) local download_dir download_dir=$(create_temp_dir "opencode") - local tmp_archive="$download_dir/${SLOPTRAP_CODEX_BIN_NAME}.tar.gz" + local tmp_archive="$download_dir/$target_asset" + local opencode_url="https://github.com/anomalyco/opencode/releases/latest/download/${target_asset}" if $DRY_RUN; then - print_command curl -Lso "$tmp_archive" "https://github.com/anomalyco/opencode/releases/latest/download/${SLOPTRAP_CODEX_BIN_NAME}.tar.gz" + print_command curl -Lso "$tmp_archive" "$opencode_url" print_command sha256sum -c - - print_command tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT" + print_command tar -xzf "$tmp_archive" --transform="s/opencode/$SLOPTRAP_CODEX_BIN_NAME/" -C "$SLOPTRAP_BUILD_CONTEXT" print_command chmod 0755 "$CODEX_BIN_PATH" return 0 fi - if ! curl -Lso "$tmp_archive" "https://github.com/anomalyco/opencode/releases/latest/download/${SLOPTRAP_CODEX_BIN_NAME}.tar.gz"; then + if ! curl -Lso "$tmp_archive" "$opencode_url"; then rm -rf "$download_dir" - error "failed to download opencode binary from https://github.com/anomalyco/opencode/releases/latest/download/${SLOPTRAP_CODEX_BIN_NAME}.tar.gz" + error "failed to download opencode binary from '$opencode_url'" fi local expected_digest expected_digest=$(fetch_latest_opencode_digest) if ! printf "%s %s\n" "$expected_digest" "$tmp_archive" | sha256sum -c - >/dev/null 2>&1; then rm -rf "$download_dir" "$CODEX_BIN_PATH" - error "checksum verification failed for ${SLOPTRAP_CODEX_BIN_NAME}" + error "checksum verification failed for $SLOPTRAP_CODEX_BIN_NAME" fi - if ! tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"; then + if ! tar -xzf "$tmp_archive" --transform="s/opencode/$SLOPTRAP_CODEX_BIN_NAME/" -C "$SLOPTRAP_BUILD_CONTEXT"; then rm -rf "$download_dir" error "failed to extract opencode binary" fi @@ -1345,7 +1532,13 @@ normalize_package_list() { [[ -z $raw ]] && return 0 local -a tokens=() read -r -a tokens <<< "$raw" - printf '%s' "${tokens[*]}" + local -a normalized=() + local token + for token in "${tokens[@]}"; do + # Expand package aliases + normalized+=("$(expand_package_alias "$token")") + done + printf '%s' "${normalized[*]}" } targets_need_build() { @@ -1364,6 +1557,30 @@ targets_need_build() { return 1 } +normalize_local_server_url() { + local url=$1 + local replacement_host=$2 + if [[ $url =~ ^([A-Za-z][A-Za-z0-9+.-]*://)(localhost|127\.0\.0\.1|\[::1\])([:/?#].*)?$ ]]; then + printf '%s%s%s' "${BASH_REMATCH[1]}" "$replacement_host" "${BASH_REMATCH[3]}" + return 0 + fi + printf '%s' "$url" +} + +ensure_host_loopback_network_access() { + if [[ $SLOPTRAP_NETWORK_NAME != slirp4netns* ]]; then + return 0 + fi + if [[ $SLOPTRAP_NETWORK_NAME == *allow_host_loopback=* ]]; then + return 0 + fi + if [[ $SLOPTRAP_NETWORK_NAME == "slirp4netns" ]]; then + SLOPTRAP_NETWORK_NAME="slirp4netns:allow_host_loopback=true" + else + SLOPTRAP_NETWORK_NAME="${SLOPTRAP_NETWORK_NAME},allow_host_loopback=true" + fi +} + prepare_container_runtime() { resolve_container_workdir SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA @@ -1390,7 +1607,7 @@ prepare_container_runtime() { SLOPTRAP_DOCKERFILE_SOURCE="" fi - SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps util-linux") + SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 wget git ripgrep xxd file procps util-linux") validate_package_list "SLOPTRAP_PACKAGES" "$SLOPTRAP_PACKAGES_BASE" "SLOPTRAP_PACKAGES" local default_codex_archive default_codex_archive=$(detect_codex_archive_name) @@ -1413,8 +1630,15 @@ prepare_container_runtime() { SLOPTRAP_CODEX_ARCHIVE=$inferred_archive fi fi - SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "codex") + if [[ "$BACKEND" == "opencode" ]]; then + SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "opencode") + else + SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "codex") + fi SLOPTRAP_CODEX_HOME_CONT=$(get_env_default "SLOPTRAP_CODEX_HOME_CONT" "/codex") + if [[ "$BACKEND" == "opencode" ]]; then + OPENCODE_CONFIG_CONT="$SLOPTRAP_CODEX_HOME_CONT/config/opencode/opencode.json" + fi SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337") SLOPTRAP_CODEX_GID=$(get_env_default "SLOPTRAP_CODEX_GID" "1337") SLOPTRAP_SECURITY_OPTS_EXTRA=$(get_env_default "SLOPTRAP_SECURITY_OPTS_EXTRA" "") @@ -1432,6 +1656,9 @@ prepare_container_runtime() { elif [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then error "$MANIFEST_PATH: host networking requires allow_host_network=true" fi + if [[ "$BACKEND" == "opencode" && $ALLOW_HOST_NETWORK == false ]]; then + ensure_host_loopback_network_access + fi SLOPTRAP_LIMITS_PID=$(get_env_default "SLOPTRAP_LIMITS_PID" "1024") SLOPTRAP_LIMITS_RAM=$(get_env_default "SLOPTRAP_LIMITS_RAM" "1024m") SLOPTRAP_LIMITS_SWP=$(get_env_default "SLOPTRAP_LIMITS_SWP" "1024m") @@ -1510,6 +1737,7 @@ prepare_container_runtime() { if [[ "$BACKEND" == "opencode" ]]; then env_args+=( -e "OPENCODE_HOME=$SLOPTRAP_CODEX_HOME_CONT" + -e "OPENCODE_CONFIG=$OPENCODE_CONFIG_CONT" -e "OPENCODE_SERVER=$OPENCODE_SERVER" -e "OPENCODE_MODEL=$OPENCODE_MODEL" ) @@ -1527,6 +1755,16 @@ prepare_container_runtime() { env_args+=(-e "SLOPTRAP_HOST_USER=$user") fi local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME") + if [[ $ALLOW_HOST_NETWORK == false ]]; then + SLOPTRAP_HOST_ALIAS="sloptrap.host" + network_opts+=(--add-host "$SLOPTRAP_HOST_ALIAS:host-gateway") + env_args+=(-e "SLOPTRAP_HOST_ALIAS=$SLOPTRAP_HOST_ALIAS") + if [[ "$BACKEND" == "opencode" ]]; then + OPENCODE_SERVER=$(normalize_local_server_url "$OPENCODE_SERVER" "$SLOPTRAP_HOST_ALIAS") + fi + else + SLOPTRAP_HOST_ALIAS="" + fi local -a user_opts=("--user" "$uid:$gid") if [[ $CONTAINER_ENGINE == "podman" ]]; then user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}") @@ -1560,55 +1798,57 @@ prepare_container_runtime() { } build_image() { - if [[ "$BACKEND" == "opencode" ]]; then - ensure_opencode_binary - else - ensure_codex_binary - fi - if [[ $SKIP_BUILD_BANNER != true ]]; then - print_banner - fi - print_manifest_summary - local extra_packages_arg - extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") - if ! $DRY_RUN; then - status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME" - fi - local -a cmd=( - "$CONTAINER_ENGINE" build --quiet - -t "$SLOPTRAP_IMAGE_NAME" - -f "$SLOPTRAP_DOCKERFILE_PATH" - --network "$SLOPTRAP_NETWORK_NAME" - --label "$SLOPTRAP_IMAGE_LABEL" - --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" - --build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME" - --build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID" - --build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID" - ) - if [[ -n $extra_packages_arg ]]; then - cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg") - fi - cmd+=("$SLOPTRAP_BUILD_CONTEXT") - if $DRY_RUN; then - run_or_print "${cmd[@]}" - return - fi + prepare_build_context + if [[ "$BACKEND" == "opencode" ]]; then + ensure_opencode_binary + else + ensure_codex_binary + fi + if [[ $SKIP_BUILD_BANNER != true ]]; then + print_banner + fi + print_manifest_summary + local extra_packages_arg + extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED") + if ! $DRY_RUN; then + status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME" + fi + local -a cmd=( + "$CONTAINER_ENGINE" build --quiet + -t "$SLOPTRAP_IMAGE_NAME" + -f "$SLOPTRAP_DOCKERFILE_PATH" + --network "$SLOPTRAP_NETWORK_NAME" + --label "$SLOPTRAP_IMAGE_LABEL" + --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" + --build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME" + --build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID" + --build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID" + ) + if [[ -n $extra_packages_arg ]]; then + cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg") + fi + cmd+=("$SLOPTRAP_BUILD_CONTEXT") + if $DRY_RUN; then + run_or_print "${cmd[@]}" + return + fi - local build_output - if ! build_output=$("${cmd[@]}"); then - return 1 - fi - build_output=$(trim "$build_output") - if [[ -n $build_output ]]; then - comment_line "Image %s\n" "$build_output" - fi + local build_output + if ! build_output=$("${cmd[@]}"); then + return 1 + fi + build_output=$(trim "$build_output") + if [[ -n $build_output ]]; then + comment_line "Image %s\n" "$build_output" + fi } rebuild_image() { - if [[ "$BACKEND" == "opencode" ]]; then - ensure_opencode_binary - else - ensure_codex_binary + prepare_build_context + if [[ "$BACKEND" == "opencode" ]]; then + ensure_opencode_binary + else + ensure_codex_binary fi if [[ $SKIP_BUILD_BANNER != true ]]; then print_banner @@ -1720,15 +1960,14 @@ run_codex_command() { local -a auth_mount=() if [[ "$BACKEND" == "opencode" ]]; then ensure_opencode_storage_paths + ensure_opencode_config else ensure_codex_storage_paths fi append_auth_mount_arg false auth_mount - local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}" "$BACKEND") + local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}") if [[ "$BACKEND" == "opencode" ]]; then - cmd+=("--server" "$OPENCODE_SERVER") - cmd+=("--model" "$OPENCODE_MODEL") - cmd+=("--sandbox" "workspace-write") + true else if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then cmd+=("${CODEX_ARGS_ARRAY[@]}") @@ -1744,9 +1983,13 @@ run_codex() { if ! $DRY_RUN; then status_line "Running %s\n" "$SLOPTRAP_IMAGE_NAME" fi - local runtime_prompt - runtime_prompt=$(build_runtime_context_prompt) - run_codex_command "$runtime_prompt" + if [[ "$BACKEND" == "opencode" ]]; then + run_codex_command + else + local runtime_prompt + runtime_prompt=$(build_runtime_context_prompt) + run_codex_command "$runtime_prompt" + fi } run_login_target() { @@ -1757,19 +2000,23 @@ run_login_target() { status_line "Login %s\n" "$SLOPTRAP_IMAGE_NAME" fi append_auth_mount_arg true auth_mount - local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}" "codex" login) + local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}" login) run_runtime_container_cmd "${cmd[@]}" } run_shell_target() { - ensure_codex_storage_paths + if [[ "$BACKEND" == "opencode" ]]; then + ensure_opencode_storage_paths + else + ensure_codex_storage_paths + fi local -a source_args=("$SLOPTRAP_IMAGE_NAME") local -a auth_mount=() if ! $DRY_RUN; then status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME" fi append_auth_mount_arg false auth_mount - local -a cmd=("${BASE_CONTAINER_CMD[@]}" "${auth_mount[@]}" "${source_args[@]}" /bin/bash) + local -a cmd=("${BASE_CONTAINER_CMD[@]}" --entrypoint /bin/bash "${auth_mount[@]}" "${source_args[@]}") run_runtime_container_cmd "${cmd[@]}" } @@ -1778,7 +2025,11 @@ run_resume_target() { if ! $DRY_RUN; then status_line "Resume %s (%s)\n" "$SLOPTRAP_IMAGE_NAME" "$session_id" fi - run_codex_command resume "$session_id" + if [[ "$BACKEND" == "opencode" ]]; then + run_codex_command --session "$session_id" + else + run_codex_command resume "$session_id" + fi } process_resume_target() { @@ -1930,10 +2181,6 @@ ensure_ignore_helper_root IGNORE_STUB_BASE="$IGNORE_HELPER_ROOT/session-${BASHPID:-$$}" resolve_sloptrap_ignore "$CODE_DIR" resolve_container_workdir -NEED_LOGIN=false -if [[ ! -s "$CODEX_AUTH_FILE_HOST" ]]; then - NEED_LOGIN=true -fi TARGETS=("${TARGETS_INPUT[@]}") if [[ ${#TARGETS[@]} -eq 0 ]]; then @@ -1942,7 +2189,7 @@ fi DEFAULT_TARGETS=("${TARGETS[@]}") -PACKAGES_EXTRA=${MANIFEST[packages_extra]-} +PACKAGES_EXTRA=$(normalize_package_list "${MANIFEST[packages_extra]:-}") if [[ -n ${MANIFEST[allow_host_network]-} ]]; then case "${MANIFEST[allow_host_network],,}" in 1|true|yes) @@ -1982,22 +2229,29 @@ select_backend() { opencode) BACKEND="opencode" ;; codex|*) BACKEND="codex" ;; esac - - if [[ "$BACKEND" == "opencode" && ! -x "$(command -v opencode)" ]]; then - error "opencode CLI not found; install from https://opencode.ai" - fi } select_backend if [[ "$BACKEND" == "opencode" ]]; then - OPENCODE_SERVER="${MANIFEST[opencode_server]:-http://localhost:11434}" - OPENCODE_MODEL="${MANIFEST[opencode_model]:-llama3}" + OPENCODE_SERVER="${MANIFEST[opencode_server]:-http://localhost:8080}" + OPENCODE_MODEL="${MANIFEST[opencode_model]:-bartowski/Qwen_Qwen3.5-9B-GGUF:Q8_0}" + OPENCODE_CONTEXT="${MANIFEST[opencode_context]:-256K}" OPENCODE_STATE_HOME_HOST="$CODEX_STATE_HOME_HOST/opencode" + OPENCODE_CONFIG_HOST="$CODEX_STATE_HOME_HOST/config/opencode/opencode.json" + OPENCODE_CONFIG_CONT="" else OPENCODE_SERVER="" OPENCODE_MODEL="" + OPENCODE_CONTEXT="" OPENCODE_STATE_HOME_HOST="" + OPENCODE_CONFIG_HOST="" + OPENCODE_CONFIG_CONT="" +fi + +NEED_LOGIN=false +if [[ "$BACKEND" != "opencode" && ! -s "$CODEX_AUTH_FILE_HOST" ]]; then + NEED_LOGIN=true fi CONTAINER_ENGINE="$(detect_container_engine)" diff --git a/tests/run_tests.sh b/tests/run_tests.sh index bf9f3f5..b7eb638 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -189,6 +189,10 @@ EOF cat >"$STUB_BIN/jq" <<'EOF' #!/usr/bin/env bash set -euo pipefail +if [[ ${1-} == "-n" ]]; then + shift + exec /usr/bin/jq -n "$@" +fi while [[ $# -gt 0 ]]; do case "$1" in -r) @@ -454,12 +458,28 @@ run_resume_omits_runtime_context() { if grep -q -- "You are running inside sloptrap" "$STUB_LOG"; then record_failure "resume_omits_runtime_context: resume should not receive startup prompt" fi - if ! grep -q -- "codex --sandbox danger-full-access --ask-for-approval never resume $session_id" "$STUB_LOG"; then + 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" fi 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 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 + teardown_stub_env +} + run_auth_file_mount() { local scenario_dir scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P) @@ -732,6 +752,125 @@ run_wizard_build_trigger() { 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= +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 + teardown_stub_env +} + +run_opencode_localhost_rewrite() { + local scenario_dir="$TEST_ROOT/opencode_localhost" + printf '==> opencode_localhost_rewrite\n' + setup_stub_env + 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 2>&1; then + 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 -- '"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 + teardown_stub_env +} + +run_opencode_print_config_runtime_flags() { + local scenario_dir="$TEST_ROOT/opencode_print_config" + printf '==> opencode_print_config_runtime_flags\n' + setup_stub_env + mkdir -p "$scenario_dir" + cat > "$scenario_dir/.sloptrap" <<'EOF' +name=opencode-print-config +packages_extra= +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 + teardown_stub_env +} + run_symlink_escape run_manifest_injection run_helper_symlink @@ -740,6 +879,7 @@ run_resume_target run_runtime_context_prompt run_sh_reexec run_resume_omits_runtime_context +run_shell_target_uses_entrypoint run_auth_file_mount run_codex_home_override run_project_state_isolation @@ -749,6 +889,9 @@ run_root_directory_project run_wizard_create_manifest run_wizard_existing_defaults run_wizard_build_trigger +run_opencode_build_downloads_release_cli +run_opencode_localhost_rewrite +run_opencode_print_config_runtime_flags if [[ ${#failures[@]} -gt 0 ]]; then printf '\nTest failures:\n' diff --git a/tests/wizard_build/.sloptrap b/tests/wizard_build/.sloptrap index 55fd1e6..c8c97b1 100644 --- a/tests/wizard_build/.sloptrap +++ b/tests/wizard_build/.sloptrap @@ -1,4 +1,4 @@ name=wizard_build packages_extra= -capabilities= +agent=codex allow_host_network=false diff --git a/tests/wizard_empty/.sloptrap b/tests/wizard_empty/.sloptrap index 47c7920..d94f5a0 100644 --- a/tests/wizard_empty/.sloptrap +++ b/tests/wizard_empty/.sloptrap @@ -1,4 +1,4 @@ name=wizard_empty packages_extra= -capabilities= +agent=codex allow_host_network=false diff --git a/tests/wizard_existing/.sloptrap b/tests/wizard_existing/.sloptrap index 15575dc..28a504b 100644 --- a/tests/wizard_existing/.sloptrap +++ b/tests/wizard_existing/.sloptrap @@ -1,4 +1,4 @@ name=custom-wizard packages_extra=make git -capabilities=apt-install +agent=codex allow_host_network=true