diff --git a/cli/README.md b/cli/README.md index 8d2b8940..f40f5e4d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -23,7 +23,7 @@ Options: - `--proxy` - Proxy UI mode: `web` (default, mitmweb browser UI) or `tui` (mitmproxy console, use with `sandcat proxy` to attach) - `--secret-provider` / `--sp` - Secret backend: `none` (default), `1password`, `protonpass` (skips prompt when set) - `--1password` - Deprecated alias for `--secret-provider 1password` -- `--features` - Comma-separated optional non-provider features: `tui` (proxy console mode; prefer `--proxy tui`), `no-gitignore` (skip appending the `# Sandcat` block to the project's `.gitignore`; equivalent to `SANDCAT_GITIGNORE=false`), `no-rtk` (skip RTK installation; equivalent to `SANDCAT_RTK=false`), `strict-network` (project settings get network presets for the selected stacks instead of the allow-all-GET wildcard; equivalent to `SANDCAT_STRICT_NETWORK=true`) +- `--features` - Comma-separated optional non-provider features: `tui` (proxy console mode; prefer `--proxy tui`), `no-gitignore` (skip appending the `# Sandcat` block to the project's `.gitignore`; equivalent to `SANDCAT_GITIGNORE=false`), `strict-network` (project settings get network presets for the selected stacks instead of the allow-all-GET wildcard; equivalent to `SANDCAT_STRICT_NETWORK=true`) - `--name` - Project name for Docker Compose (default: derived from directory name) - `--path` - Project directory (default: current directory) diff --git a/cli/lib/agents.bash b/cli/lib/agents.bash index 342ce4f4..7171ee00 100644 --- a/cli/lib/agents.bash +++ b/cli/lib/agents.bash @@ -1,8 +1,5 @@ #!/usr/bin/env bash -# shellcheck source=./rtk.bash -source "${BASH_SOURCE[0]%/*}/rtk.bash" - # Returns supported agents as a space-separated list. sct_available_agents() { echo "claude cursor codex copilot" @@ -371,7 +368,6 @@ EOF return 0 ;; esac - sct_rtk_docker_install_block } # Returns Dockerfile config-home preparation block for selected agent. @@ -397,11 +393,8 @@ EOF ;; codex) cat <<'EOF' -# Pre-create ~/.codex and ~/.codex-host so Docker bind-mounts don't -# create them as root-owned. The host AGENTS.md is bind-mounted into -# ~/.codex-host/ (not directly into ~/.codex/) so the user-init copy -# step can seed a writable ~/.codex/AGENTS.md that rtk can patch. -RUN mkdir -p /home/vscode/.codex /home/vscode/.codex-host +# Pre-create ~/.codex so Docker bind-mounts don't create it as root-owned. +RUN mkdir -p /home/vscode/.codex RUN echo 'alias codex-yolo="codex --yolo"' >> /home/vscode/.bashrc EOF ;; @@ -509,34 +502,6 @@ if command -v codex >/dev/null 2>&1; then codex --version >/dev/null 2>&1 \ || echo "sandcat: codex --version failed (non-fatal)" >&2 fi - -# Seed the writable ~/.codex/AGENTS.md from the host bind-mount so -# `rtk init --codex` can patch it. Sandcat mounts the host's -# ~/.codex/AGENTS.md read-only at ~/.codex-host/AGENTS.md (a separate -# path) precisely so this copy can happen without hitting EROFS. -# Copy is one-shot: only when ~/.codex/AGENTS.md does not exist yet -# (agent-home volume was fresh). Later host edits to AGENTS.md take -# effect after a `docker compose down -v` (or manual rm inside). -if [ -f "$HOME/.codex-host/AGENTS.md" ] && [ ! -e "$HOME/.codex/AGENTS.md" ]; then - mkdir -p "$HOME/.codex" - cp "$HOME/.codex-host/AGENTS.md" "$HOME/.codex/AGENTS.md" -elif [ ! -e "$HOME/.codex/AGENTS.md" ]; then - # No host AGENTS.md either — create empty one so rtk init has - # something to patch. - mkdir -p "$HOME/.codex" - : > "$HOME/.codex/AGENTS.md" -fi - -# Auto-init rtk for codex: writes ~/.codex/RTK.md and appends an -# @RTK.md reference to ~/.codex/AGENTS.md. Idempotent — skipped once -# the reference is already present. stdin is closed so rtk's first-run -# prompts (telemetry consent) can't block container start. -if command -v rtk >/dev/null 2>&1 \ - && [ "${SANDCAT_RTK:-true}" != "false" ] \ - && ! grep -q '@RTK.md\|RTK\.md' "$HOME/.codex/AGENTS.md" 2>/dev/null; then - rtk init -g --codex /dev/null 2>&1 \ - || echo "sandcat: rtk init failed (non-fatal)" >&2 -fi EOF ;; copilot) @@ -555,5 +520,4 @@ EOF return 0 ;; esac - sct_rtk_user_init_block "$agent" } diff --git a/cli/lib/composefile.bash b/cli/lib/composefile.bash index c510ea6f..4462ea17 100644 --- a/cli/lib/composefile.bash +++ b/cli/lib/composefile.bash @@ -274,7 +274,7 @@ add_codex_config_volumes() { local active=${2:-true} # shellcheck disable=SC2016 - add_volume_entry "$compose_file" '${HOME}/.codex/AGENTS.md:/home/vscode/.codex-host/AGENTS.md:ro' "$active" 'Host Codex config (optional) — copied into writable ~/.codex/AGENTS.md by app-user-init.sh so rtk can patch it' + add_volume_entry "$compose_file" '${HOME}/.codex/AGENTS.md:/home/vscode/.codex/AGENTS.md:ro' "$active" 'Host Codex config (optional)' # shellcheck disable=SC2016 add_volume_entry "$compose_file" '${HOME}/.codex/skills:/home/vscode/.codex/skills:ro' "$active" # shellcheck disable=SC2016 diff --git a/cli/lib/rtk.bash b/cli/lib/rtk.bash deleted file mode 100644 index f4c8cbd3..00000000 --- a/cli/lib/rtk.bash +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# rtk (Rust Token Killer, https://github.com/rtk-ai/rtk) install + init -# helpers. Emission is gated on SANDCAT_RTK (default: true). Consumed by -# the per-agent dispatchers in cli/lib/agents.bash so the rtk install RUN -# lands in Dockerfile.app and rtk init runs at container start. - -# Returns 0 (enabled) when SANDCAT_RTK is unset or anything other than -# the literal string "false". Default-on posture: absent env var means -# the user has not explicitly opted out. -sct_rtk_enabled() { - [[ "${SANDCAT_RTK:-true}" != "false" ]] -} - -# Emits the Dockerfile.app RUN block that installs the rtk binary -# globally via its official install script. Agent-agnostic — the same -# binary serves every supported agent; per-agent hook wiring happens -# at container start via sct_rtk_user_init_block. -# -# The install script URL is pinned to a commit SHA to freeze the fetched -# shell code and close the arbitrary-code-via-master vector. The binary -# itself is pinned separately via RTK_VERSION, which install.sh verifies -# with a SHA-256 check against the release's checksums.txt before -# installing — so both the script and the binary it fetches are pinned. -# When bumping rtk, update the commit SHA and RTK_VERSION together. -# -# Emits an empty output when the feature is disabled so the caller can -# unconditionally append it to Dockerfile fragments. -sct_rtk_docker_install_block() { - sct_rtk_enabled || return 0 - cat <<'EOF' -# Install rtk (Rust Token Killer) — compresses shell command output so AI -# agents consume fewer tokens per command. Installed system-wide so the -# agent-home volume can't mask the binary on upgrade. Disable at init -# time with `sandcat init --features no-rtk` or `SANDCAT_RTK=false`. -USER root -RUN curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/b34be37caf3796b69a50952a28e60e32b5daad43/install.sh | \ - RTK_INSTALL_DIR=/usr/local/bin RTK_VERSION=v0.45.0 sh -USER vscode -EOF -} - -# Emits the app-user-init.sh fragment that runs `rtk init` for the given -# agent, guarded to a one-time execution. Every invocation gets stdin -# from /dev/null: rtk init asks interactive questions on first run -# (telemetry consent since 0.45) and would otherwise block forever on -# the container's TTY while its output is discarded. -# Currently wires `claude` and `cursor`; unknown/future agents -# (including codex — rtk 0.44 does not support `--agent codex`, and -# `--codex` cannot combine with `--hook-only` or `--auto-patch`) get a -# safe no-op so the binary is still available on PATH but rtk stays -# uninitialized until the case is added. -# -# Emits an empty output when the feature is disabled OR when the agent -# has no rtk profile. -# -# Args: -# $1 - Agent name -sct_rtk_user_init_block() { - local agent=$1 - sct_rtk_enabled || return 0 - case "$agent" in - claude) - cat <<'EOF' -# rtk (Rust Token Killer) — one-time hook config for Claude Code. -# `rtk init -g --hook-only --auto-patch` patches ~/.claude/settings.json -# with the PreToolUse hook non-interactively; --hook-only skips writing -# RTK.md (sandcat mounts ~/.claude/CLAUDE.md read-only, so rtk's default -# CLAUDE.md injection would EROFS). -# Idempotency: skipped once the hook string is already present. -if command -v rtk >/dev/null 2>&1 && ! grep -q '"command": "rtk hook' "$HOME/.claude/settings.json" 2>/dev/null; then - rtk init -g --hook-only --auto-patch /dev/null 2>&1 \ - || echo "sandcat: rtk init failed (non-fatal)" >&2 -fi -EOF - ;; - cursor) - cat <<'EOF' -# rtk (Rust Token Killer) — one-time hook config for Cursor CLI. -# The hook lives in ~/.cursor/hooks.json, which sandcat bind-mounts -# read-only from the host (SANDCAT_MOUNT_CURSOR_CONFIG=true default). -# Expected workflow: user runs `rtk init -g --hook-only --auto-patch \ -# --agent cursor` on the HOST once — then this guard's grep passes and -# the init below stays a no-op. If the guard doesn't pass here, we -# still try inside the container so the warning nudges the user to -# initialize rtk on the host. -if command -v rtk >/dev/null 2>&1 && ! grep -q '"rtk hook cursor' "$HOME/.cursor/hooks.json" 2>/dev/null; then - rtk init -g --hook-only --auto-patch --agent cursor /dev/null 2>&1 \ - || echo "sandcat: rtk hook not found for cursor. Install rtk on your host and run once: rtk init -g --hook-only --auto-patch --agent cursor (see README RTK section)" >&2 -fi -EOF - ;; - *) - # No rtk profile wired for this agent — safe no-op. Binary is - # still installed by sct_rtk_docker_install_block; user can - # invoke `rtk init` manually. - return 0 - ;; - esac -} diff --git a/cli/libexec/init/init b/cli/libexec/init/init index e3e21a99..541277de 100755 --- a/cli/libexec/init/init +++ b/cli/libexec/init/init @@ -307,19 +307,16 @@ init() { # tui → mitmproxy console instead of web UI # no-shared-cache → disable shared JVM dependency cache volumes # no-gitignore → skip appending the Sandcat block to .gitignore - # no-rtk → skip installing the rtk shell hook # strict-network → project settings get stack network presets instead # of the allow-all-GET wildcard (default deny beyond # the presets and the user-settings layer) local gitignore_enabled=${SANDCAT_GITIGNORE:-true} - local rtk_enabled=${SANDCAT_RTK:-true} local strict_network=${SANDCAT_STRICT_NETWORK:-false} if [[ "$features_provided" != "true" ]]; then local available_features=( "tui (mitmproxy console instead of web UI)" "no-shared-cache (per-project dep cache instead of shared)" "no-gitignore (do not append Sandcat block to .gitignore)" - "no-rtk (do not install rtk shell hook)" "strict-network (stack presets instead of allow-all-GET wildcard)" ) local selected_features @@ -332,7 +329,6 @@ init() { tui) proxy_mode="tui" ;; no-shared-cache) export SANDCAT_MOUNT_SHARED_CACHE="false" ;; no-gitignore) gitignore_enabled=false ;; - no-rtk) rtk_enabled=false ;; strict-network) strict_network=true ;; esac done @@ -345,13 +341,12 @@ init() { tui) proxy_mode="tui" ;; no-shared-cache) export SANDCAT_MOUNT_SHARED_CACHE="false" ;; no-gitignore) gitignore_enabled=false ;; - no-rtk) rtk_enabled=false ;; strict-network) strict_network=true ;; 1password) echo "Use --secret-provider 1password instead of --features 1password" | error return 1 ;; - *) echo "Unknown feature: $f (expected: tui, no-shared-cache, no-gitignore, no-rtk, strict-network)" | error; return 1 ;; + *) echo "Unknown feature: $f (expected: tui, no-shared-cache, no-gitignore, strict-network)" | error; return 1 ;; esac done fi @@ -416,7 +411,6 @@ init() { --proxy "$proxy_mode" --secret-provider "$secret_provider" ) - export SANDCAT_RTK="$rtk_enabled" devcontainer "${devcontainer_args[@]}" local gitignore_status="skipped" @@ -460,11 +454,6 @@ init() { echo " User settings: ~/.config/sandcat/settings.json (git identity, API keys)" | info echo " Devcontainer: .devcontainer/" | info echo " Gitignore: $gitignore_status" | info - if [[ "$rtk_enabled" == "true" ]]; then - echo " RTK: installed (disable with --features no-rtk)" | info - else - echo " RTK: disabled" | info - fi if [[ "$strict_network" == "true" ]]; then local preset_summary="${stacks_resolved:-none}" echo " Network: strict — stack presets: ${preset_summary// /, } (edit .sandcat/settings.json to allow more)" | info diff --git a/cli/test/agents/agents.bats b/cli/test/agents/agents.bats index b3f84c3b..f30c8e11 100755 --- a/cli/test/agents/agents.bats +++ b/cli/test/agents/agents.bats @@ -338,7 +338,6 @@ setup() { } @test "sct_agent_docker_install_block: codex installs codex to /usr/local/bin" { - unset SANDCAT_RTK run sct_agent_docker_install_block codex assert_output --partial "chatgpt.com/codex/install.sh" assert_output --partial "CODEX_INSTALL_DIR=/usr/local/bin" @@ -351,49 +350,11 @@ setup() { refute_output --partial "ENV CODEX_INSTALL_DIR" } -@test "sct_agent_docker_install_block: codex append rtk install when enabled" { - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK - run sct_agent_docker_install_block codex - assert_output --partial "chatgpt.com/codex/install.sh" - assert_output --partial "raw.githubusercontent.com/rtk-ai/rtk" -} - -@test "sct_agent_docker_install_block: codex skips rtk when SANDCAT_RTK=false" { - source "$SCT_LIBDIR/rtk.bash" - SANDCAT_RTK=false run sct_agent_docker_install_block codex - assert_output --partial "chatgpt.com/codex/install.sh" - refute_output --partial "raw.githubusercontent.com/rtk-ai/rtk" -} - @test "sct_agent_docker_install_block: unknown returns empty" { run sct_agent_docker_install_block unknown assert_output "" } -@test "sct_agent_docker_install_block: claude append rtk install when enabled" { - # shellcheck source=../../lib/rtk.bash - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK - run sct_agent_docker_install_block claude - assert_output --partial "claude.ai/install.sh" - assert_output --partial "raw.githubusercontent.com/rtk-ai/rtk" -} - -@test "sct_agent_docker_install_block: claude skips rtk when SANDCAT_RTK=false" { - source "$SCT_LIBDIR/rtk.bash" - SANDCAT_RTK=false run sct_agent_docker_install_block claude - assert_output --partial "claude.ai/install.sh" - refute_output --partial "raw.githubusercontent.com/rtk-ai/rtk" -} - -@test "sct_agent_docker_install_block: unknown returns empty even with rtk enabled" { - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK - run sct_agent_docker_install_block unknown - assert_output "" -} - # --------------------------------------------------- Dockerfile home prep @test "sct_agent_docker_home_prep_block: claude pre-creates ~/.claude" { @@ -406,10 +367,9 @@ setup() { assert_output --partial "/home/vscode/.cursor" } -@test "sct_agent_docker_home_prep_block: codex pre-creates ~/.codex, ~/.codex-host and codex-yolo alias" { +@test "sct_agent_docker_home_prep_block: codex pre-creates ~/.codex and codex-yolo alias" { run sct_agent_docker_home_prep_block codex assert_output --partial "/home/vscode/.codex" - assert_output --partial "/home/vscode/.codex-host" assert_output --partial 'alias codex-yolo="codex --yolo"' } @@ -431,58 +391,14 @@ setup() { assert_output --partial "Sandcat cursor.cli" } -@test "sct_agent_user_init_block: unknown returns empty" { - run sct_agent_user_init_block unknown - assert_output "" -} - -@test "sct_agent_user_init_block: claude appends rtk init when enabled" { - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK - run sct_agent_user_init_block claude - assert_output --partial "hasCompletedOnboarding" - assert_output --partial "rtk init -g" -} - -@test "sct_agent_user_init_block: claude skips rtk when SANDCAT_RTK=false" { - source "$SCT_LIBDIR/rtk.bash" - SANDCAT_RTK=false run sct_agent_user_init_block claude - assert_output --partial "hasCompletedOnboarding" - refute_output --partial "rtk init" -} - -@test "sct_agent_user_init_block: cursor appends rtk init when enabled" { - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK - run sct_agent_user_init_block cursor - assert_output --partial "cursor-cli-config.json" - assert_output --partial "rtk init -g --hook-only --auto-patch --agent cursor" -} - -@test "sct_agent_user_init_block: cursor skips rtk when SANDCAT_RTK=false" { - source "$SCT_LIBDIR/rtk.bash" - SANDCAT_RTK=false run sct_agent_user_init_block cursor - assert_output --partial "cursor-cli-config.json" - refute_output --partial "rtk init" -} - -@test "sct_agent_user_init_block: codex emits rtk init --codex + host AGENTS.md seed" { - source "$SCT_LIBDIR/rtk.bash" - unset SANDCAT_RTK +@test "sct_agent_user_init_block: codex runs health check" { run sct_agent_user_init_block codex assert_output --partial "codex --version" - assert_output --partial "rtk init -g --codex