diff --git a/docs/agents/claude.md b/docs/agents/claude.md new file mode 100644 index 00000000..bfa0aa13 --- /dev/null +++ b/docs/agents/claude.md @@ -0,0 +1,98 @@ +# Claude Code + +Claude Code is sandcat's default agent (`--agent claude`, or simply omit the +flag). The sections below cover authentication, the host paths sandcat mounts +for it, and its RTK hook; everything generic — network policy, secret +mechanics, stacks — works the same for every agent. + +## Authentication + +Claude Code supports two authentication methods inside the container: + +- **API key** — add an `ANTHROPIC_API_KEY` secret to `settings.json`. The + entrypoint detects the key and seeds `~/.claude.json` with + `{"hasCompletedOnboarding": true}` so Claude Code uses it without interactive + setup. +- **Subscription (browser login)** — omit `ANTHROPIC_API_KEY` from + `settings.json`. On first run Claude Code will display a URL and a code. Open + the URL in a browser on your host machine, enter the code, and authenticate + there — the container itself cannot open a browser. + +**Autonomous mode.** The bundled `devcontainer.json` enables +`claudeCode.allowDangerouslySkipPermissions` and sets +`claudeCode.initialPermissionMode` to `bypassPermissions`. This lets Claude Code +run without interactive permission prompts inside the container. The trade-off: +sandcat already provides the security boundary (network isolation, secret +substitution, iptables kill-switch), so the in-container prompts add friction +without meaningful security benefit. Remove these settings if you prefer +interactive approval. See [Secure & Dangerous Claude Code + VS Code +Setup](https://warski.org/blog/secure-dangerous-claude-code-vs-code-setup/) for +background on this approach. + +**Host customizations.** The example `compose-all.yml` bind-mounts +`~/.claude/CLAUDE.md`, `~/.claude/agents`, and `~/.claude/commands` from the +host (read-only) so your personal instructions, custom agents, and slash +commands are available inside the container. Remove any mount whose source does +not exist on your host — Docker will otherwise create an empty directory in its +place. + +**Multi-line prompts.** Composing a multi-line prompt with `⌘+Enter` does not +work on macOS — the terminal reserves the `⌘` modifier and never transmits it +over the PTY, so `sandcat attach` (and Claude Code) only ever receive a plain +`Enter`. This is not sandcat-specific and cannot be fixed inside the container. +Use one of these instead: + +- **`\` then `Enter`** — inserts a newline in any terminal with no setup. The + simplest option. +- **`Option+Enter`** — Claude Code's macOS default. In Apple Terminal, first + enable *Settings → Profiles → Keyboard → Use Option as Meta key*; iTerm2 sends + it out of the box. +- **`Shift+Enter`** — the most familiar combination, but Claude Code only + receives whatever bytes the terminal chooses to send for it, so it needs a + one-time mapping in the **host** terminal. Claude Code's `/terminal-setup` is + meant to install this, but it has two traps in this setup: it configures the + host terminal, so running it from Claude Code *inside* the sandbox does + nothing; and it caches an "installed" flag, so a second run reports *"already + enabled"* even when the terminal was never actually changed. The reliable route + is to map the key by hand: + - **iTerm2** — Settings → Keys → Key Bindings → `+`, record `Shift+Enter`, + choose *Send Hex Codes* and enter `0x1b 0x0d` (this is `Option+Enter`, which + Claude Code treats as a newline). GUI bindings take effect immediately. To + confirm it worked, run `cat -v` in the sandbox shell and press `Shift+Enter`: + it should print `^[` instead of a blank line. + - **VS Code integrated terminal** — add to `keybindings.json`: + + ```json + { "key": "shift+enter", + "command": "workbench.action.terminal.sendSequence", + "args": { "text": "\u001b\r" }, + "when": "terminalFocus" } + ``` + +## Host paths and mounts + +**Claude paths** (host `~/.claude/`, read-only when mounted): + +- `CLAUDE.md`, `agents/`, `commands/` + +Project-local configuration (`.claude/` in the repo) and the isolation +semantics of these mounts are described in +[Customizing optional volume mounts](../configuration/volume-mounts.md). + +## RTK hook + +Works out of the box, zero configuration. `sandcat init` generates an +`app-user-init.sh` block that runs `rtk init -g --hook-only --auto-patch` +on the first container start; the hook lands in the sandbox's +`~/.claude/settings.json` (inside the `agent-home` volume, not +bind-mounted). Subsequent starts are idempotent no-ops. + +See [RTK — LLM token compression](rtk.md) for what RTK does and how to opt +out. + +## Convenience alias + +Every claude sandbox ships a `claude-yolo` alias (= `claude +--dangerously-skip-permissions`): the sandcat network isolation is the +security boundary, so bypassing in-container permission prompts is the +intended workflow. diff --git a/docs/agents/codex.md b/docs/agents/codex.md new file mode 100644 index 00000000..136b59c4 --- /dev/null +++ b/docs/agents/codex.md @@ -0,0 +1,47 @@ +# Codex CLI + +Sandcat installs [OpenAI's Codex CLI](https://github.com/openai/codex) +into every codex-agent sandbox and wires `OPENAI_API_KEY` through the +mitmproxy secret substitution layer. Codex reads its config from +`~/.codex/config.toml` (per-sandbox, agent-home volume) and picks up +the API key directly from the environment — no `codex login` required. + +**Setup:** + +```bash +sandcat init --agent codex --ide vscode +# Edit ~/.config/sandcat/settings.json — set secrets.OPENAI_API_KEY.value +sandcat run +codex "explain this codebase" +``` + +**Bash alias:** `codex-yolo` (= `codex --yolo`) is available in every +codex sandbox for parity with `claude-yolo`. + +**Host config sharing** (optional, default on): `~/.codex/AGENTS.md`, +`~/.codex/skills/`, and `~/.codex/commands/` are bind-mounted read-only +from the host into the container, matching how `~/.claude/` is handled. +The rest of `~/.codex/` (config.toml, credentials, history) lives in +the container's agent-home volume — per-sandbox persistent, per-sandbox +isolated. Opt out with `SANDCAT_MOUNT_CODEX_CONFIG=false`. + +**RTK integration:** works out of the box. On first container start, +sandcat seeds `~/.codex/AGENTS.md` (from the host bind-mount if +present) and runs `rtk init -g --codex` to write `~/.codex/RTK.md` +and add an `@RTK.md` reference to `AGENTS.md`. Idempotent: skipped +once the reference is already there. Disable with `--features +no-rtk` or `SANDCAT_RTK=false`. + +Note: because rtk needs to patch a writable `AGENTS.md`, sandcat +mounts the host's `~/.codex/AGENTS.md` at `~/.codex-host/AGENTS.md` +(a helper path) — the user-init step copies it into the writable +`~/.codex/AGENTS.md`. Host edits to `AGENTS.md` take effect after a +`docker compose down -v` (or manual rm inside). Skills and commands +directories are bind-mounted normally at `~/.codex/skills` and +`~/.codex/commands`, so those live-reload as usual. + +**Auth model:** first iteration supports `OPENAI_API_KEY` only. +ChatGPT sign-in (`chatgpt.com` / `auth.openai.com`) is not in the +default allowlist — users who want that flow can add the hosts to +`.sandcat/settings.local.json` and run `codex login` manually inside +the container. diff --git a/docs/agents/copilot.md b/docs/agents/copilot.md new file mode 100644 index 00000000..f17a8b84 --- /dev/null +++ b/docs/agents/copilot.md @@ -0,0 +1,61 @@ +# GitHub Copilot CLI + +GitHub's [Copilot CLI](https://docs.github.com/copilot/how-tos/copilot-cli) +(`@github/copilot`) is available as a first-class sandcat agent. Sandcat installs +Node.js 22 and the Copilot package into every copilot-agent sandbox and wires +`COPILOT_GITHUB_TOKEN` through the mitmproxy secret substitution layer. + +**Setup:** + +```bash +sandcat init --agent copilot --ide vscode +# Edit ~/.config/sandcat/settings.json — set secrets.COPILOT_GITHUB_TOKEN.value +sandcat run +copilot "explain this codebase" +``` + +**Authentication:** Copilot CLI requires a GitHub token. Choose one of: + +1. **Fine-grained Personal Access Token (recommended):** Create a PAT at + [`https://github.com/settings/personal-access-tokens`](https://github.com/settings/personal-access-tokens) + with the **"Copilot Requests"** permission (Read and write). Then add it to + `~/.config/sandcat/settings.json`: + ```json + { + "secrets": { + "COPILOT_GITHUB_TOKEN": { + "value": "github_pat_...", + "hosts": ["api.github.com", "*.github.com", "*.githubcopilot.com", "*.githubusercontent.com"] + } + } + } + ``` + +2. **GitHub CLI OAuth token (quick setup):** If you already have `gh` CLI logged in, + run this once to write the token directly into `settings.json`: + ```bash + export TKN=$(gh auth token) + yq -i -o json '.secrets.COPILOT_GITHUB_TOKEN.value = strenv(TKN)' \ + ~/.config/sandcat/settings.json + ``` + +**Note:** Adding Node.js 22 and Copilot to the base image increases its size by +approximately 120 MB. The image is built once and cached locally; rebuilds are +fast. + +**VS Code integration:** When the IDE is `vscode`, the bundled `devcontainer.json` +includes the `GitHub.copilot` extension. Note that the VS Code extension +authenticates through VS Code's own GitHub sign-in (not the `COPILOT_GITHUB_TOKEN` +env var used by the CLI), so you may need to sign in the first time you open the +extension. + +**Placeholder:** Sandcat automatically sets the placeholder to +`gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN`. The container sees only the +placeholder; the real token is injected by mitmproxy only for allowed Copilot +hosts. No manual configuration is needed. + +**Bash alias:** `copilot-yolo` (= `copilot --yolo`) is available in every +copilot sandbox for parity with `claude-yolo` and `codex-yolo`. `--yolo` is +equivalent to `--allow-all-tools --allow-all-paths --allow-all-urls` — the +sandcat network isolation is the security boundary, so bypassing in-container +permission prompts is the intended workflow. diff --git a/docs/agents/cursor.md b/docs/agents/cursor.md new file mode 100644 index 00000000..b8ddb00e --- /dev/null +++ b/docs/agents/cursor.md @@ -0,0 +1,139 @@ +# Cursor CLI + +Cursor CLI support is available via `sandcat init --agent cursor`. + +## Authentication and CLI configuration + +Cursor CLI support is available via `sandcat init --agent cursor`. + +- The current template uses temporary compatibility defaults for auth/network: + - **Auth passthrough via placeholder substitution.** The container sees only + `SANDCAT_PLACEHOLDER_CURSOR_API_KEY`; the real `CURSOR_API_KEY` is injected + by the mitmproxy addon only for allowed Cursor hosts. + - **HTTP/1 compatibility bootstrap.** On startup, Sandcat forces + `.network.useHttp1ForAgent = true` in Cursor CLI config to avoid known + proxy/TLS instability with HTTP/2 streaming through mitmproxy. + - **Proxy command defaults tuned for Cursor.** The generated proxy config uses + the Cursor addon and keeps mitmproxy HTTP/2 enabled (`http2=true`) (plus + streaming-safe mitmproxy + flags such as `stream_large_bodies=1m`, `connection_strategy=lazy`, + `anticomp=true`, and `timeout_read=300`). + + Those streaming-safe flags are **Cursor-only** — they are intentionally + omitted on the Claude path (`sct_agent_mitm_streaming_flags`). With + `stream_large_bodies` unset, mitmproxy buffers request bodies up to ~1 MB + before forwarding, which lets the addon's `_substitute_secrets` run a + body-content scan for placeholder leaks. Setting them on Claude would + weaken that defence-in-depth check; on Cursor they are required to keep + Connect/HTTP-2 streaming responses stable, and the body-leak check is + instead enforced via header/URL scans plus the textual-only body-mutation + gate (binary protobuf bodies are left untouched). + - **Streaming detection is path-only.** The Cursor addon decides whether a + request is streaming purely from the request path + (`/agent.v1.AgentService/Run*`, `/aiserver.v1.RepositoryService/...`). + A client-supplied `content-type: application/connect+proto` header alone + is **not** sufficient — accepting it would let any request with the right + header bypass body substitution and the placeholder leak check. + These defaults are conservative and may be relaxed when Cursor proxy behavior + is consistently stable across environments. +- **Authentication:** put the Cursor API key in `secrets.CURSOR_API_KEY` in + Sandcat settings (not in `cursor.cli`). The agent container receives only + `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` via `sandcat.env`; mitmproxy substitutes + the real key on allowed Cursor hosts (see placeholder substitution above). + Do not use `agent login` in the sandbox unless you accept that Cursor may + store session state under agent-home outside Sandcat's placeholder model. +- **Cursor CLI settings via Sandcat:** add a `cursor.cli` block to + `~/.config/sandcat/settings.json` (or project `.sandcat/settings.json`) using + the same JSON shape as Cursor's global `cli-config.json` (permissions, model, + network flags — not API keys). Sandcat merges settings layers at mitmproxy + startup, writes `/mitmproxy-public/cursor-cli-config.json`, and the agent + deep-merges that fragment into `cli-config.json` in agent-home on each start. + Sandcat-owned keys win; other Cursor-written keys in that file (model choice, + permissions allow/deny lists, etc.) are preserved. The Cursor user template + defaults include `cursor.cli.network.useHttp1ForAgent: true` for mitmproxy + stability. +- `SANDCAT_MOUNT_CURSOR_CONFIG=true` mounts host Cursor config into the agent + container. Customization paths are read-only: `AGENTS.md`, `rules/`, `skills/`, + `commands/`, `hooks.json`, `hooks/`, `agents/`, and `mcp.json`. Runtime state + for this sandbox is read-write on the host under + `projects//` only (`workspaces-` — agent + transcripts, terminals, MCP session state). `chats/`, `plugins/`, and + `subagents/` are not host-mounted (they live in `agent-home`). On + `sandcat init`, missing bind sources are pre-created on the host (directories + via `mkdir`, JSON files with minimal valid defaults, markdown files empty) so + Docker mounts a file instead of materialising a root-owned directory. +- **Config precedence:** `~/.config/sandcat/settings.json` governs network + allowlists, secret substitution (mitmproxy), and Sandcat-managed Cursor CLI + settings (`cursor.cli` — not credentials). Host Cursor customization mounts + are read-only user config. The workspace-scoped `projects//` + mount is read-write on the host. MCP servers in `mcp.json` still need + matching mitmproxy allowlist entries before they can reach the network from + the sandbox. +- **Cursor CLI TLS through mitmproxy.** The Cursor CLI bundles its own Node.js + binary with compiled-in Mozilla CA roots. Sandcat sets + `NODE_OPTIONS=--use-openssl-ca` so the bundled Node.js uses the system CA + store (which includes the mitmproxy CA) instead of its built-in roots. + When Cursor honors that environment setting, mitmproxy can intercept Cursor + API traffic and perform `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` substitution + transparently. +- Provider-specific onboarding/bootstrap logic is intentionally minimal in this + first iteration and can be extended in project-level Dockerfile/scripts. + +## Host paths and mounts + +**Cursor paths** (host `~/.cursor/`): + +| Path | Mode | Typical use | +|----------------------------------------------------------------------------------------------|------------|---------------------------------------| +| `AGENTS.md`, `rules/`, `skills/`, `commands/`, `hooks.json`, `hooks/`, `agents/`, `mcp.json` | read-only | Shared customization | +| `projects//` | read-write | This sandbox's transcripts/terminals | + +Sandcat mounts only `projects//` for the current sandbox +(`workspaces-`), not the whole host `projects/` tree. `chats/`, +`plugins/`, and `subagents/` stay in `agent-home` so other workspaces' runtime +state is not exposed. + +Cursor CLI keys Sandcat manages (`cursor.cli` in settings) are **not** +host-mounted — see the Cursor section below. + +Project-local configuration (`.cursor/` in the repo) and the isolation +semantics of these mounts are described in +[Customizing optional volume mounts](../configuration/volume-mounts.md). + +## RTK hook + +Cursor's rtk hook lives in `~/.cursor/hooks.json`. Sandcat bind-mounts +that file **read-only** from your host (`SANDCAT_MOUNT_CURSOR_CONFIG=true` +default) so cursor customizations are shared across all your sandboxes. +Because the mount is read-only, sandcat cannot install the rtk hook +into the container's copy of `hooks.json`. + +**Setup — run once on your host:** + +```bash +# Install rtk locally (needed once on the host) +brew install rtk # or: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/b34be37caf3796b69a50952a28e60e32b5daad43/install.sh | RTK_VERSION=v0.45.0 sh + +# Register the cursor hook in your host ~/.cursor/hooks.json +rtk init -g --hook-only --auto-patch --agent cursor +``` + +That writes the rtk hook to your host `~/.cursor/hooks.json`. Every +sandcat cursor sandbox from now on bind-mounts that file into the +container, so Cursor CLI sees the hook and calls `rtk hook cursor` on +each `Bash` tool invocation. The container's own `rtk` binary +(installed by sandcat) executes the hook — you never need rtk on the +host for anything except this one-time init step, and you can +uninstall it afterwards if you like. + +Bonus: the same host hook is picked up by every cursor sandbox you +start on that machine (and by host Cursor CLI, if you use it directly). + +**If you skip the host init:** the container prints a one-time warning +on start (`sandcat: rtk hook not found for cursor. Install rtk on your +host …`) and Cursor CLI runs without the hook. The rtk binary is still +on `PATH` inside the container, so you can invoke `rtk grep`, `rtk ls`, +etc. by hand. + +See [RTK — LLM token compression](rtk.md) for what RTK does and how to opt +out. diff --git a/docs/agents/rtk.md b/docs/agents/rtk.md new file mode 100644 index 00000000..c3ac29f7 --- /dev/null +++ b/docs/agents/rtk.md @@ -0,0 +1,30 @@ +# RTK — LLM token compression + +[rtk-ai/rtk](https://github.com/rtk-ai/rtk) ("Rust Token Killer") wraps +shell commands invoked by AI agents and compresses their output before +the agent reads it, cutting token consumption 60-90% on typical dev +commands (test runs, grep output, build logs). `sandcat init` installs +the `rtk` binary into every sandbox by default and wires the agent +hook so the agent picks it up automatically. Setup differs slightly +per agent — see below. + +**Opt out** if you'd rather run without it (e.g. debugging a shell +command's raw output): + +```bash +sandcat init --features no-rtk ... +SANDCAT_RTK=false sandcat init ... +``` + +Both are equivalent — the env var is the scripted counterpart of the +interactive/CSV feature flag. When disabled, the rtk binary is not +installed into the image and no init hook is emitted for any agent. + +## Per-agent setup + +| Agent | Setup | +|-------|-------| +| Claude Code | zero configuration — see [Claude Code → RTK hook](claude.md#rtk-hook) | +| Cursor CLI | one-time host-side hook — see [Cursor CLI → RTK hook](cursor.md#rtk-hook) | +| Codex CLI | wired automatically at init — see [Codex CLI](codex.md) | +| GitHub Copilot CLI | wired automatically at init — see [GitHub Copilot CLI](copilot.md) | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 3c28ade1..19eab80c 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -85,7 +85,38 @@ flowchart TB - **Claude Code customizations** (`CLAUDE.md`, `agents/`, `commands/`) and **Cursor host config** (`~/.cursor/*` — see Cursor section above) are bind-mounted from the host when enabled in `compose-all.yml`. Per-path toggles - are described in [Customizing optional volume mounts](../getting-started/initialization.md#customizing-optional-volume-mounts). + are described in [Customizing optional volume mounts](../configuration/volume-mounts.md). + +## Agent container hardening + +The agent container is where untrusted code runs, so beyond the network +boundary it is stripped of every escalation path it does not need: + +- **`no-new-privileges`** — the agent service runs with + `security_opt: no-new-privileges`, so no process inside can gain + privileges through setuid/setgid binaries or file capabilities. +- **No sudo** — the base devcontainer image grants the `vscode` user + passwordless sudo; sandcat's `Dockerfile.app` removes that grant + (`rm -f /etc/sudoers.d/vscode`). Root-phase setup runs in the entrypoint + before it drops to `vscode` via `gosu`; nothing in the sandbox needs sudo + afterwards. Combined with `no-new-privileges`, `sudo` is blocked twice + over even if reinstalled. +- **No `NET_ADMIN`** — only `wg-client` holds it. The agent shares + wg-client's network namespace, so it *sees* the tunnel, routing tables, + and iptables kill switch, but cannot modify any of them. +- **No key material** — the agent mounts only the `mitmproxy-public` + volume (CA *certificate*, `sandcat.env`); the CA private key and + WireGuard keys stay on the private volume it never sees (see + [Volumes](#volumes) above). +- **Read-only sandbox config** — `.devcontainer/` is overlaid read-only on + the workspace mount, so the agent cannot rewrite its own entrypoint, + compose files, or `devcontainer.json`. + +With `--ide jetbrains` the agent additionally gets +`DAC_OVERRIDE`/`CHOWN`/`FOWNER` (the backend IDE manages files it does not +own) — still under `no-new-privileges` and still without `NET_ADMIN`. The +IDE-side trust boundary is covered separately in +[VS Code → Security hardening](../ide/vscode.md#security-hardening). ## Startup sequence diff --git a/docs/configuration/caches.md b/docs/configuration/caches.md new file mode 100644 index 00000000..fc0a8842 --- /dev/null +++ b/docs/configuration/caches.md @@ -0,0 +1,85 @@ +# Shared dependency caches + +For stacks that download a lot of common dependencies, sandcat mounts a set +of **host-scoped named volumes** so `cats-effect`, `spring-boot`, etc. +downloaded in one project are instantly available in every other project on +the same host. Otherwise each sandbox re-downloads and re-stores the same +JAR trees inside its own `agent-home`, adding several GB per project. + +The cache set is picked **per stack** — a project without a matching stack +gets no shared cache mounts at all. Today only the JVM stack (`--stacks java`, +also pulled in by `scala`) contributes cache entries; other language stacks +are not supported now. + +Java/Scala cache mounts (all under `/home/vscode/` in the container): + +| Path | Cache for | +|-----------------------------|-------------------------------------------------------| +| `.m2/repository/` | Maven local repository | +| `.cache/coursier/` | Coursier — sbt (modern), scala-cli, Metals | +| `.gradle/caches/` | Gradle dependency cache | +| `.gradle/wrapper/dists/` | Gradle Wrapper distributions | +| `.ivy2/cache/` | Ivy — legacy sbt (pre-Coursier resolver) | +| `.sbt/boot/` | sbt bootstrap (sbt binaries + Scala compiler) | + +Each is a Docker named volume with a stable host-wide name +(`sandcat-cache-maven`, `sandcat-cache-coursier`, …) declared as +`external: true` in the generated `compose-all.yml`. Multiple sandcat compose +projects reference the same physical volume, and `sandcat compose down -v` on +one project will **not** wipe caches other projects rely on. The `sandcat run` +wrapper creates them lazily via `docker volume create` (idempotent), so no +manual setup is required. + +Only `/home/vscode/.m2/repository/` is shared, not the whole `.m2/` — user +config like `settings.xml` stays per-project inside `agent-home`. Same pattern +for `.gradle/` (only `caches/` and `wrapper/dists/`, not `daemon/` or +`init.d/`) and `.ivy2/` (only `cache/`, not `local/` where `sbt publishLocal` +outputs live). + +**Opt out per project:** + +```bash +sandcat init --features no-shared-cache ... # interactive selection also + # exposes it in the menu +``` + +Or set the env var before init (equivalent to the feature flag): + +```bash +SANDCAT_MOUNT_SHARED_CACHE=false sandcat init ... +``` + +With shared cache disabled, the mount lines stay in `compose-all.yml` as +comments — you can flip individual ones back on by uncommenting. + +**Trade-offs to be aware of:** + +* Shared caches break sandcat's per-project isolation model for those specific + paths. If one project's build corrupts a JAR (rare — Maven and Coursier both + do content-hash validation), other projects using shared cache pick up the + corruption. Disable per project if you need hermetic isolation (regulated + environments, security-sensitive projects). +* Two parallel builds writing the same artifact rely on the tools' own file + locking (Maven `.locks/`, Coursier per-artifact `.lock`, Gradle `.lock`). + This works reliably in practice but is not sandcat-mediated. + +**Managing shared caches:** + +```bash +# Detailed table — volume name, size, file count, running container users +sandcat cache list +sandcat cache # same as `list` + +# Quick total across all shared-cache volumes +sandcat cache size + +# Wipe one (next build re-downloads what the project needs) +sandcat cache rm sandcat-cache-maven + +# Wipe them all — resets every shared cache on the host +sandcat cache rm --all +``` + +`sandcat cache rm` refuses to remove a volume that a running sandbox +still mounts; stop the sandbox first, or pass `--force` to bypass the +check (Docker will then error out if the volume is truly locked). diff --git a/docs/configuration/generated-files.md b/docs/configuration/generated-files.md index 335a0eb7..e0bd9f77 100644 --- a/docs/configuration/generated-files.md +++ b/docs/configuration/generated-files.md @@ -16,10 +16,10 @@ mount whose source does not exist on your host. [devbox](https://www.jetify.com/devbox), a wrapper over Nix. Stack toolchains and user tools are merged from `devbox.stack.json` + `devbox.tools.json` into a single devbox global profile at build time — -see [Stack and tool packages via devbox](../getting-started/initialization.md#stack-and-tool-packages-via-devbox) +see [Stack and tool packages via devbox](stacks.md) for the two-file model. Some runtimes need extra configuration to trust the mitmproxy CA — see [TLS and CA certificates](../reference/notes.md#tls-and-ca-certificates). **`devcontainer.json`** — includes VS Code hardening settings (credential socket cleanup, workspace trust, disabled local terminal). See [Hardening the VS Code -setup](../security/vscode-hardening.md#hardening-the-vs-code-setup) for details. +setup](../ide/vscode.md#security-hardening) for details. diff --git a/docs/configuration/gitignore.md b/docs/configuration/gitignore.md new file mode 100644 index 00000000..271dc6d7 --- /dev/null +++ b/docs/configuration/gitignore.md @@ -0,0 +1,43 @@ +# Gitignore defaults + +When the project has a `.git/` directory, `sandcat init` appends a +`# Sandcat` block to `.gitignore` (creating the file if needed) so +users don't accidentally commit files that are either regenerated on +next init or per-machine: + +```text +# Sandcat +.devcontainer/* +!.devcontainer/devbox.tools.json +.sandcat/settings.local.json +# /Sandcat +``` + +The `!.devcontainer/devbox.tools.json` negation keeps the user-managed +tool list in git — it's the project-shared extension of the stack (see +[Stack and tool packages via devbox](stacks.md)) +and travels with the repo even though everything else under +`.devcontainer/` is ignored. + +The block is bracketed by `# Sandcat` / `# /Sandcat` sentinels so +sandcat can manage it symmetrically: enabling on a subsequent init is +a no-op when the block is already present, and disabling **removes** +the block cleanly (preserving your other rules). + +**Opt out** if you'd rather keep the generated files in git (e.g. team +convention where each dev clones a ready-to-run devcontainer without +re-running `sandcat init`): + +```bash +sandcat init --features no-gitignore ... +SANDCAT_GITIGNORE=false sandcat init ... +``` + +Both are equivalent — the env var is the scripted counterpart of the +interactive/CSV feature flag. If a Sandcat block already exists in +`.gitignore`, opting out on a re-init deletes the block (and, when +the block was the file's only content, deletes the file too). Rules +outside the sandcat markers are always preserved. + +If the project is not a git working tree (no `.git/` directory), init +silently skips the gitignore step — no `.gitignore` gets created. diff --git a/docs/configuration/secrets.md b/docs/configuration/secrets.md index a08be097..0fc52c93 100644 --- a/docs/configuration/secrets.md +++ b/docs/configuration/secrets.md @@ -24,6 +24,48 @@ If a placeholder appears in a request to a host **not** in the allowlist, mitmproxy blocks the request with HTTP 403 and logs a warning. This prevents accidental secret leakage to unintended services. +## Basic Auth (git credentials) + +Placeholders hidden inside `Authorization: Basic` headers are substituted +too. This matters for git over HTTPS: a credential helper (e.g. `gh auth +git-credential`, or a stored placeholder) returns the placeholder as the +*password*, and git then base64-encodes the whole `username:password` pair — +so the placeholder never appears in plain text anywhere in the request. + +For every request whose `Authorization` header uses the `Basic` scheme, the +addon decodes the blob, replaces any secret placeholder found inside, and +re-encodes it. The [leak detection](#leak-detection) gate applies exactly as +for plain-text substitution: the secret is only injected when the destination +host matches the secret's `hosts` allowlist. Malformed or non-Basic headers +pass through untouched. This works for every agent and every allowlisted +host. + +Example — an on-prem GitLab: + +```json +"secrets": { + "GITLAB_TOKEN": { + "value": "glpat-…", + "hosts": ["gitlab.corp.example"] + } +} +``` + +Inside the container, authenticate with the placeholder as the password: + +```bash +git clone https://oauth2:$GITLAB_TOKEN@gitlab.corp.example/group/repo.git +# or let git prompt / a credential helper supply it: +# username: oauth2 +# password: $GITLAB_TOKEN (i.e. SANDCAT_PLACEHOLDER_GITLAB_TOKEN) +``` + +Git encodes `oauth2:SANDCAT_PLACEHOLDER_GITLAB_TOKEN` into the Basic header; +mitmproxy decodes it, swaps in the real token for `gitlab.corp.example`, and +re-encodes — the container never sees the token. For a self-signed GitLab +also see [`upstream_ca_bundles`](../reference/notes.md#trusting-internal-cas-upstream) +and [`extra_hosts`](dns.md). + ## 1Password integration Instead of storing secret values directly in settings files, you can reference @@ -115,143 +157,8 @@ Remove all settings files. If no settings file exists at any layer, the addon disables itself — no network rules are enforced and `sandcat.env` is not written. -## Claude Code - -Claude Code supports two authentication methods inside the container: - -- **API key** — add an `ANTHROPIC_API_KEY` secret to `settings.json`. The - entrypoint detects the key and seeds `~/.claude.json` with - `{"hasCompletedOnboarding": true}` so Claude Code uses it without interactive - setup. -- **Subscription (browser login)** — omit `ANTHROPIC_API_KEY` from - `settings.json`. On first run Claude Code will display a URL and a code. Open - the URL in a browser on your host machine, enter the code, and authenticate - there — the container itself cannot open a browser. - -**Autonomous mode.** The bundled `devcontainer.json` enables -`claudeCode.allowDangerouslySkipPermissions` and sets -`claudeCode.initialPermissionMode` to `bypassPermissions`. This lets Claude Code -run without interactive permission prompts inside the container. The trade-off: -sandcat already provides the security boundary (network isolation, secret -substitution, iptables kill-switch), so the in-container prompts add friction -without meaningful security benefit. Remove these settings if you prefer -interactive approval. See [Secure & Dangerous Claude Code + VS Code -Setup](https://warski.org/blog/secure-dangerous-claude-code-vs-code-setup/) for -background on this approach. - -**Host customizations.** The example `compose-all.yml` bind-mounts -`~/.claude/CLAUDE.md`, `~/.claude/agents`, and `~/.claude/commands` from the -host (read-only) so your personal instructions, custom agents, and slash -commands are available inside the container. Remove any mount whose source does -not exist on your host — Docker will otherwise create an empty directory in its -place. - -**Multi-line prompts.** Composing a multi-line prompt with `⌘+Enter` does not -work on macOS — the terminal reserves the `⌘` modifier and never transmits it -over the PTY, so `sandcat attach` (and Claude Code) only ever receive a plain -`Enter`. This is not sandcat-specific and cannot be fixed inside the container. -Use one of these instead: - -- **`\` then `Enter`** — inserts a newline in any terminal with no setup. The - simplest option. -- **`Option+Enter`** — Claude Code's macOS default. In Apple Terminal, first - enable *Settings → Profiles → Keyboard → Use Option as Meta key*; iTerm2 sends - it out of the box. -- **`Shift+Enter`** — the most familiar combination, but Claude Code only - receives whatever bytes the terminal chooses to send for it, so it needs a - one-time mapping in the **host** terminal. Claude Code's `/terminal-setup` is - meant to install this, but it has two traps in this setup: it configures the - host terminal, so running it from Claude Code *inside* the sandbox does - nothing; and it caches an "installed" flag, so a second run reports *"already - enabled"* even when the terminal was never actually changed. The reliable route - is to map the key by hand: - - **iTerm2** — Settings → Keys → Key Bindings → `+`, record `Shift+Enter`, - choose *Send Hex Codes* and enter `0x1b 0x0d` (this is `Option+Enter`, which - Claude Code treats as a newline). GUI bindings take effect immediately. To - confirm it worked, run `cat -v` in the sandbox shell and press `Shift+Enter`: - it should print `^[` instead of a blank line. - - **VS Code integrated terminal** — add to `keybindings.json`: - - ```json - { "key": "shift+enter", - "command": "workbench.action.terminal.sendSequence", - "args": { "text": "\u001b\r" }, - "when": "terminalFocus" } - ``` - -## Cursor CLI - -Cursor CLI support is available via `sandcat init --agent cursor`. - -- The current template uses temporary compatibility defaults for auth/network: - - **Auth passthrough via placeholder substitution.** The container sees only - `SANDCAT_PLACEHOLDER_CURSOR_API_KEY`; the real `CURSOR_API_KEY` is injected - by the mitmproxy addon only for allowed Cursor hosts. - - **HTTP/1 compatibility bootstrap.** On startup, Sandcat forces - `.network.useHttp1ForAgent = true` in Cursor CLI config to avoid known - proxy/TLS instability with HTTP/2 streaming through mitmproxy. - - **Proxy command defaults tuned for Cursor.** The generated proxy config uses - the Cursor addon and keeps mitmproxy HTTP/2 enabled (`http2=true`) (plus - streaming-safe mitmproxy - flags such as `stream_large_bodies=1m`, `connection_strategy=lazy`, - `anticomp=true`, and `timeout_read=300`). - - Those streaming-safe flags are **Cursor-only** — they are intentionally - omitted on the Claude path (`sct_agent_mitm_streaming_flags`). With - `stream_large_bodies` unset, mitmproxy buffers request bodies up to ~1 MB - before forwarding, which lets the addon's `_substitute_secrets` run a - body-content scan for placeholder leaks. Setting them on Claude would - weaken that defence-in-depth check; on Cursor they are required to keep - Connect/HTTP-2 streaming responses stable, and the body-leak check is - instead enforced via header/URL scans plus the textual-only body-mutation - gate (binary protobuf bodies are left untouched). - - **Streaming detection is path-only.** The Cursor addon decides whether a - request is streaming purely from the request path - (`/agent.v1.AgentService/Run*`, `/aiserver.v1.RepositoryService/...`). - A client-supplied `content-type: application/connect+proto` header alone - is **not** sufficient — accepting it would let any request with the right - header bypass body substitution and the placeholder leak check. - These defaults are conservative and may be relaxed when Cursor proxy behavior - is consistently stable across environments. -- **Authentication:** put the Cursor API key in `secrets.CURSOR_API_KEY` in - Sandcat settings (not in `cursor.cli`). The agent container receives only - `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` via `sandcat.env`; mitmproxy substitutes - the real key on allowed Cursor hosts (see placeholder substitution above). - Do not use `agent login` in the sandbox unless you accept that Cursor may - store session state under agent-home outside Sandcat's placeholder model. -- **Cursor CLI settings via Sandcat:** add a `cursor.cli` block to - `~/.config/sandcat/settings.json` (or project `.sandcat/settings.json`) using - the same JSON shape as Cursor's global `cli-config.json` (permissions, model, - network flags — not API keys). Sandcat merges settings layers at mitmproxy - startup, writes `/mitmproxy-public/cursor-cli-config.json`, and the agent - deep-merges that fragment into `cli-config.json` in agent-home on each start. - Sandcat-owned keys win; other Cursor-written keys in that file (model choice, - permissions allow/deny lists, etc.) are preserved. The Cursor user template - defaults include `cursor.cli.network.useHttp1ForAgent: true` for mitmproxy - stability. -- `SANDCAT_MOUNT_CURSOR_CONFIG=true` mounts host Cursor config into the agent - container. Customization paths are read-only: `AGENTS.md`, `rules/`, `skills/`, - `commands/`, `hooks.json`, `hooks/`, `agents/`, and `mcp.json`. Runtime state - for this sandbox is read-write on the host under - `projects//` only (`workspaces-` — agent - transcripts, terminals, MCP session state). `chats/`, `plugins/`, and - `subagents/` are not host-mounted (they live in `agent-home`). On - `sandcat init`, missing bind sources are pre-created on the host (directories - via `mkdir`, JSON files with minimal valid defaults, markdown files empty) so - Docker mounts a file instead of materialising a root-owned directory. -- **Config precedence:** `~/.config/sandcat/settings.json` governs network - allowlists, secret substitution (mitmproxy), and Sandcat-managed Cursor CLI - settings (`cursor.cli` — not credentials). Host Cursor customization mounts - are read-only user config. The workspace-scoped `projects//` - mount is read-write on the host. MCP servers in `mcp.json` still need - matching mitmproxy allowlist entries before they can reach the network from - the sandbox. -- **Cursor CLI TLS through mitmproxy.** The Cursor CLI bundles its own Node.js - binary with compiled-in Mozilla CA roots. Sandcat sets - `NODE_OPTIONS=--use-openssl-ca` so the bundled Node.js uses the system CA - store (which includes the mitmproxy CA) instead of its built-in roots. - When Cursor honors that environment setting, mitmproxy can intercept Cursor - API traffic and perform `SANDCAT_PLACEHOLDER_CURSOR_API_KEY` substitution - transparently. -- Provider-specific onboarding/bootstrap logic is intentionally minimal in this - first iteration and can be extended in project-level Dockerfile/scripts. +## Agent-specific notes + +Per-agent authentication and substitution behavior moved to the **Agents** +section: [Claude Code](../agents/claude.md#authentication), +[Cursor CLI](../agents/cursor.md#authentication-and-cli-configuration). diff --git a/docs/configuration/stacks.md b/docs/configuration/stacks.md new file mode 100644 index 00000000..ccfbe75c --- /dev/null +++ b/docs/configuration/stacks.md @@ -0,0 +1,64 @@ +# Stack and tool packages via devbox + +All packages inside the sandbox — both stack toolchains and user tools — +are managed with [devbox](https://www.jetify.com/devbox), which resolves +them from Nix. `sandcat init` generates two config files side by side in +`.devcontainer/`: + +**`devbox.stack.json`** — sandcat-managed. Regenerated on every +`sandcat init` from the `--stacks` selection plus a baseline of shell tools +every sandbox needs (`fd`, `fzf`, `gh`, `jq`, `ripgrep`, `tmux`, `vim`). +Do not edit by hand — your changes will be overwritten on the next init. + +**`devbox.tools.json`** — user-managed. Written once with an empty +`packages` list; subsequent `sandcat init` invocations leave it untouched. +Add project-specific tools here. + +At image build time the two files are merged into a single devbox global +config. `devbox.tools.json` wins over `devbox.stack.json` on: + +* **Same package name** — the `@` prefix. Put `nodejs@22.5.1` in tools + to replace the stack's `nodejs` (without a specifier, `nodejs` refers to lts). +* **Cross-family collisions** — tools packages providing the same file + as a stack package win too. Put `openjdk17@latest` in tools to make + it the active Java over the stack's `temurin-bin-25@latest`; the + agent's `java`, `JAVA_HOME` and the injected mitmproxy CA all + resolve to the tools JDK. + +Non-overriding tools entries just add to the merged config. Search +available packages on [nixhub.io](https://www.nixhub.io/). + +Example — give the agent [yq](https://github.com/mikefarah/yq), +[shellcheck](https://www.shellcheck.net/), and +[hyperfine](https://github.com/sharkdp/hyperfine) by dropping them into +`devbox.tools.json`: + +```json +{ + "packages": ["yq-go@latest", "shellcheck@latest", "hyperfine@latest"] +} +``` + +Then rebuild the agent image: + +```bash +sandcat run --build +# or, without starting the full stack: +docker compose -f .devcontainer/compose-all.yml build agent +``` + +Every shell inside the sandbox — including the agent's — picks up the +packages on `PATH`. Iterating on `devbox.tools.json` is the fast path: +the stack install layer stays cached and only the delta downloads +(typically seconds). + +Installs are build-time only: `devbox add` inside the sandbox is not +supported, and no Nix download hosts are added to the network allowlist. +To pin the exact package versions across environments, commit +`.devcontainer/devbox.lock` next to the JSON files; the build picks it up +automatically. + +Optional volume mounts (agent config, `.git`, `.idea`) are written into the +generated `.devcontainer/compose-all.yml`. See [Customizing optional volume +mounts](volume-mounts.md) below. For scripted `sandcat init`, +set `SANDCAT_*` environment variables (see the [CLI reference](../reference/cli.md)). diff --git a/docs/configuration/volume-mounts.md b/docs/configuration/volume-mounts.md new file mode 100644 index 00000000..89c26ff1 --- /dev/null +++ b/docs/configuration/volume-mounts.md @@ -0,0 +1,54 @@ +# Customizing optional volume mounts + +`sandcat init` adds optional bind-mounts to `services.agent.volumes` in +`.devcontainer/compose-all.yml`. Each mount is an independent line — you can +enable or disable **individual paths** by editing that file after init. This +works the same way for Claude and Cursor; there are no per-folder `sandcat init` +flags today. + +**All-or-nothing at init time** (scripted workflows only): + +| Agent | Environment variable | Default | +|-----------|-------------------------------|-----------------------------------------| +| Claude | `SANDCAT_MOUNT_CLAUDE_CONFIG` | `true` | +| Cursor | `SANDCAT_MOUNT_CURSOR_CONFIG` | `true` | +| Codex | `SANDCAT_MOUNT_CODEX_CONFIG` | `true` | +| Any | `SANDCAT_MOUNT_GIT_READONLY` | `false` (commented in compose) | +| JetBrains | `SANDCAT_MOUNT_IDEA_READONLY` | `false` (active when `--ide jetbrains`) | +| Any | `SANDCAT_MOUNT_SHARED_CACHE` | `true` — see [Shared dependency caches](caches.md) | +| Any | `SANDCAT_GITIGNORE` | `true` (see [Gitignore defaults](gitignore.md)) | +| Any | `SANDCAT_RTK` | `true` (see [RTK — LLM token compression](../agents/rtk.md)) | + +When an agent mount flag is `false`, Sandcat lists every path as a foot comment +on the first volume entry — copy the lines you want into the active `volumes:` +list. + +**Per-path tuning (recommended):** edit `.devcontainer/compose-all.yml`, remove +or comment out mounts you do not want, then rebuild/reopen the devcontainer: + +```yaml +# Mount a different workspace's Cursor transcripts (not recommended): +# - ${HOME}/.cursor/projects/workspaces-other-project:/home/vscode/.cursor/projects/workspaces-other-project +``` + +**Do not re-run `sandcat init`** unless you intend to reset generated files — +it recopies the template and overwrites manual compose edits. Commit your +customized `compose-all.yml` to keep changes across the team. + +**Project-local config** (per repository, via the workspace code mount — not +controlled by `SANDCAT_MOUNT_*_CONFIG`): + +- Claude: `.claude/` in the repo (skills, agents, etc.) +- Cursor: `.cursor/` in the repo (rules, skills, agents, `cli.json`, etc.) + +Use host mounts for personal defaults shared across sandboxes; use repo +`.claude/` or `.cursor/` for project-specific or team-shared customization. + +**Isolation notes:** host `~/.claude/` and shared Cursor customization mounts +(`rules/`, `skills/`, etc.) are one profile per user on the machine — all +sandcat projects on that host see the same mounted trees. Cursor transcripts for +this sandbox persist under host `projects//` only +(`workspaces-`). Other workspaces' `projects/`, plus `chats/`, +`plugins/`, and `subagents/`, are not mounted. To keep a sandcat project fully +isolated from host agent state, set `SANDCAT_MOUNT__CONFIG=false` and +rely on repo-local config plus the `agent-home` volume inside the container. diff --git a/docs/getting-started/initialization.md b/docs/getting-started/initialization.md deleted file mode 100644 index 7401fd8e..00000000 --- a/docs/getting-started/initialization.md +++ /dev/null @@ -1,476 +0,0 @@ -# Initializing the sandbox - -```bash -sandcat init -``` - -This prompts you to select the agent type, IDE (for devcontainer mode), and -development stacks to install. You can also pass flags to skip prompts: - -```bash -sandcat init --agent claude --ide vscode --stacks "python,node" - -# With optional features (proxy TUI, 1Password integration) -sandcat init --secret-provider 1password --agent claude --ide vscode -``` - -Available agents: -- `claude` (Claude Code CLI) -- `cursor` (Cursor IDE) -- `codex` (OpenAI Codex CLI — https://github.com/openai/codex) -- `copilot` (GitHub Copilot CLI — https://docs.github.com/copilot/how-tos/copilot-cli) - -Available stacks: `node`, `python`, `java`, `rust`, `go`, `scala`, `ruby`, -`dotnet`, `zig`. Versions default to LTS where available (e.g. Node.js LTS, -Java LTS 25). To change a version for a single project, add the desired -package to `.devcontainer/devbox.tools.json` — see [Stack and tool packages -via devbox](#stack-and-tool-packages-via-devbox) below for how tool entries -override stack defaults. - -Selecting `scala` automatically includes `java` as a dependency. Stacks also -install the corresponding VS Code extension (e.g. `rust-analyzer` for Rust, -`metals` for Scala). - -## Stack and tool packages via devbox - -All packages inside the sandbox — both stack toolchains and user tools — -are managed with [devbox](https://www.jetify.com/devbox), which resolves -them from Nix. `sandcat init` generates two config files side by side in -`.devcontainer/`: - -**`devbox.stack.json`** — sandcat-managed. Regenerated on every -`sandcat init` from the `--stacks` selection plus a baseline of shell tools -every sandbox needs (`fd`, `fzf`, `gh`, `jq`, `ripgrep`, `tmux`, `vim`). -Do not edit by hand — your changes will be overwritten on the next init. - -**`devbox.tools.json`** — user-managed. Written once with an empty -`packages` list; subsequent `sandcat init` invocations leave it untouched. -Add project-specific tools here. - -At image build time the two files are merged into a single devbox global -config. `devbox.tools.json` wins over `devbox.stack.json` on: - -* **Same package name** — the `@` prefix. Put `nodejs@22.5.1` in tools - to replace the stack's `nodejs` (without a specifier, `nodejs` refers to lts). -* **Cross-family collisions** — tools packages providing the same file - as a stack package win too. Put `openjdk17@latest` in tools to make - it the active Java over the stack's `temurin-bin-25@latest`; the - agent's `java`, `JAVA_HOME` and the injected mitmproxy CA all - resolve to the tools JDK. - -Non-overriding tools entries just add to the merged config. Search -available packages on [nixhub.io](https://www.nixhub.io/). - -Example — give the agent [yq](https://github.com/mikefarah/yq), -[shellcheck](https://www.shellcheck.net/), and -[hyperfine](https://github.com/sharkdp/hyperfine) by dropping them into -`devbox.tools.json`: - -```json -{ - "packages": ["yq-go@latest", "shellcheck@latest", "hyperfine@latest"] -} -``` - -Then rebuild the agent image: - -```bash -sandcat run --build -# or, without starting the full stack: -docker compose -f .devcontainer/compose-all.yml build agent -``` - -Every shell inside the sandbox — including the agent's — picks up the -packages on `PATH`. Iterating on `devbox.tools.json` is the fast path: -the stack install layer stays cached and only the delta downloads -(typically seconds). - -Installs are build-time only: `devbox add` inside the sandbox is not -supported, and no Nix download hosts are added to the network allowlist. -To pin the exact package versions across environments, commit -`.devcontainer/devbox.lock` next to the JSON files; the build picks it up -automatically. - -Optional volume mounts (agent config, `.git`, `.idea`) are written into the -generated `.devcontainer/compose-all.yml`. See [Customizing optional volume -mounts](#customizing-optional-volume-mounts) below. For scripted `sandcat init`, -set `SANDCAT_*` environment variables (see the [CLI reference](../reference/cli.md)). - -## Customizing optional volume mounts - -`sandcat init` adds optional bind-mounts to `services.agent.volumes` in -`.devcontainer/compose-all.yml`. Each mount is an independent line — you can -enable or disable **individual paths** by editing that file after init. This -works the same way for Claude and Cursor; there are no per-folder `sandcat init` -flags today. - -**All-or-nothing at init time** (scripted workflows only): - -| Agent | Environment variable | Default | -|-----------|-------------------------------|-----------------------------------------| -| Claude | `SANDCAT_MOUNT_CLAUDE_CONFIG` | `true` | -| Cursor | `SANDCAT_MOUNT_CURSOR_CONFIG` | `true` | -| Codex | `SANDCAT_MOUNT_CODEX_CONFIG` | `true` | -| Any | `SANDCAT_MOUNT_GIT_READONLY` | `false` (commented in compose) | -| JetBrains | `SANDCAT_MOUNT_IDEA_READONLY` | `false` (active when `--ide jetbrains`) | -| Any | `SANDCAT_MOUNT_SHARED_CACHE` | `true` — see [Shared dependency caches](#shared-dependency-caches) | -| Any | `SANDCAT_GITIGNORE` | `true` (see [Gitignore defaults](#gitignore-defaults)) | -| Any | `SANDCAT_RTK` | `true` (see [RTK — LLM token compression](#rtk--llm-token-compression)) | - -When an agent mount flag is `false`, Sandcat lists every path as a foot comment -on the first volume entry — copy the lines you want into the active `volumes:` -list. - -**Per-path tuning (recommended):** edit `.devcontainer/compose-all.yml`, remove -or comment out mounts you do not want, then rebuild/reopen the devcontainer: - -```yaml -# Mount a different workspace's Cursor transcripts (not recommended): -# - ${HOME}/.cursor/projects/workspaces-other-project:/home/vscode/.cursor/projects/workspaces-other-project -``` - -**Do not re-run `sandcat init`** unless you intend to reset generated files — -it recopies the template and overwrites manual compose edits. Commit your -customized `compose-all.yml` to keep changes across the team. - -**Claude paths** (host `~/.claude/`, read-only when mounted): - -- `CLAUDE.md`, `agents/`, `commands/` - -**Cursor paths** (host `~/.cursor/`): - -| Path | Mode | Typical use | -|----------------------------------------------------------------------------------------------|------------|---------------------------------------| -| `AGENTS.md`, `rules/`, `skills/`, `commands/`, `hooks.json`, `hooks/`, `agents/`, `mcp.json` | read-only | Shared customization | -| `projects//` | read-write | This sandbox's transcripts/terminals | - -Sandcat mounts only `projects//` for the current sandbox -(`workspaces-`), not the whole host `projects/` tree. `chats/`, -`plugins/`, and `subagents/` stay in `agent-home` so other workspaces' runtime -state is not exposed. - -Cursor CLI keys Sandcat manages (`cursor.cli` in settings) are **not** -host-mounted — see the Cursor section below. - -**Project-local config** (per repository, via the workspace code mount — not -controlled by `SANDCAT_MOUNT_*_CONFIG`): - -- Claude: `.claude/` in the repo (skills, agents, etc.) -- Cursor: `.cursor/` in the repo (rules, skills, agents, `cli.json`, etc.) - -Use host mounts for personal defaults shared across sandboxes; use repo -`.claude/` or `.cursor/` for project-specific or team-shared customization. - -**Isolation notes:** host `~/.claude/` and shared Cursor customization mounts -(`rules/`, `skills/`, etc.) are one profile per user on the machine — all -sandcat projects on that host see the same mounted trees. Cursor transcripts for -this sandbox persist under host `projects//` only -(`workspaces-`). Other workspaces' `projects/`, plus `chats/`, -`plugins/`, and `subagents/`, are not mounted. To keep a sandcat project fully -isolated from host agent state, set `SANDCAT_MOUNT__CONFIG=false` and -rely on repo-local config plus the `agent-home` volume inside the container. - -## Shared dependency caches - -For stacks that download a lot of common dependencies, sandcat mounts a set -of **host-scoped named volumes** so `cats-effect`, `spring-boot`, etc. -downloaded in one project are instantly available in every other project on -the same host. Otherwise each sandbox re-downloads and re-stores the same -JAR trees inside its own `agent-home`, adding several GB per project. - -The cache set is picked **per stack** — a project without a matching stack -gets no shared cache mounts at all. Today only the JVM stack (`--stacks java`, -also pulled in by `scala`) contributes cache entries; other language stacks -are not supported now. - -Java/Scala cache mounts (all under `/home/vscode/` in the container): - -| Path | Cache for | -|-----------------------------|-------------------------------------------------------| -| `.m2/repository/` | Maven local repository | -| `.cache/coursier/` | Coursier — sbt (modern), scala-cli, Metals | -| `.gradle/caches/` | Gradle dependency cache | -| `.gradle/wrapper/dists/` | Gradle Wrapper distributions | -| `.ivy2/cache/` | Ivy — legacy sbt (pre-Coursier resolver) | -| `.sbt/boot/` | sbt bootstrap (sbt binaries + Scala compiler) | - -Each is a Docker named volume with a stable host-wide name -(`sandcat-cache-maven`, `sandcat-cache-coursier`, …) declared as -`external: true` in the generated `compose-all.yml`. Multiple sandcat compose -projects reference the same physical volume, and `sandcat compose down -v` on -one project will **not** wipe caches other projects rely on. The `sandcat run` -wrapper creates them lazily via `docker volume create` (idempotent), so no -manual setup is required. - -Only `/home/vscode/.m2/repository/` is shared, not the whole `.m2/` — user -config like `settings.xml` stays per-project inside `agent-home`. Same pattern -for `.gradle/` (only `caches/` and `wrapper/dists/`, not `daemon/` or -`init.d/`) and `.ivy2/` (only `cache/`, not `local/` where `sbt publishLocal` -outputs live). - -**Opt out per project:** - -```bash -sandcat init --features no-shared-cache ... # interactive selection also - # exposes it in the menu -``` - -Or set the env var before init (equivalent to the feature flag): - -```bash -SANDCAT_MOUNT_SHARED_CACHE=false sandcat init ... -``` - -With shared cache disabled, the mount lines stay in `compose-all.yml` as -comments — you can flip individual ones back on by uncommenting. - -**Trade-offs to be aware of:** - -* Shared caches break sandcat's per-project isolation model for those specific - paths. If one project's build corrupts a JAR (rare — Maven and Coursier both - do content-hash validation), other projects using shared cache pick up the - corruption. Disable per project if you need hermetic isolation (regulated - environments, security-sensitive projects). -* Two parallel builds writing the same artifact rely on the tools' own file - locking (Maven `.locks/`, Coursier per-artifact `.lock`, Gradle `.lock`). - This works reliably in practice but is not sandcat-mediated. - -**Managing shared caches:** - -```bash -# Detailed table — volume name, size, file count, running container users -sandcat cache list -sandcat cache # same as `list` - -# Quick total across all shared-cache volumes -sandcat cache size - -# Wipe one (next build re-downloads what the project needs) -sandcat cache rm sandcat-cache-maven - -# Wipe them all — resets every shared cache on the host -sandcat cache rm --all -``` - -`sandcat cache rm` refuses to remove a volume that a running sandbox -still mounts; stop the sandbox first, or pass `--force` to bypass the -check (Docker will then error out if the volume is truly locked). - -## Gitignore defaults - -When the project has a `.git/` directory, `sandcat init` appends a -`# Sandcat` block to `.gitignore` (creating the file if needed) so -users don't accidentally commit files that are either regenerated on -next init or per-machine: - -```text -# Sandcat -.devcontainer/* -!.devcontainer/devbox.tools.json -.sandcat/settings.local.json -# /Sandcat -``` - -The `!.devcontainer/devbox.tools.json` negation keeps the user-managed -tool list in git — it's the project-shared extension of the stack (see -[Stack and tool packages via devbox](#stack-and-tool-packages-via-devbox)) -and travels with the repo even though everything else under -`.devcontainer/` is ignored. - -The block is bracketed by `# Sandcat` / `# /Sandcat` sentinels so -sandcat can manage it symmetrically: enabling on a subsequent init is -a no-op when the block is already present, and disabling **removes** -the block cleanly (preserving your other rules). - -**Opt out** if you'd rather keep the generated files in git (e.g. team -convention where each dev clones a ready-to-run devcontainer without -re-running `sandcat init`): - -```bash -sandcat init --features no-gitignore ... -SANDCAT_GITIGNORE=false sandcat init ... -``` - -Both are equivalent — the env var is the scripted counterpart of the -interactive/CSV feature flag. If a Sandcat block already exists in -`.gitignore`, opting out on a re-init deletes the block (and, when -the block was the file's only content, deletes the file too). Rules -outside the sandcat markers are always preserved. - -If the project is not a git working tree (no `.git/` directory), init -silently skips the gitignore step — no `.gitignore` gets created. - -## RTK — LLM token compression - -[rtk-ai/rtk](https://github.com/rtk-ai/rtk) ("Rust Token Killer") wraps -shell commands invoked by AI agents and compresses their output before -the agent reads it, cutting token consumption 60-90% on typical dev -commands (test runs, grep output, build logs). `sandcat init` installs -the `rtk` binary into every sandbox by default and wires the agent -hook so the agent picks it up automatically. Setup differs slightly -per agent — see below. - -**Opt out** if you'd rather run without it (e.g. debugging a shell -command's raw output): - -```bash -sandcat init --features no-rtk ... -SANDCAT_RTK=false sandcat init ... -``` - -Both are equivalent — the env var is the scripted counterpart of the -interactive/CSV feature flag. When disabled, the rtk binary is not -installed into the image and no init hook is emitted for any agent. - -### Claude Code (`--agent claude`) - -Works out of the box, zero configuration. `sandcat init` generates an -`app-user-init.sh` block that runs `rtk init -g --hook-only --auto-patch` -on the first container start; the hook lands in the sandbox's -`~/.claude/settings.json` (inside the `agent-home` volume, not -bind-mounted). Subsequent starts are idempotent no-ops. - -### Cursor CLI (`--agent cursor`) - -Cursor's rtk hook lives in `~/.cursor/hooks.json`. Sandcat bind-mounts -that file **read-only** from your host (`SANDCAT_MOUNT_CURSOR_CONFIG=true` -default) so cursor customizations are shared across all your sandboxes. -Because the mount is read-only, sandcat cannot install the rtk hook -into the container's copy of `hooks.json`. - -**Setup — run once on your host:** - -```bash -# Install rtk locally (needed once on the host) -brew install rtk # or: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/b34be37caf3796b69a50952a28e60e32b5daad43/install.sh | RTK_VERSION=v0.45.0 sh - -# Register the cursor hook in your host ~/.cursor/hooks.json -rtk init -g --hook-only --auto-patch --agent cursor -``` - -That writes the rtk hook to your host `~/.cursor/hooks.json`. Every -sandcat cursor sandbox from now on bind-mounts that file into the -container, so Cursor CLI sees the hook and calls `rtk hook cursor` on -each `Bash` tool invocation. The container's own `rtk` binary -(installed by sandcat) executes the hook — you never need rtk on the -host for anything except this one-time init step, and you can -uninstall it afterwards if you like. - -Bonus: the same host hook is picked up by every cursor sandbox you -start on that machine (and by host Cursor CLI, if you use it directly). - -**If you skip the host init:** the container prints a one-time warning -on start (`sandcat: rtk hook not found for cursor. Install rtk on your -host …`) and Cursor CLI runs without the hook. The rtk binary is still -on `PATH` inside the container, so you can invoke `rtk grep`, `rtk ls`, -etc. by hand. - -### Codex CLI (`--agent codex`) - -Sandcat installs [OpenAI's Codex CLI](https://github.com/openai/codex) -into every codex-agent sandbox and wires `OPENAI_API_KEY` through the -mitmproxy secret substitution layer. Codex reads its config from -`~/.codex/config.toml` (per-sandbox, agent-home volume) and picks up -the API key directly from the environment — no `codex login` required. - -**Setup:** - -```bash -sandcat init --agent codex --ide vscode -# Edit ~/.config/sandcat/settings.json — set secrets.OPENAI_API_KEY.value -sandcat run -codex "explain this codebase" -``` - -**Bash alias:** `codex-yolo` (= `codex --yolo`) is available in every -codex sandbox for parity with `claude-yolo`. - -**Host config sharing** (optional, default on): `~/.codex/AGENTS.md`, -`~/.codex/skills/`, and `~/.codex/commands/` are bind-mounted read-only -from the host into the container, matching how `~/.claude/` is handled. -The rest of `~/.codex/` (config.toml, credentials, history) lives in -the container's agent-home volume — per-sandbox persistent, per-sandbox -isolated. Opt out with `SANDCAT_MOUNT_CODEX_CONFIG=false`. - -**RTK integration:** works out of the box. On first container start, -sandcat seeds `~/.codex/AGENTS.md` (from the host bind-mount if -present) and runs `rtk init -g --codex` to write `~/.codex/RTK.md` -and add an `@RTK.md` reference to `AGENTS.md`. Idempotent: skipped -once the reference is already there. Disable with `--features -no-rtk` or `SANDCAT_RTK=false`. - -Note: because rtk needs to patch a writable `AGENTS.md`, sandcat -mounts the host's `~/.codex/AGENTS.md` at `~/.codex-host/AGENTS.md` -(a helper path) — the user-init step copies it into the writable -`~/.codex/AGENTS.md`. Host edits to `AGENTS.md` take effect after a -`docker compose down -v` (or manual rm inside). Skills and commands -directories are bind-mounted normally at `~/.codex/skills` and -`~/.codex/commands`, so those live-reload as usual. - -**Auth model:** first iteration supports `OPENAI_API_KEY` only. -ChatGPT sign-in (`chatgpt.com` / `auth.openai.com`) is not in the -default allowlist — users who want that flow can add the hosts to -`.sandcat/settings.local.json` and run `codex login` manually inside -the container. - -### GitHub Copilot CLI (`--agent copilot`) - -GitHub's [Copilot CLI](https://docs.github.com/copilot/how-tos/copilot-cli) -(`@github/copilot`) is available as a first-class sandcat agent. Sandcat installs -Node.js 22 and the Copilot package into every copilot-agent sandbox and wires -`COPILOT_GITHUB_TOKEN` through the mitmproxy secret substitution layer. - -**Setup:** - -```bash -sandcat init --agent copilot --ide vscode -# Edit ~/.config/sandcat/settings.json — set secrets.COPILOT_GITHUB_TOKEN.value -sandcat run -copilot "explain this codebase" -``` - -**Authentication:** Copilot CLI requires a GitHub token. Choose one of: - -1. **Fine-grained Personal Access Token (recommended):** Create a PAT at - [`https://github.com/settings/personal-access-tokens`](https://github.com/settings/personal-access-tokens) - with the **"Copilot Requests"** permission (Read and write). Then add it to - `~/.config/sandcat/settings.json`: - ```json - { - "secrets": { - "COPILOT_GITHUB_TOKEN": { - "value": "github_pat_...", - "hosts": ["api.github.com", "*.github.com", "*.githubcopilot.com", "*.githubusercontent.com"] - } - } - } - ``` - -2. **GitHub CLI OAuth token (quick setup):** If you already have `gh` CLI logged in, - run this once to write the token directly into `settings.json`: - ```bash - export TKN=$(gh auth token) - yq -i -o json '.secrets.COPILOT_GITHUB_TOKEN.value = strenv(TKN)' \ - ~/.config/sandcat/settings.json - ``` - -**Note:** Adding Node.js 22 and Copilot to the base image increases its size by -approximately 120 MB. The image is built once and cached locally; rebuilds are -fast. - -**VS Code integration:** When the IDE is `vscode`, the bundled `devcontainer.json` -includes the `GitHub.copilot` extension. Note that the VS Code extension -authenticates through VS Code's own GitHub sign-in (not the `COPILOT_GITHUB_TOKEN` -env var used by the CLI), so you may need to sign in the first time you open the -extension. - -**Placeholder:** Sandcat automatically sets the placeholder to -`gho_SANDCAT_PLACEHOLDER_COPILOT_GITHUB_TOKEN`. The container sees only the -placeholder; the real token is injected by mitmproxy only for allowed Copilot -hosts. No manual configuration is needed. - -**Bash alias:** `copilot-yolo` (= `copilot --yolo`) is available in every -copilot sandbox for parity with `claude-yolo` and `codex-yolo`. `--yolo` is -equivalent to `--allow-all-tools --allow-all-paths --allow-all-urls` — the -sandcat network isolation is the security boundary, so bypassing in-container -permission prompts is the intended workflow. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 4e085c5b..55a82925 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -18,29 +18,45 @@ curl -fsSL https://raw.githubusercontent.com/VirtusLab/sandcat/master/install.sh ``` This installs the CLI to `~/.local/share/sandcat/` with a launcher symlink at -`~/.local/bin/sandcat` — make sure `~/.local/bin` is on your `PATH`. A local -git clone works too: run `cli/bin/sandcat` from the checkout. +`~/.local/bin/sandcat` — make sure `~/.local/bin` is on your `PATH`. For all +install options (Docker image, git clone, uninstall), see +[Installation](../installation.md). ## 2. Initialize the sandbox for your project From (or pointing at) your project directory: ```bash -sandcat init --agent claude --ide vscode --stacks "python,node" --name myproject +sandcat init ``` -`init` copies the devcontainer templates into `.devcontainer/`, creates the -network policy in `.sandcat/settings.json`, and seeds user-level settings -(API-key placeholders, git identity) in `~/.config/sandcat/settings.json`. -Any flag you omit is prompted for interactively. +This prompts you to select the agent type, IDE (for devcontainer mode), and +development stacks to install, then copies the devcontainer templates into +`.devcontainer/`, creates the network policy in `.sandcat/settings.json`, and +seeds user-level settings (API-key placeholders, git identity) in +`~/.config/sandcat/settings.json`. Pass flags to skip the prompts: -Commonly used flags: +```bash +sandcat init --agent claude --ide vscode --stacks "python,node" + +# With optional features (proxy TUI, 1Password integration) +sandcat init --secret-provider 1password --agent claude --ide vscode +``` -- `--agent` — `claude`, `cursor`, `codex`, or `copilot` -- `--ide` — `vscode`, `jetbrains`, or `none` -- `--stacks` — comma-separated toolchains to install (`python`, `node`, - `java`, `rust`, `go`, `scala`, `ruby`, `dotnet`, `zig`) -- `--secret-provider` — `none` (default), `1password`, or `protonpass` +Available agents — see the **Agents** chapter for per-agent setup: +[`claude`](../agents/claude.md) (Claude Code), +[`cursor`](../agents/cursor.md) (Cursor CLI), +[`codex`](../agents/codex.md) (OpenAI Codex CLI), +[`copilot`](../agents/copilot.md) (GitHub Copilot CLI). + +Available stacks: `node`, `python`, `java`, `rust`, `go`, `scala`, `ruby`, +`dotnet`, `zig`. Versions default to LTS where available; selecting `scala` +automatically includes `java`, and stacks install the matching IDE +extension/plugin. Version overrides and extra tools go through +[devbox packages](../configuration/stacks.md); optional bind-mounts and +shared caches are covered under +[Volume mounts](../configuration/volume-mounts.md) and +[Shared dependency caches](../configuration/caches.md). ## 3. Add your API keys @@ -49,15 +65,40 @@ example `ANTHROPIC_API_KEY` (Claude Code) and `GITHUB_TOKEN` (git push, gh CLI). With a secret provider, use `op://` / `pass://` references instead of literal values. -## 4. Run +## 4. Start the sandbox + +**CLI mode:** + +```bash +# Open a shell in the agent container +sandcat run + +# Rebuild images first (after editing Dockerfile.app or scripts) +sandcat run --build + +# Start your agent cli (e.g. claude). Because you're in a sandbox, you can use yolo mode! +# (an alias for --dangerously-skip-permissions) +claude-yolo +``` + +**IDE mode:** reopen the project in [VS Code](../ide/vscode.md) or +[JetBrains](../ide/jetbrains.md) as a dev container. The first start builds +the agent image and brings up the proxy stack; subsequent starts are fast. + +**Attaching to a running container:** + +If the sandbox is already running (e.g. started by VS Code's devcontainer +integration or another terminal), use `attach` to open an additional shell in +it without starting a new container: ```bash -sandcat run # opens a shell inside the agent container +sandcat attach # opens bash --login +sandcat attach # runs directly, e.g. sandcat attach zsh ``` -or reopen the project in VS Code / JetBrains as a dev container. The first -start builds the agent image and brings up the proxy stack; subsequent starts -are fast. +Unlike `sandcat run`, this connects to an existing container rather than +starting a fresh one, and works reliably even when multiple sandboxes run in +parallel. Useful commands once running: diff --git a/docs/getting-started/running.md b/docs/getting-started/running.md deleted file mode 100644 index a8e2e0fb..00000000 --- a/docs/getting-started/running.md +++ /dev/null @@ -1,30 +0,0 @@ -# Starting the sandbox - -**CLI mode:** - -```bash -# Open a shell in the agent container -sandcat run - -# Rebuild images first (after editing Dockerfile.app or scripts) -sandcat run --build - -# Start your agent cli (e.g. claude). Because you're in a sandbox, you can use yolo mode! -# (an alias for --dangerously-skip-permissions) -claude-yolo -``` - -**Attaching to a running container:** - -If the sandbox is already running (e.g. started by VS Code's devcontainer integration or -another terminal), use `attach` to open an additional shell in it without starting a new -container: - -```bash -sandcat attach # opens bash --login -sandcat attach # runs directly, e.g. sandcat attach zsh -``` - -Unlike `sandcat run`, this connects to an existing container rather than starting a fresh one. -It uses `find_compose_file` to locate the correct project, so it works reliably even when -multiple sandboxes are running in parallel. diff --git a/docs/ide/jetbrains.md b/docs/ide/jetbrains.md new file mode 100644 index 00000000..8b5e39a6 --- /dev/null +++ b/docs/ide/jetbrains.md @@ -0,0 +1,82 @@ +# JetBrains + +Sandcat supports JetBrains IDEs through [JetBrains +Gateway](https://www.jetbrains.com/remote-development/gateway/) and its dev +containers integration: `sandcat init --ide jetbrains …` generates a +`devcontainer.json` with a `customizations.jetbrains` block instead of the +VS Code one. + +## Opening the sandbox + +In Gateway (or a JetBrains IDE with remote development), choose **Dev +Containers → From Local Project** and point it at the project's +`.devcontainer/devcontainer.json`. Gateway builds the images, starts the +compose stack, installs the backend IDE inside the agent container, and +connects a thin client to it. + +The generated block looks like: + +```json +"customizations": { + "jetbrains": { + "backend": "IntelliJ", + "plugins": ["org.intellij.scala"], + "settings": {} + } +} +``` + +- **`backend`** selects which JetBrains product runs inside the container. + `IntelliJ` is the default; edit it per project (e.g. `PyCharm`, `GoLand`). +- **`plugins`** is seeded from the selected stacks — see below. + +## Per-stack plugins + +Like the VS Code path adds one language extension per stack, the JetBrains +path seeds `customizations.jetbrains.plugins` with the stack's Marketplace +plugin ID, and Gateway installs the listed plugins into the backend IDE at +first start: + +| Stack | Plugin | Caveat | +|-------|--------|--------| +| `python` | `PythonCore` | free; works in IDEA Community | +| `scala` | `org.intellij.scala` | free | +| `go` | `org.jetbrains.plugins.go` | requires an **Ultimate** backend (or GoLand); silently skipped on Community | +| `ruby` | `org.jetbrains.plugins.ruby` | requires an **Ultimate** backend (or RubyMine); silently skipped on Community | +| `zig` | `com.falsepattern.zigbrains` | community-maintained | +| `node`, `java` | — | language support is bundled in IntelliJ IDEA | +| `dotnet` | — | JetBrains' .NET IDE is standalone Rider; no IntelliJ plugin exists | +| `rust` | — | Rust moved to standalone RustRover; the old plugin is discontinued | + +## `overrideCommand: false` — do not remove + +The generated `devcontainer.json` sets `overrideCommand: false`. This is a +**security-relevant** line, not a preference: JetBrains Gateway otherwise +replaces the container command and skips the sandcat entrypoint +(`app-init.sh`) — the step that installs the mitmproxy CA, adopts +wg-client's DNS, and loads the sandcat environment. With the entrypoint +skipped, processes in the container run without the sandbox's TLS and +policy wiring. Leave it in place. + +## Extra capabilities and mounts + +The JetBrains backend manages files it does not own (indexes, caches, +IDE state), so with `--ide jetbrains` the agent service additionally gets +`cap_add: [DAC_OVERRIDE, CHOWN, FOWNER]` in the generated compose file — +still combined with `no-new-privileges`, and still without `NET_ADMIN`, +so the network boundary is unaffected. + +Optionally, the host project's `.idea/` directory can be mounted read-only +into the workspace with `SANDCAT_MOUNT_IDEA_READONLY=true` at init time +(defaults to on for the JetBrains IDE path), so run configurations and code +styles carry over. See [Customizing optional volume +mounts](../configuration/volume-mounts.md). + +## Network + +The backend IDE downloads itself, plugins, and updates from JetBrains +servers — through the proxy, like all container traffic. The `jetbrains` +[network preset](../configuration/network-rules.md#network-presets) +(`plugins.jetbrains.com`, `downloads.marketplace.jetbrains.com`) covers the +plugin marketplace; on a tightened policy add the JetBrains download hosts +your setup needs. diff --git a/docs/security/vscode-hardening.md b/docs/ide/vscode.md similarity index 62% rename from docs/security/vscode-hardening.md rename to docs/ide/vscode.md index 3dd907e2..30673e3b 100644 --- a/docs/security/vscode-hardening.md +++ b/docs/ide/vscode.md @@ -1,4 +1,55 @@ -# Hardening the VS Code setup +# VS Code + +Sandcat's primary IDE path: the generated `.devcontainer/devcontainer.json` +is a standard [dev container](https://containers.dev) definition, so opening +the project in VS Code with the **Dev Containers** extension "just works". + +## Opening the sandbox + +After `sandcat init --ide vscode …`, open the project folder in VS Code and +choose **Reopen in Container** (or let the automatic prompt do it). VS Code +builds the agent image on first open, brings up the proxy stack via the +compose file referenced from `devcontainer.json`, and attaches to the agent +container as the `vscode` user. + +Integrated terminals are interactive shells, so they pick up everything the +sandbox environment provides — the sandcat env vars and secret placeholders +(via `/etc/profile.d` and the `/etc/bash.bashrc` sourcing block) and, with a +Java stack, `JAVA_HOME` / `JAVA_TOOL_OPTIONS` pointing at the +mitmproxy-aware trust store. + +## The `customizations.vscode` block + +`sandcat init` fills `customizations.vscode` in `devcontainer.json` with: + +- **Settings** that are part of the sandbox's security posture — see + [Security hardening](#security-hardening) below for what each one does and + why (workspace trust, `dev.containers.copyGitConfig`, credential-socket + cleanup). +- **Extensions**: the selected agent's extension (e.g. `GitHub.copilot` for + the Copilot agent) and one language extension per selected stack: + + | Stack | Extension | + |-------|-----------| + | `python` | `ms-python.python` | + | `java` | `redhat.java` | + | `rust` | `rust-lang.rust-analyzer` | + | `go` | `golang.go` | + | `scala` | `scalameta.metals` | + | `ruby` | `shopify.ruby-lsp` | + | `dotnet` | `ms-dotnettools.csdevkit` | + | `zig` | `ziglang.vscode-zig` | + + (`node` needs no extension — JavaScript/TypeScript support is built into + VS Code.) + +Extensions run inside the container, so their network traffic goes through +the proxy like everything else. If an extension needs extra hosts, add them +to the [network rules](../configuration/network-rules.md) — the `vscode` +[network preset](../configuration/network-rules.md#network-presets) covers +the marketplace and update endpoints. + +## Security hardening Sandcat secures the **network path** out of the container, but VS Code's dev container integration introduces a separate trust boundary. The VS Code remote @@ -10,7 +61,7 @@ For background on these attack vectors see [Leveraging VS Code Internals to Escape Containers](https://blog.theredguild.org/leveraging-vscode-internals-to-escape-containers/). -## What the bundled devcontainer.json already does +### What the bundled devcontainer.json already does The included `devcontainer.json` applies the following mitigations out of the box: @@ -53,7 +104,7 @@ box: configuration (entrypoint scripts, Dockerfile, compose files, devcontainer.json). -## Consequences of hardening +### Consequences of hardening Disabling credential forwarding and git config copying improves isolation but requires a few adjustments. diff --git a/docs/index.md b/docs/index.md index fb045f9b..76f3af01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,12 @@ # Sandcat: sandbox for securely running AI agents in "dangerous" mode Sandcat is a Docker & [dev container](https://containers.dev) setup for -securely running AI agents (Claude Code, Cursor CLI, Codex CLI, GitHub -Copilot CLI). The environment is sandboxed, with controlled network access +securely running AI agents ([Claude Code](agents/claude.md), +[Cursor CLI](agents/cursor.md), [Codex CLI](agents/codex.md), +[GitHub Copilot CLI](agents/copilot.md)). The environment is sandboxed, with controlled network access and transparent secret substitution — while retaining the convenience of -working in an IDE like VS Code or JetBrains. +working in an IDE — both [VS Code](ide/vscode.md) and +[JetBrains](ide/jetbrains.md) are supported. All container traffic is routed through a transparent [mitmproxy](https://mitmproxy.org/) via WireGuard, capturing HTTP/S, DNS, and @@ -23,12 +25,31 @@ VirtusLab's AI-native SDLC platform. ```{eval-rst} .. toctree:: :maxdepth: 2 - :caption: Getting started getting-started/quickstart - getting-started/installation - getting-started/initialization - getting-started/running + +.. toctree:: + :maxdepth: 2 + :caption: Agents + + agents/claude + agents/cursor + agents/codex + agents/copilot + agents/rtk + +.. toctree:: + :maxdepth: 2 + :caption: IDE integration + + ide/vscode + ide/jetbrains + +.. toctree:: + :maxdepth: 1 + :caption: Installation + + installation .. toctree:: :maxdepth: 2 @@ -40,13 +61,16 @@ VirtusLab's AI-native SDLC platform. configuration/dns configuration/secrets configuration/generated-files + configuration/stacks + configuration/volume-mounts + configuration/caches + configuration/gitignore .. toctree:: :maxdepth: 2 - :caption: Architecture & security + :caption: Architecture architecture/overview - security/vscode-hardening .. toctree:: :maxdepth: 2 diff --git a/docs/getting-started/installation.md b/docs/installation.md similarity index 93% rename from docs/getting-started/installation.md rename to docs/installation.md index 5d7cd324..a6283972 100644 --- a/docs/getting-started/installation.md +++ b/docs/installation.md @@ -1,6 +1,6 @@ -# Installing the sandcat CLI +# Installation -The [CLI](../reference/cli.md) is a helper script and thin wrapper around +The [CLI](reference/cli.md) is a helper script and thin wrapper around docker-compose that simplifies the process of initializing and starting the sandbox. @@ -22,7 +22,7 @@ curl -fsSL https://raw.githubusercontent.com/VirtusLab/sandcat/master/install.sh ``` Ensure `~/.local/bin` is on your `PATH` (the installer prints a hint if it -isn't), then jump to [Initialize the sandbox](quickstart.md#2-initialize-the-sandbox-for-your-project). +isn't), then jump to [Initialize the sandbox](getting-started/quickstart.md#2-initialize-the-sandbox-for-your-project). **Upgrade:** re-run the same command. The installer atomically swaps the existing install; `~/.config/sandcat/` (user settings) is never touched. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 892bc219..10d4c7d1 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4,7 +4,7 @@ Command-line tool for managing sandcat configurations and Docker Compose setups. Requires `docker` (and `docker compose`) and [`yq`](https://github.com/mikefarah/yq). -See [Installing the sandcat CLI](../getting-started/installation.md) for install +See [Installing the sandcat CLI](../installation.md) for install options: Docker image, shell installer (`curl … | sh`), or local git clone. @@ -131,7 +131,7 @@ Options: Note: Cursor agent support uses placeholder-based API key substitution and Sandcat-managed CLI settings (`cursor.cli` in settings — permissions, model, network flags). Put the API key in `secrets.CURSOR_API_KEY`, not in -`cursor.cli`. See the [Cursor CLI section](../configuration/secrets.md#cursor-cli) for details. +`cursor.cli`. See the [Cursor CLI page](../agents/cursor.md) for details. #### `sandcat init settings` @@ -157,7 +157,7 @@ Runs docker compose commands with the correct compose file automatically detecte ### `sandcat cache` Manages the host-scoped shared dependency-cache volumes (`sandcat-cache-*`) -that back the shared-cache feature (see [Shared dependency caches](../getting-started/initialization.md#shared-dependency-caches)). Bare `sandcat cache` is a shorthand for `sandcat cache list`. +that back the shared-cache feature (see [Shared dependency caches](../configuration/caches.md)). Bare `sandcat cache` is a shorthand for `sandcat cache list`. Subcommands: @@ -250,7 +250,7 @@ except provider config mounts, which default to `true` for the selected agent. Per-folder mounts are **not** separate init flags — tune individual paths by editing `.devcontainer/compose-all.yml` after init. See the main -[Customizing optional volume mounts](../getting-started/initialization.md#customizing-optional-volume-mounts). +[Customizing optional volume mounts](../configuration/volume-mounts.md). - `SANDCAT_MOUNT_CLAUDE_CONFIG` - `true` to mount host `~/.claude` config (Claude agent only) - `SANDCAT_MOUNT_CURSOR_CONFIG` - `true` to mount host `~/.cursor` customization