diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a8821..3a9fc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to aidc are tracked here. Format follows [Keep a Changelog]( ### Added +- **Grok Build agent.** `aidc grok` launches xAI's Grok Build CLI inside the container, with a `grok_home` named volume (`~/.grok`), a read-only `/host-seed/grok` seed mount, `sync_grok` seeding in `bootstrap-state.sh`, and `aidc sync-config`/`sync-sessions` support. Grok reads the same generated `AGENTS.md`. - **GPL-3.0 license.** `LICENSE` at repo root. - **Vulnerability disclosure policy.** `SECURITY.md` documents scope, reporting channels, and timelines. - **Shellcheck CI.** `.github/workflows/shellcheck.yml` lints `bin/aidc`, `install.sh`, `lib/aidc.sh`, and `templates/**/*.sh`, plus syntax-checks the Python clipboard server. @@ -22,6 +23,8 @@ All notable changes to aidc are tracked here. Format follows [Keep a Changelog]( ### Changed +- **Coding agents now install as native prebuilt binaries.** `claude`, `codex`, and `opencode` switched from a root `npm install -g` to each vendor's native `curl | sh` installer (into `~/.local/bin`), dropping the Node runtime dependency for the agents and shrinking the npm supply-chain surface. Removes the only build step that ran package-manager installs *before* `pmg setup install`. +- **pmg wired in before any user-level install.** `pmg setup install` is now the first `USER vscode` build step. Interception is documented as riding on the `~/.pmg/bin` PATH shims (first on `ENV PATH`) rather than the rc-file aliases pmg also writes — build `RUN` steps and exec'd agents never source rc files. `docs/security.md` now describes the shared-credentials seed-mount/env-passthrough matrix. - **Pinned `gitleaks` and `vet` versions** in `templates/devcontainer/Dockerfile.tmpl` (`v8.30.1` and `v1.17.3` respectively). Defaults were `latest`, which resolved at build time and defeated the surrounding base-image SHA pin. Repin instructions are in the Dockerfile. - **README** and the docs/ tree brought into sync with the above. README's command list includes `aidc status`, `aidc down`, `aidc destroy`, `aidc exec`, and `aidc sync-sessions`. `docs/security.md` covers the scanners, supply-chain guardrails, and egress firewall; `docs/claude-profiles.md` covers the local-model profiles (`localhost.env.example`, `localnetwork.env.example`). diff --git a/README.md b/README.md index e2c7f22..0d6e4d5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#status) [![macOS only](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](#prereqs) -**aidc** — short for **AI Dev Container** — is a one-command devcontainer wrapper for AI coding agents (`claude`, `codex`, `opencode`, `cursor-agent`). It scaffolds a hardened Linux container per repo, mounts your code at `/workspace`, persists agent state in named Docker volumes (so agents don't read your `~/.ssh` or your shell history), and bakes in always-on security scanners and supply-chain guardrails. +**aidc** — short for **AI Dev Container** — is a one-command devcontainer wrapper for AI coding agents (`claude`, `codex`, `opencode`, `grok`, `cursor-agent`). It scaffolds a hardened Linux container per repo, mounts your code at `/workspace`, persists agent state in named Docker volumes (so agents don't read your `~/.ssh` or your shell history), and bakes in always-on security scanners and supply-chain guardrails. If you're already running these agents directly on your Mac and have been quietly nervous about it, this is for you. @@ -37,12 +37,13 @@ aidc init # one-time scaffold; writes .devcontainer/, .ai-container/, C aidc claude # auto-runs `aidc up` if needed, then drops you into Claude Code in the container ``` -Tool commands (`aidc claude` / `codex` / `opencode` / `cursor-agent`) auto-bootstrap the container on first run. +Tool commands (`aidc claude` / `codex` / `opencode` / `grok` / `cursor-agent`) auto-bootstrap the container on first run. ## What aidc actually does - creates local-only `.devcontainer/`, `.ai-container/`, `CLAUDE.md`, `AGENTS.md`, and `.cursor/rules/00-core-logics.mdc` - mounts project code only at `/workspace`; overlays `/workspace/.devcontainer` read-only inside the container +- installs the coding agents (`claude`, `codex`, `opencode`, `grok`) as native prebuilt binaries — no npm-global, no Node runtime dependency for the agents themselves - persists tool state in per-repo Docker volumes instead of mounting whole host homes - seeds selected config from host read-only mounts on first startup - creates one `CORE_LOGICS` git worktree per repo and mounts it at `/opt/CORE_LOGICS` for shared cross-repo notes @@ -75,11 +76,12 @@ aidc exec -- ... aidc claude [--profile NAME] [--provider NAME] [--list-profiles] [-- ...] aidc codex [-- ...] aidc opencode [-- ...] +aidc grok [-- ...] aidc cursor-agent [-- ...] aidc cursor aidc sync-claude-aliases -aidc sync-config -aidc sync-sessions [claude|codex|opencode|all] +aidc sync-config +aidc sync-sessions [claude|codex|opencode|grok|all] ``` `aidc status` shows the container + mounts/config for the current folder. `--global` lists every aidc container on the host with disk/CPU/memory and a totals line. diff --git a/docs/install.md b/docs/install.md index 80954d1..3af50f5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -36,6 +36,7 @@ aidc up # build + start container | `aidc claude --profile ` | start Claude against a host-defined profile | | `aidc codex` | start OpenAI Codex | | `aidc opencode` | start OpenCode | +| `aidc grok` | start Grok Build | | `aidc cursor-agent` | start Cursor Agent | | `aidc cursor` | open host Cursor on the repo | | `aidc status` | container + config/mounts status for this folder | @@ -53,8 +54,9 @@ aidc up # build + start container /home/vscode/.claude Claude state (named volume) /home/vscode/.codex Codex state (named volume) /home/vscode/.config/opencode OpenCode state (named volume) +/home/vscode/.grok Grok state (named volume) /commandhistory bash + zsh history (named volume) -/host-seed/{claude,codex,opencode,gitconfig} read-only host seeds +/host-seed/{claude,codex,opencode,grok,gitconfig} read-only host seeds ``` `GIT_CONFIG_GLOBAL=/home/vscode/.gitconfig.local` — host gitconfig is seed-only, in-container `git config --global` writes land in the overlay (ephemeral across rebuilds). @@ -100,7 +102,7 @@ sudo apt-get install -y --no-install-recommends golang-go go install golang.org/x/tools/gopls@latest ``` -Runs as `vscode` at image build time, with passwordless `sudo` available for system packages. The `COPY` is the last layer in the Dockerfile, so edits invalidate **only** the project-setup layer — the heavy base layers (apt, uv/Python, npm globals, pmg/vet/gryph/rtk) stay cached. +Runs as `vscode` at image build time, with passwordless `sudo` available for system packages. The `COPY` is the last layer in the Dockerfile, so edits invalidate **only** the project-setup layer — the heavy base layers (apt, uv/Python, native agent binaries, pmg/vet/gryph/rtk) stay cached. After editing, `aidc rebuild` (or just `aidc up` — `--build` is implicit) picks up the change. diff --git a/docs/security.md b/docs/security.md index c39dd2d..76eed8f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -41,12 +41,45 @@ The scaffold writes a "Security guardrails (non-negotiable)" block into `CLAUDE. ## Supply-chain guardrails (always on) -The container ships with SafeDep's [`pmg`](https://github.com/safedep/pmg) and [`vet`](https://github.com/safedep/vet) baked in. `pmg setup install` runs at image build, so `~/.pmg/bin` is first on `PATH` and intercepts `npm`, `pnpm`, `yarn`, `bun`, `npx`, `pnpx`, `pip`, `pip3`, `uv`, and `poetry` — including subprocess calls from agents (Claude, Codex, OpenCode, Cursor Agent). Malicious packages are blocked before install. +The container ships with SafeDep's [`pmg`](https://github.com/safedep/pmg) and [`vet`](https://github.com/safedep/vet) baked in. `pmg setup install` runs at image build **before any user-level package install**, and interception rides on the `~/.pmg/bin` PATH shims — which are first on the image `ENV PATH` for both build and runtime. It is deliberately **not** dependent on the shell aliases pmg also writes to `~/.zshrc`/`~/.bashrc`: Docker build `RUN` steps and exec'd agent subprocesses never source rc files, so only the PATH shims reliably gate package managers. The shims intercept `npm`, `pnpm`, `yarn`, `bun`, `npx`, `pnpx`, `pip`, `pip3`, `uv`, and `poetry` — including subprocess calls from agents (Claude, Codex, OpenCode, Grok, Cursor Agent). Malicious packages are blocked before install. -Run scans by hand with `aidc exec -- vet scan -D /workspace`. Re-run `pmg setup doctor` inside the container to verify wiring (`aidc exec -- pmg setup doctor`). +The coding agents themselves are installed as **native prebuilt binaries** (via each vendor's `curl | sh` installer), not `npm install -g`, so there is no agent-install step for pmg to vet and no Node runtime dependency for the agents. The `NPM_CONFIG_*` hardening in the image still governs any npm the agents or project toolchains invoke at runtime, which the pmg shims gate. + +Run scans by hand with `aidc exec -- vet scan -D /workspace`. Re-run `pmg setup doctor` inside the container to verify wiring (`aidc exec -- pmg setup doctor`). To confirm interception is alias-independent, check that the shim wins without sourcing rc files: `aidc exec -- bash -c 'command -v npm'` should resolve under `~/.pmg/bin`. If the egress firewall is enabled, the allowlist already includes `api.safedep.io`, `vetpkg.dev`, `osv.dev`, and `semgrep.dev`. +## Sharing credentials with the agents + +aidc never mounts your whole host home into the container. Instead each agent's +config/auth is shared two ways, both scoped to that agent: + +**1. Read-only seed mounts (the default path).** On first startup +`bootstrap-state.sh init` copies selected files from host config dirs (mounted +read-only at `/host-seed/`) into the agent's per-repo volume: + +| Agent | Host source | Container volume | Seeded by | +|---|---|---|---| +| Claude | `~/.claude` | `~/.claude` | `settings.json`, `CLAUDE.md` | +| Codex | `~/.codex` | `~/.codex` | `auth.json`, `config.toml`, `AGENTS.md`, `rules/`, `skills/` | +| OpenCode | `~/.config/opencode` | `~/.config/opencode` | `opencode.json`, `plugins/` | +| Grok | `~/.grok` | `~/.grok` | `config.toml` / `user-settings.json` / `auth.json` (whichever exists) | + +Re-sync after changing host config with `aidc sync-config `. +Because the seed is read-only and only specific files are copied, the agents +reuse your existing logins without the container being able to write back to the +host. After interactive login *inside* the container, credentials persist in the +named volume across restarts (and are wiped by `aidc destroy`). + +**2. Environment-variable passthrough.** For headless/API-key auth, `aidc` +forwards a fixed set of host env vars into the agent process when present: +`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `OPENAI_API_KEY`, +`CURSOR_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_BASE_URL`. OpenCode and Grok can +both speak to multiple providers, so these shared keys let all the agents reuse +the same host credentials. For xAI specifically, either log in interactively +(persisted in `~/.grok`) or export the xAI key on the host and add it to the +passthrough list in `AIDC_PASSTHROUGH_ENV_KEYS` (`lib/aidc.sh`) before launching. + ## Agent guardrails: gryph + rtk The image also ships [`gryph`](https://github.com/safedep/gryph) (SafeDep's agent security layer — patches hook entries into the detected agents' settings) and [`rtk`](https://github.com/rtk-ai/rtk) (Rust Token Killer — token-saving CLI proxy that rewrites commands like `git status` → `rtk git status` via the Claude Code hook). diff --git a/lib/aidc.sh b/lib/aidc.sh index c0dc646..1103e39 100644 --- a/lib/aidc.sh +++ b/lib/aidc.sh @@ -81,6 +81,9 @@ aidc::main() { opencode) aidc::cmd_opencode "$@" ;; + grok) + aidc::cmd_grok "$@" + ;; cursor-agent) aidc::cmd_cursor_agent "$@" ;; @@ -121,11 +124,12 @@ Usage: aidc claude [--profile NAME] [--provider NAME] [--list-profiles] [-- ...] aidc codex [-- ...] aidc opencode [-- ...] + aidc grok [-- ...] aidc cursor-agent [-- ...] aidc cursor aidc sync-claude-aliases - aidc sync-config - aidc sync-sessions [claude|codex|opencode|all] + aidc sync-config + aidc sync-sessions [claude|codex|opencode|grok|all] Notes: - Run commands from the repo root you want to isolate. @@ -385,6 +389,7 @@ aidc::status_config() { aidc::status_mount_row "/host-seed/claude" "$AIDC_HOST_SEED_CLAUDE" "$c_dim" "$c_rst" aidc::status_mount_row "/host-seed/codex" "$AIDC_HOST_SEED_CODEX" "$c_dim" "$c_rst" aidc::status_mount_row "/host-seed/opencode" "$AIDC_HOST_SEED_OPENCODE" "$c_dim" "$c_rst" + aidc::status_mount_row "/host-seed/grok" "$AIDC_HOST_SEED_GROK" "$c_dim" "$c_rst" aidc::status_mount_row "/host-seed/gitconfig" "$AIDC_GITCONFIG_SOURCE" "$c_dim" "$c_rst" aidc::status_mount_row "/host-clipboard" "$AIDC_CLIPBOARD_DIR_SOURCE" "$c_dim" "$c_rst" @@ -689,6 +694,10 @@ aidc::cmd_opencode() { aidc::run_tool "opencode" "" "$@" } +aidc::cmd_grok() { + aidc::run_tool "grok" "" "$@" +} + aidc::cmd_cursor_agent() { aidc::run_tool "cursor-agent" "" "$@" } @@ -713,7 +722,7 @@ aidc::cmd_sync_config() { local workspace workspace="$(aidc::default_workspace)" local tool="${1:-}" - [[ -n "$tool" ]] || aidc::die "usage: aidc sync-config " + [[ -n "$tool" ]] || aidc::die "usage: aidc sync-config " aidc::ensure_container_running "$workspace" aidc::compose "$workspace" exec workspace /workspace/.devcontainer/scripts/bootstrap-state.sh sync "$tool" aidc::log "synced $tool config into the container volume" @@ -726,14 +735,15 @@ aidc::cmd_sync_sessions() { aidc::ensure_container_running "$workspace" case "$tool" in - claude|codex|opencode|all) ;; - *) aidc::die "usage: aidc sync-sessions [claude|codex|opencode|all]" ;; + claude|codex|opencode|grok|all) ;; + *) aidc::die "usage: aidc sync-sessions [claude|codex|opencode|grok|all]" ;; esac if [[ "$tool" == "all" ]]; then aidc::sync_session_tool "$workspace" claude aidc::sync_session_tool "$workspace" codex aidc::sync_session_tool "$workspace" opencode + aidc::sync_session_tool "$workspace" grok else aidc::sync_session_tool "$workspace" "$tool" fi @@ -757,6 +767,10 @@ aidc::sync_session_tool() { container_src="/home/vscode/.config/opencode/projects" host_dst="$HOME/.config/opencode/projects" ;; + grok) + container_src="/home/vscode/.grok/sessions" + host_dst="$HOME/.grok/sessions" + ;; *) aidc::die "unknown session tool: $tool" ;; @@ -804,6 +818,12 @@ aidc::run_tool() { opencode) command=("opencode") ;; + grok) + # The container is already the isolation boundary; grok runs unsandboxed + # like the other agents. Grok Build has operating modes (e.g. plan/auto); + # append the full-autonomy mode flag here once confirmed against the CLI. + command=("grok") + ;; cursor-agent) command=("cursor-agent" "--sandbox" "disabled" "-f") ;; @@ -1510,7 +1530,7 @@ aidc::ensure_core_worktree() { aidc::ensure_host_config_dirs() { mkdir -p "$AIDC_HOST_CONFIG_ROOT" "$AIDC_EMPTY_ROOT" "$AIDC_CLAUDE_PROFILE_ROOT" - mkdir -p "$AIDC_EMPTY_ROOT/claude" "$AIDC_EMPTY_ROOT/codex" "$AIDC_EMPTY_ROOT/opencode" "$AIDC_EMPTY_ROOT/clipboard" + mkdir -p "$AIDC_EMPTY_ROOT/claude" "$AIDC_EMPTY_ROOT/codex" "$AIDC_EMPTY_ROOT/opencode" "$AIDC_EMPTY_ROOT/grok" "$AIDC_EMPTY_ROOT/clipboard" touch "$AIDC_EMPTY_ROOT/gitconfig" } @@ -1609,6 +1629,8 @@ aidc::export_compose_env() { AIDC_HOST_SEED_CODEX="$(aidc::mount_dir_or_empty "$HOME/.codex" "codex")" export AIDC_HOST_SEED_OPENCODE AIDC_HOST_SEED_OPENCODE="$(aidc::mount_dir_or_empty "$HOME/.config/opencode" "opencode")" + export AIDC_HOST_SEED_GROK + AIDC_HOST_SEED_GROK="$(aidc::mount_dir_or_empty "$HOME/.grok" "grok")" export AIDC_GITCONFIG_SOURCE AIDC_GITCONFIG_SOURCE="$(aidc::mount_file_or_empty "$HOME/.gitconfig" "gitconfig")" # Host-clipboard bridge is opt-in (off by default). When disabled, mount an diff --git a/templates/devcontainer/Dockerfile.tmpl b/templates/devcontainer/Dockerfile.tmpl index f806256..c47f458 100644 --- a/templates/devcontainer/Dockerfile.tmpl +++ b/templates/devcontainer/Dockerfile.tmpl @@ -68,8 +68,10 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=true \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ NODE_OPTIONS=--max-old-space-size=4096 -RUN npm_config_ignore_scripts=false npm install -g @anthropic-ai/claude-code @openai/codex opencode-ai@latest \ - && npm cache clean --force +# Coding agents are installed as native prebuilt binaries per-user below (no +# npm-global, no Node runtime dependency for the agents themselves). The +# NPM_CONFIG_* hardening above still governs any npm the agents or project +# toolchains invoke at runtime. # Supply-chain guardrails: pmg intercepts npm/pip/uv/etc. and runs vet checks # before installs. Binaries installed system-wide; shell/shim wiring done @@ -187,18 +189,34 @@ RUN chmod 0755 /usr/local/bin/aidc-bootstrap-claude USER vscode ENV HOME=/home/vscode +# ~/.pmg/bin is FIRST on PATH on purpose. pmg interception rides on these PATH +# shims, NOT on the shell aliases pmg also writes to ~/.zshrc/~/.bashrc: build +# RUN steps and exec'd agent subprocesses never source rc files, so only the +# shims (first on PATH at build and runtime) reliably gate npm/pip/uv/etc. ENV PATH=/home/vscode/.pmg/bin:/home/vscode/.local/bin:/usr/local/bin:/usr/bin:/bin +# Wire pmg in before ANY user-level package install so nothing slips past it. +RUN pmg setup install \ + && pmg setup doctor || true + +# Coding agents as native prebuilt binaries. Each installer drops a binary into +# ~/.local/bin (already on PATH) via plain curl — no npm dependency resolution, +# so pmg has nothing to vet here; the "pmg first" invariant above still holds. +# Grok's binary is forced into ~/.local/bin via GROK_BIN_DIR so the grok_home +# volume mounted at ~/.grok at runtime cannot shadow it. RUN mkdir -p \ /home/vscode/.local/bin \ /home/vscode/.claude \ /home/vscode/.codex \ /home/vscode/.config/opencode \ /home/vscode/.cursor-agent \ + /home/vscode/.grok \ + && curl -fsSL https://claude.ai/install.sh | bash \ + && curl -fsSL https://chatgpt.com/codex/install.sh | sh \ + && curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path \ && curl -fsS https://cursor.com/install | bash \ && { cursor-agent --version || true; } \ - && pmg setup install \ - && pmg setup doctor || true + && curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR=/home/vscode/.local/bin bash # Auto-detected toolchains. aidc fills AIDC_TOOLCHAINS by scanning the # workspace for marker files (go.mod, Cargo.toml, etc.). Override per repo diff --git a/templates/devcontainer/compose.yaml.tmpl b/templates/devcontainer/compose.yaml.tmpl index 458fd05..cc520fe 100644 --- a/templates/devcontainer/compose.yaml.tmpl +++ b/templates/devcontainer/compose.yaml.tmpl @@ -49,6 +49,10 @@ services: source: ${AIDC_HOST_SEED_OPENCODE} target: /host-seed/opencode read_only: true + - type: bind + source: ${AIDC_HOST_SEED_GROK} + target: /host-seed/grok + read_only: true - type: bind source: ${AIDC_GITCONFIG_SOURCE} target: /host-seed/gitconfig @@ -72,6 +76,9 @@ services: - type: volume source: cursor_agent_home target: /home/vscode/.cursor-agent + - type: volume + source: grok_home + target: /home/vscode/.grok volumes: command_history: @@ -79,3 +86,4 @@ volumes: codex_home: opencode_home: cursor_agent_home: + grok_home: diff --git a/templates/devcontainer/scripts/bootstrap-state.sh.tmpl b/templates/devcontainer/scripts/bootstrap-state.sh.tmpl index 437c733..1005809 100644 --- a/templates/devcontainer/scripts/bootstrap-state.sh.tmpl +++ b/templates/devcontainer/scripts/bootstrap-state.sh.tmpl @@ -66,6 +66,16 @@ sync_opencode() { copy_dir_from_seed "/host-seed/opencode/plugins" "$home_dir/.config/opencode/plugins" } +sync_grok() { + ensure_dir "$home_dir/.grok" + # Grok stores config + credentials under ~/.grok. Filenames differ across + # builds (config.toml / user-settings.json / an auth file), so copy whichever + # the host provides — copy_file_from_seed is a no-op when the source is absent. + copy_file_from_seed "/host-seed/grok/config.toml" "$home_dir/.grok/config.toml" "0600" + copy_file_from_seed "/host-seed/grok/user-settings.json" "$home_dir/.grok/user-settings.json" "0600" + copy_file_from_seed "/host-seed/grok/auth.json" "$home_dir/.grok/auth.json" "0600" +} + sync_gitconfig_overlay() { local overlay="$home_dir/.gitconfig.local" if [[ ! -f "$overlay" && -f /host-seed/gitconfig ]]; then @@ -132,10 +142,14 @@ sync_tool() { opencode) sync_opencode ;; + grok) + sync_grok + ;; all) sync_claude sync_codex sync_opencode + sync_grok ;; *) echo "unsupported tool: $1" >&2 @@ -157,7 +171,7 @@ case "$mode" in sync_tool "$tool" ;; *) - echo "usage: bootstrap-state.sh [init|sync] [claude|codex|opencode|all]" >&2 + echo "usage: bootstrap-state.sh [init|sync] [claude|codex|opencode|grok|all]" >&2 exit 1 ;; esac