From 48248aa4a8217f75425e5612f55b1fe7a33ffd6f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 23:08:50 -0700 Subject: [PATCH 01/51] X-Smart-Branch-Parent: master From 6191758b2c1a1ec8324e581fec3111162634d557 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 14:26:57 -0700 Subject: [PATCH 02/51] feat: add devcontainer and Claude Code agent development environment Add a devcontainer based on the collector-builder image that enables agent-driven development of collector. The devcontainer includes all C++ build dependencies, Go, Node.js, Claude Code, gcloud CLI, and developer tooling (ripgrep, fd, gh). Verified: cmake configure, full collector build, and 17/17 unit tests pass inside the container. Claude Code authenticates to Vertex AI via read-only gcloud credential mount. - .devcontainer/: Dockerfile, devcontainer.json, network firewall - CLAUDE.md: agent development guide with build/test workflows - .claude/skills/: /build, /ci-status, /iterate slash commands - .claude/settings.json: deny Read(.devcontainer/**) for security - Security: bubblewrap sandboxing, npm hardening, read-only mounts, optional iptables firewall with NET_ADMIN Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 7 ++ .claude/skills/build/SKILL.md | 44 ++++++++ .claude/skills/ci-status/SKILL.md | 41 +++++++ .claude/skills/iterate/SKILL.md | 43 ++++++++ .devcontainer/Dockerfile | 82 ++++++++++++++ .devcontainer/devcontainer.json | 69 ++++++++++++ .devcontainer/init-firewall.sh | 75 +++++++++++++ .gitignore | 2 - CLAUDE.md | 177 ++++++++++++++++++++++++++++++ 9 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .claude/skills/build/SKILL.md create mode 100644 .claude/skills/ci-status/SKILL.md create mode 100644 .claude/skills/iterate/SKILL.md create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/init-firewall.sh create mode 100644 CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..a6a3d98bd1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "deny": [ + "Read(.devcontainer/**)" + ] + } +} diff --git a/.claude/skills/build/SKILL.md b/.claude/skills/build/SKILL.md new file mode 100644 index 0000000000..713bd1d6ea --- /dev/null +++ b/.claude/skills/build/SKILL.md @@ -0,0 +1,44 @@ +--- +name: build +description: Build collector binary with options (debug, asan, tsan, clean) +tags: [collector, build, cmake, cpp] +--- + +# Build Collector + +Build the collector binary. Supports optional arguments: +- `debug` — Debug build with symbols +- `asan` — AddressSanitizer build +- `tsan` — ThreadSanitizer build +- `clean` — Clean build directory first +- (no args) — Release build + +## Steps + +1. Determine build environment: + - If inside the devcontainer (check: `/usr/local/bin/cmake` exists and we're on Linux), run cmake directly. + - If on the host (macOS), use `make start-builder && make collector`. + +2. If `clean` argument is provided, remove `cmake-build/` directory first. + +3. Set build variables based on arguments: + - `debug`: `CMAKE_BUILD_TYPE=Debug` + - `asan`: `CMAKE_BUILD_TYPE=Debug`, `ADDRESS_SANITIZER=ON` + - `tsan`: `CMAKE_BUILD_TYPE=Debug`, `THREAD_SANITIZER=ON` + - default: `CMAKE_BUILD_TYPE=Release` + +4. Run cmake configure (if `cmake-build/` doesn't exist or CMakeLists.txt changed): + ```bash + cmake -S . -B cmake-build \ + -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE \ + -DADDRESS_SANITIZER=$ADDRESS_SANITIZER \ + -DTHREAD_SANITIZER=$THREAD_SANITIZER \ + -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) + ``` + +5. Run cmake build: + ```bash + cmake --build cmake-build -- -j$(nproc) + ``` + +6. Report result: success with binary size, or failure with the first error and its file:line. diff --git a/.claude/skills/ci-status/SKILL.md b/.claude/skills/ci-status/SKILL.md new file mode 100644 index 0000000000..b925102f71 --- /dev/null +++ b/.claude/skills/ci-status/SKILL.md @@ -0,0 +1,41 @@ +--- +name: ci-status +description: Check CI status on current PR, fetch failure logs, diagnose issues +tags: [collector, ci, github, testing] +--- + +# CI Status + +Check CI pipeline status for the current branch/PR and diagnose failures. + +## Steps + +1. Get the current branch and check for an open PR: + ```bash + gh pr view --json number,title,url,statusCheckRollup,headRefName + ``` + If no PR exists, report that and suggest pushing or creating one. + +2. Parse the check results. Group by status: + - Passed checks + - Failed checks (show full names) + - Pending/running checks + +3. For any **failed checks**: + - Identify which workflow failed (unit-tests, integration-tests, k8s-integration-tests, benchmarks, lint) + - Get the failed job logs: + ```bash + gh run view --log-failed 2>&1 | tail -100 + ``` + - For integration test failures, identify: + - Which VM type failed (rhel, ubuntu, cos, flatcar, etc.) + - Which test suite failed (e.g., TestProcessNetwork, TestConnectionsAndEndpoints) + - The relevant error message + +4. **Diagnose** the failure: + - If a unit test failed: show the failing assertion and the relevant source file + - If an integration test failed: identify if it's a test infrastructure issue (VM creation, timeout) vs an actual test failure + - If lint failed: show which files need formatting + - If build failed: show the compiler error with file:line + +5. **Suggest next steps**: what code changes would fix the failure, or if it's a flaky test / infra issue. diff --git a/.claude/skills/iterate/SKILL.md b/.claude/skills/iterate/SKILL.md new file mode 100644 index 0000000000..da8cfa93eb --- /dev/null +++ b/.claude/skills/iterate/SKILL.md @@ -0,0 +1,43 @@ +--- +name: iterate +description: Full development cycle — build, unit test, format check, commit, push +tags: [collector, build, test, workflow] +--- + +# Iterate + +Run the full development inner loop. Stops at the first failure. + +## Steps + +1. **Build** the collector: + - Detect environment (devcontainer vs host) + - In devcontainer: `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` + - On host: `make collector` + - **Stop on failure** — report the compiler error with file:line. + +2. **Unit test**: + - In devcontainer: `ctest --no-tests=error -V --test-dir cmake-build` + - On host: `make unittest` + - **Stop on failure** — report which test failed and the assertion. + +3. **Format check** (C++ files changed in this branch only): + - Get changed C++ files: `git diff --name-only origin/master...HEAD | grep -E '\.(cpp|h)$'` + - Run: `clang-format --style=file -n --Werror ` + - If formatting issues found, auto-fix them: `clang-format --style=file -i ` + - Report what was fixed. + +4. **Commit**: + - Stage changed files (source + any format fixes) + - Create a commit with a descriptive message summarizing the changes + - Follow the repository's commit conventions + +5. **Push**: + - Push to the current branch + - If a PR exists, report the PR URL + - If no PR exists, suggest creating one + +6. **Report**: + - Summary: built, N tests passed, formatted M files, pushed to branch X + - Link to PR if it exists + - Note: CI will run integration tests on the PR — use `/ci-status` to check results later diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..f0d3537f69 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,82 @@ +# Collector development container +# Based on the collector-builder image which has all C++ dependencies pre-installed. +# Adds Claude Code, Go, and developer tooling for agent-driven development. +# +# Build environment: CentOS Stream 10 with clang, llvm, cmake, grpc, protobuf, +# libbpf, bpftool, and all other collector dependencies. + +ARG COLLECTOR_BUILDER_TAG=master +FROM quay.io/stackrox-io/collector-builder:${COLLECTOR_BUILDER_TAG} + +# Install developer tooling not in the builder image +# Note: git, findutils, which, openssh-clients already in builder +# bubblewrap: Claude Code uses this for built-in command sandboxing +RUN dnf install -y \ + bubblewrap \ + jq \ + socat \ + zsh \ + procps-ng \ + sudo \ + python3-pip \ + iptables \ + ipset \ + && dnf clean all + +# Determine architecture strings used by various download URLs +# uname -m gives aarch64 or x86_64 +# Go uses arm64/amd64, gh uses arm64/amd64, ripgrep/fd use aarch64/x86_64 +RUN ARCH=$(uname -m) \ + && GOARCH=$([ "$ARCH" = "aarch64" ] && echo "arm64" || echo "amd64") \ + && GH_ARCH=$GOARCH \ + # Install Go + && curl -fsSL "https://go.dev/dl/go1.23.6.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xzf - \ + # Install gh CLI + && GH_VER=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") \ + && curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VER}/gh_${GH_VER}_linux_${GH_ARCH}.tar.gz" \ + | tar -xzf - --strip-components=1 -C /usr/local \ + # Install ripgrep + && curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-${ARCH}-unknown-linux-gnu.tar.gz" \ + | tar -xzf - --strip-components=1 -C /usr/local/bin "ripgrep-14.1.1-${ARCH}-unknown-linux-gnu/rg" \ + # Install fd + && curl -fsSL "https://github.com/sharkdp/fd/releases/download/v10.2.0/fd-v10.2.0-${ARCH}-unknown-linux-gnu.tar.gz" \ + | tar -xzf - --strip-components=1 -C /usr/local/bin "fd-v10.2.0-${ARCH}-unknown-linux-gnu/fd" + +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/home/dev/go" +ENV PATH="${GOPATH}/bin:${PATH}" + +# Install Node.js (needed for Claude Code) +ARG NODE_VERSION=22 +RUN curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && dnf install -y nodejs \ + && dnf clean all + +# Install Claude Code +RUN npm install -g @anthropic-ai/claude-code + +# Install gcloud CLI (for Vertex AI auth and GCP VM management) +RUN curl -fsSL https://sdk.cloud.google.com > /tmp/install-gcloud.sh \ + && bash /tmp/install-gcloud.sh --disable-prompts --install-dir=/opt \ + && rm /tmp/install-gcloud.sh +ENV PATH="/opt/google-cloud-sdk/bin:${PATH}" + +# Create non-root dev user with passwordless sudo +RUN useradd -m -s /bin/zsh dev \ + && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev + +# Install ansible for VM-based testing (optional, lightweight) +RUN pip3 install ansible-core + +# Firewall script for network isolation (optional, used with --dangerously-skip-permissions) +COPY init-firewall.sh /usr/local/bin/init-firewall.sh +RUN chmod +x /usr/local/bin/init-firewall.sh + +USER dev +WORKDIR /workspace + +# Persist shell history and Claude state across rebuilds (volumes in devcontainer.json) +ENV HISTFILE=/home/dev/.commandhistory/.zsh_history + +# Default to zsh +ENV SHELL=/bin/zsh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..29c30ad638 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,69 @@ +{ + "name": "collector-dev", + "build": { + "dockerfile": "Dockerfile", + "args": { + "COLLECTOR_BUILDER_TAG": "master" + } + }, + "containerUser": "dev", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + + "mounts": [ + "source=collector-dev-history,target=/home/dev/.commandhistory,type=volume", + "source=collector-dev-claude,target=/home/dev/.claude,type=volume", + "source=${localEnv:HOME}/.gitconfig,target=/home/dev/.gitconfig,type=bind,readonly", + "source=${localEnv:HOME}/.config/gcloud,target=/home/dev/.config/gcloud,type=bind,readonly", + "source=${localEnv:HOME}/.ssh,target=/home/dev/.ssh,type=bind,readonly", + "source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly" + ], + + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--cap-add=NET_ADMIN", + "--cap-add=NET_RAW", + "--init" + ], + + "customizations": { + "vscode": { + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools", + "golang.go", + "ms-python.python" + ], + "settings": { + "cmake.sourceDirectory": "${workspaceFolder}", + "cmake.buildDirectory": "${workspaceFolder}/cmake-build/${buildType}", + "clangd.path": "/usr/bin/clangd", + "files.associations": { + "*.bpf.c": "c", + "*.skel.h": "c" + } + } + } + }, + + "postCreateCommand": "/usr/local/bin/init-firewall.sh || true", + + "containerEnv": { + "DEVCONTAINER": "true", + "NPM_CONFIG_IGNORE_SCRIPTS": "true", + "NPM_CONFIG_AUDIT": "true", + "NPM_CONFIG_FUND": "false", + "PYTHONDONTWRITEBYTECODE": "1" + }, + + "remoteEnv": { + "COLLECTOR_BUILDER_TAG": "master", + "CMAKE_BUILD_TYPE": "Release", + "CLOUDSDK_CONFIG": "/home/dev/.config/gcloud", + "GOOGLE_APPLICATION_CREDENTIALS": "/home/dev/.config/gcloud/application_default_credentials.json", + "CLAUDE_CODE_USE_VERTEX": "${localEnv:CLAUDE_CODE_USE_VERTEX}", + "GOOGLE_CLOUD_PROJECT": "${localEnv:GOOGLE_CLOUD_PROJECT}", + "GOOGLE_CLOUD_LOCATION": "${localEnv:GOOGLE_CLOUD_LOCATION}", + "ANTHROPIC_VERTEX_PROJECT_ID": "${localEnv:ANTHROPIC_VERTEX_PROJECT_ID}" + } +} diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100644 index 0000000000..391088536b --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Optional network firewall for use with --dangerously-skip-permissions mode. +# Restricts outbound traffic to only necessary services. +# Requires: --cap-add=NET_ADMIN on the container (not set by default). +# To enable: add "--cap-add=NET_ADMIN" to runArgs in devcontainer.json. + +set -euo pipefail + +if ! command -v iptables &>/dev/null; then + echo "iptables not available, skipping firewall setup" + exit 0 +fi + +if ! iptables -L &>/dev/null 2>&1; then + echo "No NET_ADMIN capability, skipping firewall setup" + exit 0 +fi + +echo "Configuring network firewall..." + +# Allow loopback +iptables -A OUTPUT -o lo -j ACCEPT + +# Allow established connections +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow DNS +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT + +# Allow GCP / Vertex AI (Claude Code backend + gcloud CLI + VM management) +# Vertex AI endpoints: https://{REGION}-aiplatform.googleapis.com +iptables -A OUTPUT -d oauth2.googleapis.com -j ACCEPT +iptables -A OUTPUT -d accounts.google.com -j ACCEPT +iptables -A OUTPUT -d www.googleapis.com -j ACCEPT +iptables -A OUTPUT -d storage.googleapis.com -j ACCEPT +iptables -A OUTPUT -d compute.googleapis.com -j ACCEPT +iptables -A OUTPUT -d cloudresourcemanager.googleapis.com -j ACCEPT +# Vertex AI regions (allow all *.googleapis.com via port 443) +iptables -A OUTPUT -p tcp --dport 443 -d us-central1-aiplatform.googleapis.com -j ACCEPT +iptables -A OUTPUT -p tcp --dport 443 -d us-east5-aiplatform.googleapis.com -j ACCEPT +iptables -A OUTPUT -p tcp --dport 443 -d europe-west1-aiplatform.googleapis.com -j ACCEPT +iptables -A OUTPUT -d metadata.google.internal -j ACCEPT + +# Allow Claude API (direct Anthropic, if used alongside or instead of Vertex) +iptables -A OUTPUT -d api.anthropic.com -j ACCEPT +iptables -A OUTPUT -d statsig.anthropic.com -j ACCEPT +iptables -A OUTPUT -d sentry.io -j ACCEPT + +# Allow GitHub (for git push, gh CLI, API) +iptables -A OUTPUT -d github.com -j ACCEPT +iptables -A OUTPUT -d api.github.com -j ACCEPT + +# Allow container registries +iptables -A OUTPUT -d quay.io -j ACCEPT +iptables -A OUTPUT -d cdn.quay.io -j ACCEPT +iptables -A OUTPUT -d cdn01.quay.io -j ACCEPT +iptables -A OUTPUT -d cdn02.quay.io -j ACCEPT +iptables -A OUTPUT -d cdn03.quay.io -j ACCEPT +iptables -A OUTPUT -d registry.access.redhat.com -j ACCEPT + +# Allow SSH (for GCP VM access during integration testing) +iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT + +# Allow npm registry +iptables -A OUTPUT -d registry.npmjs.org -j ACCEPT + +# Allow Go module proxy +iptables -A OUTPUT -d proxy.golang.org -j ACCEPT +iptables -A OUTPUT -d sum.golang.org -j ACCEPT + +# Drop everything else +iptables -A OUTPUT -j DROP + +echo "Firewall configured." diff --git a/.gitignore b/.gitignore index 25842a918b..79b230ae66 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,6 @@ cmake-build-*/ # vscode configuration files .vscode/ -.devcontainer/ -.devcontainer.json cmake-build/ out/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..e99ccd3b09 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,177 @@ +# Collector - Agent Development Guide + +Collector is a C++ eBPF-based runtime security agent that captures process, +network, and container events from Linux kernels. It uses CO-RE BPF +(Compile Once, Run Everywhere) via falcosecurity-libs and reports events +to StackRox Sensor via gRPC. + +## Quick Reference + +```bash +# Build (uses builder container with all C++ deps) +make start-builder # Start builder container (first time / after reboot) +make collector # Compile collector binary (~30s incremental) +make image # Build container image +make image-dev # Build dev image (with package manager, gdb) + +# Test +make unittest # C++ unit tests via ctest (~1 min) +CMAKE_BUILD_TYPE=Debug make unittest # With debug symbols + +# Lint +make check-clang-format-all # Check C++ formatting +make check-flake8-all # Check Python + +# Integration tests (local, requires Docker + privileged) +cd integration-tests +make TestProcessNetwork # Single test suite +make ci-integration-tests # Full suite (2h timeout) +``` + +## Architecture + +``` +collector/ # Main C++ application +├── lib/ # Core library (~108 files) +│ ├── KernelDriver.h # eBPF probe lifecycle (Setup/Start/Stop) +│ ├── CollectorService.cpp # Main service loop +│ ├── ConnTracker.cpp # Connection state machine +│ ├── NetworkConnection.h # IP/port/protocol structures +│ └── ProcessSignalHandler.h # Process event formatting +├── test/ # Unit tests (GTest/GMock) +├── container/Dockerfile # Production container (UBI minimal) +└── collector.cpp # Main entrypoint + +falcosecurity-libs/ # Submodule: eBPF engine +└── driver/modern_bpf/ # CO-RE BPF programs + ├── programs/attached/ # Tracepoint handlers (sys_enter, sys_exit, sched_*) + └── maps/ # BPF maps (ring buffers, tail call tables) + +builder/ # Builder image (CentOS Stream 10) +├── Dockerfile +└── install/ # Dependency build scripts (grpc, protobuf, libbpf, etc.) + +integration-tests/ # Go test framework (testify/suite) +├── suites/ # 26 test suites +├── pkg/mock_sensor/ # Mock gRPC sensor +└── pkg/executor/ # Container runtime abstraction + +ansible/ # VM lifecycle and test orchestration +├── integration-tests.yml # Create VM → provision → test → destroy +├── dev/ # Developer inventory (acs-team-sandbox) +└── roles/ # create-vm, provision-vm, run-test-target +``` + +## Development Workflow + +### For C++ / library changes (non-eBPF) + +Changes to `collector/lib/` that don't touch kernel interaction: + +1. Edit source files +2. `make collector` — compile (~30s incremental) +3. `make unittest` — run unit tests +4. Push PR — CI validates across platforms + +Unit tests cover: config parsing, connection tracking, network structures, +process filtering, event formatting, host info detection. + +### For eBPF / kernel driver changes + +Changes to `falcosecurity-libs/driver/modern_bpf/` or `collector/lib/KernelDriver.h`: + +1. Edit source files +2. `make collector` — compile (eBPF compiles to skeleton header) +3. `make unittest` — validates C++ logic only +4. **Push PR** — CI runs integration tests on real kernels +5. Monitor CI: `.github/workflows/integration-tests.yml` runs on + rhel, ubuntu, cos, flatcar, fedora-coreos across amd64/arm64/s390x/ppc64le + +**Unit tests CANNOT validate eBPF changes.** The BPF programs must load into +a real kernel with BTF support. CI handles this across 10+ VM types. + +### For integration test changes + +Changes to `integration-tests/`: + +1. Edit Go test files +2. Build test binary: `cd integration-tests && make build` +3. Run locally if Docker available: `make TestProcessNetwork` +4. Push PR — CI runs full matrix + +### Build Variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| CMAKE_BUILD_TYPE | Release | Release or Debug | +| ADDRESS_SANITIZER | OFF | Enable AddressSanitizer | +| THREAD_SANITIZER | OFF | Enable ThreadSanitizer | +| USE_VALGRIND | OFF | Valgrind profiling | +| BPF_DEBUG_MODE | OFF | BPF debug output | +| COLLECTOR_BUILDER_TAG | master | Builder image version | + +### Running collector locally + +```yaml +# docker-compose.dev.yml pattern: +services: + collector: + image: quay.io/stackrox-io/collector:${COLLECTOR_TAG} + privileged: true + network_mode: host + environment: + - GRPC_SERVER=localhost:9999 + - COLLECTION_METHOD=core-bpf + - COLLECTOR_HOST_ROOT=/host + volumes: + - /var/run/docker.sock:/host/var/run/docker.sock:ro + - /proc:/host/proc:ro + - /etc:/host/etc:ro + - /sys/:/host/sys/:ro +``` + +Standalone mode (no gRPC server, outputs JSON to stdout): +```bash +collector --grpc-server= +``` + +### Hotreload on local K8s + +For rapid iteration without rebuilding the container image: +```bash +# Deploy stackrox to a local cluster first, then: +./utilities/hotreload.sh +# Recompile with: make -C collector container/bin/collector +``` + +## Key Dependencies + +- gRPC v1.67.0, Protobuf v28.3 +- libbpf v1.3.4, CO-RE BPF (kernel >= 5.8 with BTF) +- falcosecurity-libs (submodule, scap + sinsp) +- Google Test v1.15.2 + +## CI Pipeline + +Push to PR triggers `.github/workflows/main.yml`: +1. **init** — set tags, determine what to build +2. **build-collector** — multi-arch compile +3. **unit-tests** — ctest (Release, ASAN, Valgrind) +4. **integration-tests** — VM matrix (rhel, ubuntu, cos, flatcar, etc.) +5. **k8s-integration-tests** — KinD cluster tests +6. **benchmarks** — performance (master only or `run-benchmark` label) + +### Triggering specific CI behavior + +- Add `build-builder-image` label to rebuild the builder +- Add `run-benchmark` label for performance tests +- Add `update-baseline` label to update benchmark baseline + +## File Conventions + +- C++17, compiled with clang +- Format: `clang-format` (check with `make check-clang-format-all`) +- Integration tests: Go with testify/suite +- Shell scripts: `shfmt` + `shellcheck` +- Python: `flake8` +- Git hooks: `pre-commit` (run `make init-githook`) From c77c8a052fb83e61d054cea432670eb8426275ee Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 15:03:44 -0700 Subject: [PATCH 03/51] docs: add Vertex AI setup and devcontainer build instructions to CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index e99ccd3b09..d017ef3f7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,77 @@ network, and container events from Linux kernels. It uses CO-RE BPF (Compile Once, Run Everywhere) via falcosecurity-libs and reports events to StackRox Sensor via gRPC. +## Devcontainer Setup + +### Prerequisites + +1. Docker (Docker Desktop, OrbStack, or Colima) +2. GCP access with Vertex AI enabled (for Claude Code) + +### Vertex AI Configuration + +Claude Code in the devcontainer authenticates to Vertex AI using your host's +gcloud credentials (mounted read-only). Set up once on your host: + +```bash +# Authenticate to GCP +gcloud auth login +gcloud auth application-default login + +# Set these environment variables (add to your shell profile) +export CLAUDE_CODE_USE_VERTEX=1 +export GOOGLE_CLOUD_PROJECT= +export GOOGLE_CLOUD_LOCATION= # e.g., us-east5 +export ANTHROPIC_VERTEX_PROJECT_ID= +``` + +The devcontainer.json forwards these env vars into the container +automatically via `${localEnv:*}`. + +### Launch + +**VSCode:** Open the repo, click "Reopen in Container" when prompted. + +**CLI:** +```bash +# Build and start the devcontainer +devcontainer up --workspace-folder . + +# Or run Claude Code directly +docker run --rm \ + -v "$(pwd):/workspace" \ + -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" \ + -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" \ + -e CLAUDE_CODE_USE_VERTEX=1 \ + -e GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT \ + -e GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION \ + -e ANTHROPIC_VERTEX_PROJECT_ID=$ANTHROPIC_VERTEX_PROJECT_ID \ + -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud \ + -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json \ + -w /workspace \ + collector-dev:latest \ + claude --dangerously-skip-permissions +``` + +### Building Inside the Devcontainer + +The devcontainer IS the builder image — no nested Docker needed: + +```bash +# Configure (first time or after CMakeLists.txt changes) +cmake -S . -B cmake-build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) + +# Build (~30s incremental) +cmake --build cmake-build -- -j$(nproc) + +# Unit tests (17 tests, ~13s) +ctest --no-tests=error -V --test-dir cmake-build +``` + +### Building on the Host (without devcontainer) + ## Quick Reference ```bash From d1e7a46633fd64f9a2fc3bff8b008d2973590fb7 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 15:15:28 -0700 Subject: [PATCH 04/51] feat: add launcher script with worktree isolation and GitHub PAT support The run.sh script launches Claude Code in the devcontainer with: - Git worktree isolation: agent works on its own copy, never touches the user's checkout. Worktree is cleaned up on exit. - GitHub auth: supports fine-grained PAT via GITHUB_TOKEN or host gh CLI config (read-only mount) - Modes: autonomous (-p task), interactive, shell, no-worktree Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 154 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100755 .devcontainer/run.sh diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh new file mode 100755 index 0000000000..6f4a3216f9 --- /dev/null +++ b/.devcontainer/run.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# Launch Claude Code in the collector devcontainer with a task. +# +# Usage: +# .devcontainer/run.sh "fix the connect() handler to capture IPv6 scope IDs" +# .devcontainer/run.sh --interactive +# .devcontainer/run.sh --shell +# +# The agent works on an isolated git worktree so your working tree is untouched. +# Changes are pushed to a branch and a PR is created for CI validation. +# +# Prerequisites: +# - Docker +# - gcloud auth login && gcloud auth application-default login +# - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) +# - GITHUB_TOKEN with repo scope, or gh auth login on host + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" + +# --- Worktree isolation --- +# Create a temporary git worktree so the agent doesn't touch your checkout. +# The worktree is cleaned up when the container exits. +setup_worktree() { + local task_id + task_id="agent-$(date +%s)-$$" + local branch="claude/${task_id}" + local worktree_dir="/tmp/collector-${task_id}" + + git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD 2>/dev/null + echo "$worktree_dir" +} + +cleanup_worktree() { + local worktree_dir="$1" + if [[ -d "$worktree_dir" ]]; then + local branch + branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) + git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true + if [[ -n "$branch" ]]; then + # Only delete the branch if it was never pushed + if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then + git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true + fi + fi + fi +} + +# --- Docker args --- +build_docker_args() { + local workspace="$1" + local -a args=( + --rm + -v "$workspace:/workspace" + -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" + -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" + -v "$HOME/.ssh:/home/dev/.ssh:ro" + -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud + -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json + -w /workspace + ) + + # Forward Vertex AI env vars + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID; do + if [[ -n "${!var:-}" ]]; then + args+=(-e "$var=${!var}") + fi + done + + # Forward GitHub token (fine-grained PAT preferred) + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + args+=(-e "GITHUB_TOKEN=$GITHUB_TOKEN" -e "GH_TOKEN=$GITHUB_TOKEN") + elif [[ -d "$HOME/.config/gh" ]]; then + args+=(-v "$HOME/.config/gh:/home/dev/.config/gh:ro") + fi + + echo "${args[@]}" +} + +# --- Main --- +case "${1:-}" in + --interactive|-i) + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + echo "Working in isolated worktree: $WORKTREE" + echo "Branch: $(git -C "$WORKTREE" branch --show-current)" + DOCKER_ARGS=$(build_docker_args "$WORKTREE") + eval docker run -it $DOCKER_ARGS "$IMAGE" \ + claude --dangerously-skip-permissions + ;; + + --shell|-s) + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + echo "Working in isolated worktree: $WORKTREE" + DOCKER_ARGS=$(build_docker_args "$WORKTREE") + eval docker run -it $DOCKER_ARGS "$IMAGE" zsh + ;; + + --no-worktree) + # Run directly on the repo (no isolation, for debugging) + shift + DOCKER_ARGS=$(build_docker_args "$REPO_ROOT") + if [[ -z "${1:-}" ]]; then + eval docker run -it $DOCKER_ARGS "$IMAGE" \ + claude --dangerously-skip-permissions + else + eval docker run $DOCKER_ARGS "$IMAGE" \ + claude --dangerously-skip-permissions -p "$*" + fi + ;; + + ""|--help|-h) + cat < Date: Wed, 18 Mar 2026 15:34:38 -0700 Subject: [PATCH 05/51] feat: use official GitHub MCP server, fix run.sh, add bubblewrap - Replace gh CLI and Docker-based MCP server with official GitHub MCP server at api.githubcopilot.com/mcp (OAuth, project-scoped .mcp.json) - Add permissions.deny for dangerous MCP tools (merge, delete, fork) - Add bubblewrap, socat, iptables to Dockerfile for sandboxing - Remove gh CLI install from Dockerfile - Fix run.sh: suppress git worktree output, use bash array for docker args instead of eval with string (fixes --interactive mode) - Remove Docker socket mount and GITHUB_TOKEN forwarding from run.sh - Update skills to reference mcp__github__* tools instead of gh CLI Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 7 ++++- .claude/skills/ci-status/SKILL.md | 51 +++++++++++++------------------ .claude/skills/iterate/SKILL.md | 17 ++++++----- .devcontainer/Dockerfile | 12 +++----- .devcontainer/run.sh | 50 +++++++++++------------------- .mcp.json | 8 +++++ 6 files changed, 68 insertions(+), 77 deletions(-) create mode 100644 .mcp.json diff --git a/.claude/settings.json b/.claude/settings.json index a6a3d98bd1..f9820826aa 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,12 @@ { "permissions": { "deny": [ - "Read(.devcontainer/**)" + "Read(.devcontainer/**)", + "mcp__github__merge_pull_request", + "mcp__github__delete_file", + "mcp__github__fork_repository", + "mcp__github__create_repository", + "mcp__github__actions_run_trigger" ] } } diff --git a/.claude/skills/ci-status/SKILL.md b/.claude/skills/ci-status/SKILL.md index b925102f71..92676ede31 100644 --- a/.claude/skills/ci-status/SKILL.md +++ b/.claude/skills/ci-status/SKILL.md @@ -10,32 +10,25 @@ Check CI pipeline status for the current branch/PR and diagnose failures. ## Steps -1. Get the current branch and check for an open PR: - ```bash - gh pr view --json number,title,url,statusCheckRollup,headRefName - ``` - If no PR exists, report that and suggest pushing or creating one. - -2. Parse the check results. Group by status: - - Passed checks - - Failed checks (show full names) - - Pending/running checks - -3. For any **failed checks**: - - Identify which workflow failed (unit-tests, integration-tests, k8s-integration-tests, benchmarks, lint) - - Get the failed job logs: - ```bash - gh run view --log-failed 2>&1 | tail -100 - ``` - - For integration test failures, identify: - - Which VM type failed (rhel, ubuntu, cos, flatcar, etc.) - - Which test suite failed (e.g., TestProcessNetwork, TestConnectionsAndEndpoints) - - The relevant error message - -4. **Diagnose** the failure: - - If a unit test failed: show the failing assertion and the relevant source file - - If an integration test failed: identify if it's a test infrastructure issue (VM creation, timeout) vs an actual test failure - - If lint failed: show which files need formatting - - If build failed: show the compiler error with file:line - -5. **Suggest next steps**: what code changes would fix the failure, or if it's a flaky test / infra issue. +1. Get the current branch name from git. + +2. Use `mcp__github__search_pull_requests` to find an open PR for this branch + in `stackrox/collector`. + +3. If a PR exists, use `mcp__github__pull_request_read` to get its check status. + +4. Use `mcp__github__actions_list` to get workflow runs for the branch. + +5. For any **failed runs**: + - Use `mcp__github__actions_get` to get the run details + - Use `mcp__github__get_job_logs` to fetch failure logs + - Identify which workflow failed (unit-tests, integration-tests, k8s-integration-tests, lint) + - For integration test failures, identify which VM type and test suite failed + +6. **Diagnose** the failure: + - Unit test failure: show the failing assertion and relevant source file + - Integration test failure: distinguish infra issues (VM creation, timeout) from test failures + - Lint failure: show which files need formatting + - Build failure: show the compiler error with file:line + +7. **Suggest next steps**: what code changes would fix the failure, or note if it's flaky/infra. diff --git a/.claude/skills/iterate/SKILL.md b/.claude/skills/iterate/SKILL.md index da8cfa93eb..504572a9c7 100644 --- a/.claude/skills/iterate/SKILL.md +++ b/.claude/skills/iterate/SKILL.md @@ -1,6 +1,6 @@ --- name: iterate -description: Full development cycle — build, unit test, format check, commit, push +description: Full development cycle — build, unit test, format check, commit, push, create PR tags: [collector, build, test, workflow] --- @@ -30,14 +30,15 @@ Run the full development inner loop. Stops at the first failure. 4. **Commit**: - Stage changed files (source + any format fixes) - Create a commit with a descriptive message summarizing the changes - - Follow the repository's commit conventions -5. **Push**: - - Push to the current branch - - If a PR exists, report the PR URL - - If no PR exists, suggest creating one +5. **Push and create PR**: + - Use `mcp__github__create_branch` if the branch doesn't exist on remote + - Use `mcp__github__push_files` to push committed changes + - Use `mcp__github__create_pull_request` to create a PR if none exists, + or `mcp__github__update_pull_request` to update the description + - Alternatively, use `git push` and then create the PR via MCP 6. **Report**: - Summary: built, N tests passed, formatted M files, pushed to branch X - - Link to PR if it exists - - Note: CI will run integration tests on the PR — use `/ci-status` to check results later + - Link to PR + - Note: CI will run integration tests — use `/ci-status` to check results later diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f0d3537f69..d5707e8ee9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -25,16 +25,11 @@ RUN dnf install -y \ # Determine architecture strings used by various download URLs # uname -m gives aarch64 or x86_64 -# Go uses arm64/amd64, gh uses arm64/amd64, ripgrep/fd use aarch64/x86_64 +# Go uses arm64/amd64, ripgrep/fd use aarch64/x86_64 RUN ARCH=$(uname -m) \ && GOARCH=$([ "$ARCH" = "aarch64" ] && echo "arm64" || echo "amd64") \ - && GH_ARCH=$GOARCH \ # Install Go && curl -fsSL "https://go.dev/dl/go1.23.6.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xzf - \ - # Install gh CLI - && GH_VER=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") \ - && curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VER}/gh_${GH_VER}_linux_${GH_ARCH}.tar.gz" \ - | tar -xzf - --strip-components=1 -C /usr/local \ # Install ripgrep && curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-${ARCH}-unknown-linux-gnu.tar.gz" \ | tar -xzf - --strip-components=1 -C /usr/local/bin "ripgrep-14.1.1-${ARCH}-unknown-linux-gnu/rg" \ @@ -61,6 +56,9 @@ RUN curl -fsSL https://sdk.cloud.google.com > /tmp/install-gcloud.sh \ && rm /tmp/install-gcloud.sh ENV PATH="/opt/google-cloud-sdk/bin:${PATH}" +# Pull GitHub MCP server image (used by Claude Code for GitHub operations) +# Configured in .claude/settings.json as an MCP server + # Create non-root dev user with passwordless sudo RUN useradd -m -s /bin/zsh dev \ && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev @@ -78,5 +76,5 @@ WORKDIR /workspace # Persist shell history and Claude state across rebuilds (volumes in devcontainer.json) ENV HISTFILE=/home/dev/.commandhistory/.zsh_history -# Default to zsh ENV SHELL=/bin/zsh +ENV DEVCONTAINER=true diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 6f4a3216f9..eb1f29d6df 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -9,11 +9,13 @@ # The agent works on an isolated git worktree so your working tree is untouched. # Changes are pushed to a branch and a PR is created for CI validation. # +# GitHub access is via the official GitHub MCP server (OAuth, configured in .mcp.json). +# Authenticate once with: /mcp in Claude Code, then select GitHub → Authenticate. +# # Prerequisites: # - Docker # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) -# - GITHUB_TOKEN with repo scope, or gh auth login on host set -euo pipefail @@ -22,15 +24,13 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" # --- Worktree isolation --- -# Create a temporary git worktree so the agent doesn't touch your checkout. -# The worktree is cleaned up when the container exits. setup_worktree() { local task_id task_id="agent-$(date +%s)-$$" local branch="claude/${task_id}" local worktree_dir="/tmp/collector-${task_id}" - git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD 2>/dev/null + git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 echo "$worktree_dir" } @@ -41,7 +41,6 @@ cleanup_worktree() { branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true if [[ -n "$branch" ]]; then - # Only delete the branch if it was never pushed if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true fi @@ -52,7 +51,7 @@ cleanup_worktree() { # --- Docker args --- build_docker_args() { local workspace="$1" - local -a args=( + DOCKER_ARGS=( --rm -v "$workspace:/workspace" -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" @@ -66,18 +65,9 @@ build_docker_args() { # Forward Vertex AI env vars for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID; do if [[ -n "${!var:-}" ]]; then - args+=(-e "$var=${!var}") + DOCKER_ARGS+=(-e "$var=${!var}") fi done - - # Forward GitHub token (fine-grained PAT preferred) - if [[ -n "${GITHUB_TOKEN:-}" ]]; then - args+=(-e "GITHUB_TOKEN=$GITHUB_TOKEN" -e "GH_TOKEN=$GITHUB_TOKEN") - elif [[ -d "$HOME/.config/gh" ]]; then - args+=(-v "$HOME/.config/gh:/home/dev/.config/gh:ro") - fi - - echo "${args[@]}" } # --- Main --- @@ -87,8 +77,8 @@ case "${1:-}" in trap "cleanup_worktree '$WORKTREE'" EXIT echo "Working in isolated worktree: $WORKTREE" echo "Branch: $(git -C "$WORKTREE" branch --show-current)" - DOCKER_ARGS=$(build_docker_args "$WORKTREE") - eval docker run -it $DOCKER_ARGS "$IMAGE" \ + build_docker_args "$WORKTREE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ claude --dangerously-skip-permissions ;; @@ -96,19 +86,18 @@ case "${1:-}" in WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT echo "Working in isolated worktree: $WORKTREE" - DOCKER_ARGS=$(build_docker_args "$WORKTREE") - eval docker run -it $DOCKER_ARGS "$IMAGE" zsh + build_docker_args "$WORKTREE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh ;; --no-worktree) - # Run directly on the repo (no isolation, for debugging) shift - DOCKER_ARGS=$(build_docker_args "$REPO_ROOT") + build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then - eval docker run -it $DOCKER_ARGS "$IMAGE" \ + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ claude --dangerously-skip-permissions else - eval docker run $DOCKER_ARGS "$IMAGE" \ + docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ claude --dangerously-skip-permissions -p "$*" fi ;; @@ -123,15 +112,13 @@ Usage: Environment: COLLECTOR_DEV_IMAGE Docker image (default: collector-dev:test) - GITHUB_TOKEN Fine-grained PAT for push/PR (recommended) CLAUDE_CODE_USE_VERTEX=1 Enable Vertex AI GOOGLE_CLOUD_PROJECT GCP project ID GOOGLE_CLOUD_LOCATION Vertex AI region (e.g., us-east5) -GitHub Token: - Create a fine-grained PAT at https://github.com/settings/tokens?type=beta - Repository: stackrox/collector - Permissions: Contents (write), Pull requests (write), Actions (read) +GitHub: + Uses the official GitHub MCP server (OAuth). On first use, run /mcp + inside Claude Code and authenticate with GitHub. USAGE exit 0 ;; @@ -144,11 +131,10 @@ USAGE echo "Task: $*" echo "---" - # Cleanup worktree on exit, but only delete branch if it wasn't pushed trap "cleanup_worktree '$WORKTREE'" EXIT - DOCKER_ARGS=$(build_docker_args "$WORKTREE") - eval docker run $DOCKER_ARGS "$IMAGE" \ + build_docker_args "$WORKTREE" + docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ claude --dangerously-skip-permissions -p "$*" ;; esac diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..209568c490 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/" + } + } +} From 9ee73725b50dcc606867e64ca75d68609175a6e1 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 15:40:21 -0700 Subject: [PATCH 06/51] refactor: convert skills to collector-dev plugin with scoped tool permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move skills from .claude/skills/ to .claude/plugins/collector-dev/ as a proper Claude Code plugin. Each skill now declares only the tools it needs via allowed-tools frontmatter: - /collector-dev:build — cmake, make, git describe, strip (no GitHub) - /collector-dev:ci-status — git branch/log + GitHub MCP read-only tools - /collector-dev:iterate — build tools + git + clang-format + GitHub MCP PR/push tools The GitHub MCP server config moves from root .mcp.json into the plugin's .mcp.json so it's bundled with the skills that use it. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/plugins/collector-dev/.claude-plugin/plugin.json | 9 +++++++++ .mcp.json => .claude/plugins/collector-dev/.mcp.json | 0 .../{ => plugins/collector-dev}/skills/build/SKILL.md | 4 ++-- .../collector-dev}/skills/ci-status/SKILL.md | 2 +- .../{ => plugins/collector-dev}/skills/iterate/SKILL.md | 8 +++----- 5 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .claude/plugins/collector-dev/.claude-plugin/plugin.json rename .mcp.json => .claude/plugins/collector-dev/.mcp.json (100%) rename .claude/{ => plugins/collector-dev}/skills/build/SKILL.md (87%) rename .claude/{ => plugins/collector-dev}/skills/ci-status/SKILL.md (85%) rename .claude/{ => plugins/collector-dev}/skills/iterate/SKILL.md (77%) diff --git a/.claude/plugins/collector-dev/.claude-plugin/plugin.json b/.claude/plugins/collector-dev/.claude-plugin/plugin.json new file mode 100644 index 0000000000..0d38f68edb --- /dev/null +++ b/.claude/plugins/collector-dev/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "collector-dev", + "description": "Collector development workflows — build, test, CI status, and PR management", + "version": "1.0.0", + "author": { + "name": "RHACS Collector Team" + }, + "repository": "https://github.com/stackrox/collector" +} diff --git a/.mcp.json b/.claude/plugins/collector-dev/.mcp.json similarity index 100% rename from .mcp.json rename to .claude/plugins/collector-dev/.mcp.json diff --git a/.claude/skills/build/SKILL.md b/.claude/plugins/collector-dev/skills/build/SKILL.md similarity index 87% rename from .claude/skills/build/SKILL.md rename to .claude/plugins/collector-dev/skills/build/SKILL.md index 713bd1d6ea..ef2080aeaa 100644 --- a/.claude/skills/build/SKILL.md +++ b/.claude/plugins/collector-dev/skills/build/SKILL.md @@ -1,7 +1,7 @@ --- name: build description: Build collector binary with options (debug, asan, tsan, clean) -tags: [collector, build, cmake, cpp] +allowed-tools: Bash(cmake *), Bash(make *), Bash(nproc), Bash(git describe *), Bash(strip *), Read, Glob --- # Build Collector @@ -16,7 +16,7 @@ Build the collector binary. Supports optional arguments: ## Steps 1. Determine build environment: - - If inside the devcontainer (check: `/usr/local/bin/cmake` exists and we're on Linux), run cmake directly. + - If inside the devcontainer (check: `DEVCONTAINER=true` env var), run cmake directly. - If on the host (macOS), use `make start-builder && make collector`. 2. If `clean` argument is provided, remove `cmake-build/` directory first. diff --git a/.claude/skills/ci-status/SKILL.md b/.claude/plugins/collector-dev/skills/ci-status/SKILL.md similarity index 85% rename from .claude/skills/ci-status/SKILL.md rename to .claude/plugins/collector-dev/skills/ci-status/SKILL.md index 92676ede31..b9576aebe9 100644 --- a/.claude/skills/ci-status/SKILL.md +++ b/.claude/plugins/collector-dev/skills/ci-status/SKILL.md @@ -1,7 +1,7 @@ --- name: ci-status description: Check CI status on current PR, fetch failure logs, diagnose issues -tags: [collector, ci, github, testing] +allowed-tools: Bash(git branch *), Bash(git log *), mcp__github__search_pull_requests, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs, Read --- # CI Status diff --git a/.claude/skills/iterate/SKILL.md b/.claude/plugins/collector-dev/skills/iterate/SKILL.md similarity index 77% rename from .claude/skills/iterate/SKILL.md rename to .claude/plugins/collector-dev/skills/iterate/SKILL.md index 504572a9c7..eecfd8be34 100644 --- a/.claude/skills/iterate/SKILL.md +++ b/.claude/plugins/collector-dev/skills/iterate/SKILL.md @@ -1,7 +1,7 @@ --- name: iterate description: Full development cycle — build, unit test, format check, commit, push, create PR -tags: [collector, build, test, workflow] +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__create_branch, mcp__github__push_files, mcp__github__create_pull_request, mcp__github__update_pull_request, mcp__github__pull_request_read, mcp__github__actions_list --- # Iterate @@ -32,13 +32,11 @@ Run the full development inner loop. Stops at the first failure. - Create a commit with a descriptive message summarizing the changes 5. **Push and create PR**: - - Use `mcp__github__create_branch` if the branch doesn't exist on remote - - Use `mcp__github__push_files` to push committed changes + - Push with `git push` - Use `mcp__github__create_pull_request` to create a PR if none exists, or `mcp__github__update_pull_request` to update the description - - Alternatively, use `git push` and then create the PR via MCP 6. **Report**: - Summary: built, N tests passed, formatted M files, pushed to branch X - Link to PR - - Note: CI will run integration tests — use `/ci-status` to check results later + - Note: CI will run integration tests — use `/collector-dev:ci-status` to check results later From 24c1d2412cbac1aa4e50d87dd3e1c0dbb283b2b3 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 15:43:56 -0700 Subject: [PATCH 07/51] feat: create branch and draft PR upfront, tighten iterate permissions run.sh now creates the branch, pushes it, and opens a draft PR before launching the agent. The agent receives the branch name and PR URL in its prompt and only needs to commit and push. iterate skill drops all GitHub MCP write tools (create_branch, push_files, create_pull_request, update_pull_request). It retains only read-only GitHub MCP tools for checking CI status. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../collector-dev/skills/iterate/SKILL.md | 22 +++---- .devcontainer/run.sh | 60 +++++++++++++++---- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/.claude/plugins/collector-dev/skills/iterate/SKILL.md b/.claude/plugins/collector-dev/skills/iterate/SKILL.md index eecfd8be34..3aaf884f48 100644 --- a/.claude/plugins/collector-dev/skills/iterate/SKILL.md +++ b/.claude/plugins/collector-dev/skills/iterate/SKILL.md @@ -1,12 +1,13 @@ --- name: iterate -description: Full development cycle — build, unit test, format check, commit, push, create PR -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__create_branch, mcp__github__push_files, mcp__github__create_pull_request, mcp__github__update_pull_request, mcp__github__pull_request_read, mcp__github__actions_list +description: Full development cycle — build, unit test, format check, commit, push to existing branch +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs --- # Iterate -Run the full development inner loop. Stops at the first failure. +Run the full development inner loop. The branch and PR already exist — just build, test, and push. +Stops at the first failure. ## Steps @@ -31,12 +32,11 @@ Run the full development inner loop. Stops at the first failure. - Stage changed files (source + any format fixes) - Create a commit with a descriptive message summarizing the changes -5. **Push and create PR**: - - Push with `git push` - - Use `mcp__github__create_pull_request` to create a PR if none exists, - or `mcp__github__update_pull_request` to update the description +5. **Push**: + - `git push` to the existing branch (branch and PR already created by run.sh) + - Do NOT create new branches or PRs -6. **Report**: - - Summary: built, N tests passed, formatted M files, pushed to branch X - - Link to PR - - Note: CI will run integration tests — use `/collector-dev:ci-status` to check results later +6. **Check CI**: + - Use `mcp__github__actions_list` to see if CI has started + - Report the PR URL and note that CI is running + - Use `/collector-dev:ci-status` for detailed CI results once checks complete diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index eb1f29d6df..53bb1d9480 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -7,13 +7,11 @@ # .devcontainer/run.sh --shell # # The agent works on an isolated git worktree so your working tree is untouched. -# Changes are pushed to a branch and a PR is created for CI validation. -# -# GitHub access is via the official GitHub MCP server (OAuth, configured in .mcp.json). -# Authenticate once with: /mcp in Claude Code, then select GitHub → Authenticate. +# A draft PR is created upfront so the agent only needs to commit and push. # # Prerequisites: # - Docker +# - gh (GitHub CLI, authenticated) # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) @@ -48,6 +46,35 @@ cleanup_worktree() { fi } +# --- Create branch + draft PR --- +setup_pr() { + local worktree_dir="$1" + local task="$2" + local branch + branch=$(git -C "$worktree_dir" branch --show-current) + + # Push the branch + git -C "$worktree_dir" push -u origin "$branch" >/dev/null 2>&1 + + # Create draft PR + local pr_url + pr_url=$(gh pr create \ + --repo stackrox/collector \ + --head "$branch" \ + --draft \ + --title "claude: ${task:0:70}" \ + --body "$(cat <&1) || true + + echo "$pr_url" +} + # --- Docker args --- build_docker_args() { local workspace="$1" @@ -75,8 +102,9 @@ case "${1:-}" in --interactive|-i) WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT + BRANCH=$(git -C "$WORKTREE" branch --show-current) echo "Working in isolated worktree: $WORKTREE" - echo "Branch: $(git -C "$WORKTREE" branch --show-current)" + echo "Branch: $BRANCH" build_docker_args "$WORKTREE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ claude --dangerously-skip-permissions @@ -105,7 +133,7 @@ case "${1:-}" in ""|--help|-h) cat < Date: Wed, 18 Mar 2026 15:51:06 -0700 Subject: [PATCH 08/51] feat: add watch-ci skill for CI monitoring loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New skill that checks CI status and reacts to failures: - PASSED: all checks green, stop - PENDING: still running, wait for next loop - FIXED: diagnosed failure, pushed fix, awaiting new CI - FLAKE: infra issue, not code - BLOCKED: needs human intervention Usage: /loop 30m /collector-dev:watch-ci Same restricted tool set as iterate — read-only GitHub MCP, build tools, git push to existing branch only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../collector-dev/skills/watch-ci/SKILL.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .claude/plugins/collector-dev/skills/watch-ci/SKILL.md diff --git a/.claude/plugins/collector-dev/skills/watch-ci/SKILL.md b/.claude/plugins/collector-dev/skills/watch-ci/SKILL.md new file mode 100644 index 0000000000..e73b843e14 --- /dev/null +++ b/.claude/plugins/collector-dev/skills/watch-ci/SKILL.md @@ -0,0 +1,54 @@ +--- +name: watch-ci +description: Check CI status and react to failures — diagnose, fix, rebuild, push. Designed to run in a loop. +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__pull_request_read, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +--- + +# Watch CI + +Monitor CI for the current branch's PR and react to failures. Designed to be run +with `/loop 30m /collector-dev:watch-ci`. + +## Steps + +1. **Find the PR** for the current branch: + - Get branch name: `git branch --show-current` + - Use `mcp__github__search_pull_requests` to find the open PR in `stackrox/collector` + - If no PR found, report and stop + +2. **Check CI status**: + - Use `mcp__github__pull_request_read` to get check status + - Use `mcp__github__actions_list` to get workflow runs + +3. **Evaluate state and act**: + + **If all checks pass:** + - Report: "All CI checks passed. PR is ready for review." + - Stop — no further action needed + + **If checks are still running:** + - Report: "CI still running (X of Y checks complete). Will check again next loop." + - Stop — wait for next loop iteration + + **If checks failed:** + - Use `mcp__github__actions_get` and `mcp__github__get_job_logs` to get failure details + - Identify the failure type: + - **Build failure**: read compiler error, find the file:line, fix the code + - **Unit test failure**: read the assertion, find the test and source, fix the code + - **Integration test failure**: determine if it's a real failure or infra flake + - If infra flake (VM creation timeout, network issue): report and skip + - If real test failure: analyze the test expectation vs actual, fix the code + - **Lint failure**: run `clang-format --style=file -i` on the affected files + - After fixing: + - Build: `cmake --build cmake-build -- -j$(nproc)` + - Unit test: `ctest --no-tests=error -V --test-dir cmake-build` + - If build+test pass: `git add`, `git commit`, `git push` + - Report what was fixed and that a new CI run should start + - If the failure can't be fixed automatically, report the diagnosis and stop + +4. **Summary**: always end with a clear status line: + - `PASSED` — all checks green + - `PENDING` — checks still running, will retry + - `FIXED` — failure diagnosed and fix pushed, awaiting new CI run + - `FLAKE` — infra failure, not a code issue + - `BLOCKED` — failure requires human intervention From f75b62d255574ae06cc5bbce1351192165a4f150 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 15:53:18 -0700 Subject: [PATCH 09/51] feat: add end-to-end task skill with CI monitoring loop New /collector-dev:task skill that runs the full lifecycle: 1. Implement the task (edit, build, unit test, format, push) 2. Monitor CI in a loop (sleep 10m, check status, fix failures) 3. Stop when all checks pass, or after 6 cycles (~3h) Reports final status: PASSED, BLOCKED, or TIMEOUT. run.sh now invokes /collector-dev:task directly so a single command goes from task description to green CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../collector-dev/skills/task/SKILL.md | 82 +++++++++++++++++++ .devcontainer/run.sh | 7 +- 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 .claude/plugins/collector-dev/skills/task/SKILL.md diff --git a/.claude/plugins/collector-dev/skills/task/SKILL.md b/.claude/plugins/collector-dev/skills/task/SKILL.md new file mode 100644 index 0000000000..bda662fc99 --- /dev/null +++ b/.claude/plugins/collector-dev/skills/task/SKILL.md @@ -0,0 +1,82 @@ +--- +name: task +description: End-to-end autonomous workflow — implement a task, push, monitor CI, fix failures until green +disable-model-invocation: true +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent, mcp__github__pull_request_read, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +--- + +# Task + +Complete a development task end-to-end: implement, build, test, push, and monitor CI until all checks pass. + +## Input + +The task description is provided via $ARGUMENTS or in the initial prompt context (branch name, PR URL, task). + +## Workflow + +### Phase 1: Implement + +1. Read and understand the task +2. Explore relevant code in the repository +3. Implement the changes +4. Build the collector: + - In devcontainer: `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` + - On host: `make collector` + - If build fails, fix and retry +5. Run unit tests: + - In devcontainer: `ctest --no-tests=error -V --test-dir cmake-build` + - On host: `make unittest` + - If tests fail, fix and retry +6. Format check: + - `git diff --name-only origin/master...HEAD | grep -E '\.(cpp|h)$'` to find changed files + - `clang-format --style=file -i ` to fix formatting +7. Commit and push: + - `git add` the changed files + - `git commit` with a descriptive message + - `git push` + +### Phase 2: Monitor CI + +After pushing, enter a monitoring loop. CI typically takes 30-90 minutes. + +**Loop** (repeat until all checks pass or blocked): + +1. Wait 10 minutes: `sleep 600` +2. Check CI status: + - Get current branch: `git branch --show-current` + - Use `mcp__github__search_pull_requests` to find the PR + - Use `mcp__github__actions_list` to get workflow runs + - Use `mcp__github__pull_request_read` for check status + +3. Evaluate: + + **All checks passed** → report success and stop + + **Checks still running** → report progress ("X of Y complete"), continue loop + + **Checks failed** → + - Use `mcp__github__actions_get` and `mcp__github__get_job_logs` to get failure logs + - Diagnose the failure: + - Build failure: read error, fix code + - Unit test failure: read assertion, fix code + - Lint failure: run clang-format + - Integration test infra flake (VM timeout, network): report as flake, continue loop + - Integration test real failure: analyze and fix code + - If fixable: fix → build → unit test → commit → push → continue loop + - If not fixable: report diagnosis and stop + +4. Safety limits: + - Maximum 6 CI cycles (about 3 hours of monitoring) + - If exceeded, report status and stop + +### Completion + +End with a summary: +``` +STATUS: PASSED | BLOCKED | TIMEOUT +Branch: claude/agent-xxx +PR: +Cycles: N +Changes: list of files modified +``` diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 53bb1d9480..a21ddd3bcb 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -133,7 +133,7 @@ case "${1:-}" in ""|--help|-h) cat < Date: Wed, 18 Mar 2026 15:57:05 -0700 Subject: [PATCH 10/51] fix: load collector-dev plugin via --plugin-dir flag Claude Code doesn't auto-discover plugins from .claude/plugins/. Add --plugin-dir /workspace/.claude/plugins/collector-dev to all claude invocations so skills like /collector-dev:task are available. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index a21ddd3bcb..ad64547faa 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -20,6 +20,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" +PLUGIN_DIR="/workspace/.claude/plugins/collector-dev" +CLAUDE_CMD=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") # --- Worktree isolation --- setup_worktree() { @@ -107,7 +109,7 @@ case "${1:-}" in echo "Branch: $BRANCH" build_docker_args "$WORKTREE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - claude --dangerously-skip-permissions + "${CLAUDE_CMD[@]}" ;; --shell|-s) @@ -123,10 +125,10 @@ case "${1:-}" in build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - claude --dangerously-skip-permissions + "${CLAUDE_CMD[@]}" else docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - claude --dangerously-skip-permissions -p "$*" + "${CLAUDE_CMD[@]}" -p "$*" fi ;; @@ -169,7 +171,7 @@ USAGE build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - claude --dangerously-skip-permissions -p \ + "${CLAUDE_CMD[@]}" -p \ "/collector-dev:task You are working on branch '$BRANCH'. A draft PR has been created at: $PR_URL Your task: $TASK From 11b7afea63069c5caae961eee2cdc0d3c0384b95 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 16:03:43 -0700 Subject: [PATCH 11/51] feat: stream agent activity to stdout in autonomous mode Use --output-format stream-json --verbose for autonomous task mode so all messages (tool calls, responses, thinking) stream to container stdout in real time. Interactive mode keeps the normal TUI. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index ad64547faa..b745f95568 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -21,7 +21,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" PLUGIN_DIR="/workspace/.claude/plugins/collector-dev" -CLAUDE_CMD=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") + +# Interactive mode: normal TUI +CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") + +# Autonomous mode: stream all messages to stdout as JSON +CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR" --output-format stream-json --verbose) # --- Worktree isolation --- setup_worktree() { @@ -109,7 +114,7 @@ case "${1:-}" in echo "Branch: $BRANCH" build_docker_args "$WORKTREE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_CMD[@]}" + "${CLAUDE_INTERACTIVE[@]}" ;; --shell|-s) @@ -125,10 +130,10 @@ case "${1:-}" in build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_CMD[@]}" + "${CLAUDE_INTERACTIVE[@]}" else docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_CMD[@]}" -p "$*" + "${CLAUDE_AUTONOMOUS[@]}" -p "$*" fi ;; @@ -171,7 +176,7 @@ USAGE build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_CMD[@]}" -p \ + "${CLAUDE_AUTONOMOUS[@]}" -p \ "/collector-dev:task You are working on branch '$BRANCH'. A draft PR has been created at: $PR_URL Your task: $TASK From e32a662e7ae6ed7e820fcf5fc45eacc9b408b17c Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 16:05:40 -0700 Subject: [PATCH 12/51] feat: add --local mode for debugging without worktree or PR --local edits the working tree directly with interactive TUI. No worktree, no branch, no PR. For debugging and experimentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index b745f95568..60f5f0395a 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -4,6 +4,7 @@ # Usage: # .devcontainer/run.sh "fix the connect() handler to capture IPv6 scope IDs" # .devcontainer/run.sh --interactive +# .devcontainer/run.sh --local "debug the build failure" # .devcontainer/run.sh --shell # # The agent works on an isolated git worktree so your working tree is untouched. @@ -11,7 +12,7 @@ # # Prerequisites: # - Docker -# - gh (GitHub CLI, authenticated) +# - gh (GitHub CLI, authenticated — only needed for task mode) # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) @@ -117,33 +118,33 @@ case "${1:-}" in "${CLAUDE_INTERACTIVE[@]}" ;; - --shell|-s) - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - echo "Working in isolated worktree: $WORKTREE" - build_docker_args "$WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh - ;; - - --no-worktree) + --local|-l) shift build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ "${CLAUDE_INTERACTIVE[@]}" else - docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$*" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ + "${CLAUDE_INTERACTIVE[@]}" -p "$*" fi ;; + --shell|-s) + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + echo "Working in isolated worktree: $WORKTREE" + build_docker_args "$WORKTREE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh + ;; + ""|--help|-h) cat < Date: Wed, 18 Mar 2026 16:07:54 -0700 Subject: [PATCH 13/51] feat: add --headless mode (worktree + stream-json, no PR) Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 61 +++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 60f5f0395a..f07e203640 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -2,17 +2,15 @@ # Launch Claude Code in the collector devcontainer with a task. # # Usage: -# .devcontainer/run.sh "fix the connect() handler to capture IPv6 scope IDs" -# .devcontainer/run.sh --interactive -# .devcontainer/run.sh --local "debug the build failure" -# .devcontainer/run.sh --shell -# -# The agent works on an isolated git worktree so your working tree is untouched. -# A draft PR is created upfront so the agent only needs to commit and push. +# .devcontainer/run.sh "task description" Full: worktree + PR + CI loop +# .devcontainer/run.sh --headless "task description" Worktree + stream output, no PR +# .devcontainer/run.sh --interactive Worktree + TUI, no PR +# .devcontainer/run.sh --local ["task"] Edit working tree directly +# .devcontainer/run.sh --shell Shell into container # # Prerequisites: # - Docker -# - gh (GitHub CLI, authenticated — only needed for task mode) +# - gh (GitHub CLI, authenticated — only needed for default task mode) # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) @@ -23,10 +21,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" PLUGIN_DIR="/workspace/.claude/plugins/collector-dev" -# Interactive mode: normal TUI CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") - -# Autonomous mode: stream all messages to stdout as JSON CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR" --output-format stream-json --verbose) # --- Worktree isolation --- @@ -61,10 +56,8 @@ setup_pr() { local branch branch=$(git -C "$worktree_dir" branch --show-current) - # Push the branch git -C "$worktree_dir" push -u origin "$branch" >/dev/null 2>&1 - # Create draft PR local pr_url pr_url=$(gh pr create \ --repo stackrox/collector \ @@ -97,7 +90,6 @@ build_docker_args() { -w /workspace ) - # Forward Vertex AI env vars for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") @@ -118,6 +110,22 @@ case "${1:-}" in "${CLAUDE_INTERACTIVE[@]}" ;; + --headless|-H) + shift + if [[ -z "${1:-}" ]]; then + echo "Usage: $0 --headless \"task description\"" >&2 + exit 1 + fi + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + BRANCH=$(git -C "$WORKTREE" branch --show-current) + echo "Working in isolated worktree: $WORKTREE" >&2 + echo "Branch: $BRANCH" >&2 + build_docker_args "$WORKTREE" + docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ + "${CLAUDE_AUTONOMOUS[@]}" -p "$*" + ;; + --local|-l) shift build_docker_args "$REPO_ROOT" @@ -141,10 +149,11 @@ case "${1:-}" in ""|--help|-h) cat <&2 + echo "Branch: $BRANCH" >&2 + echo "Task: $TASK" >&2 + echo "---" >&2 + echo "Creating draft PR..." >&2 PR_URL=$(setup_pr "$WORKTREE" "$TASK") - echo "PR: $PR_URL" - echo "---" + echo "PR: $PR_URL" >&2 + echo "---" >&2 trap "cleanup_worktree '$WORKTREE'" EXIT From 2cc2a47a21364aeec76500af5ff9fbea1368f682 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 16:15:55 -0700 Subject: [PATCH 14/51] fix: initialize submodules in worktree after creation git worktree add does not init submodules. Without this, cmake fails because falcosecurity-libs and other submodules are missing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index f07e203640..31334a7938 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -32,6 +32,11 @@ setup_worktree() { local worktree_dir="/tmp/collector-${task_id}" git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 + + # Initialize submodules in the worktree + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init --recursive >/dev/null 2>&1 + echo "$worktree_dir" } From 1cc86dd139e315344420e92ae5108fd1f6569081 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 16:21:02 -0700 Subject: [PATCH 15/51] fix: only init required submodules, drop --recursive Only init falcosecurity-libs and collector/proto/third_party/stackrox. The 17 builder/third_party/* submodules are baked into the builder image and not needed for compiling collector. This avoids cloning 49 recursive submodules (was hanging on large repos like grpc). Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 31334a7938..ae716e967f 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -33,9 +33,12 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - # Initialize submodules in the worktree + # Only init submodules needed for building collector (not builder/third_party) echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init --recursive >/dev/null 2>&1 + git -C "$worktree_dir" submodule update --init \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + >/dev/null 2>&1 echo "$worktree_dir" } From f9697669a705c763f6a0451a080b12e6b48b309e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 16:42:02 -0700 Subject: [PATCH 16/51] fix: headless mode now invokes /collector-dev:task skill like default mode Both headless and default task mode now use the same task_prompt() that explicitly invokes /collector-dev:task, ensuring the skill's allowed-tools restrictions are enforced. Only difference is headless skips PR creation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index ae716e967f..176000d262 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -105,6 +105,24 @@ build_docker_args() { done } +# --- Task prompt --- +task_prompt() { + local branch="$1" + local task="$2" + local pr_url="${3:-}" + + local prompt="/collector-dev:task You are working on branch '$branch'." + if [[ -n "$pr_url" ]]; then + prompt="$prompt A draft PR has been created at: $pr_url" + fi + prompt="$prompt + +Your task: $task + +The branch is already pushed. Do not create new branches or PRs. Commit and push with git." + echo "$prompt" +} + # --- Main --- case "${1:-}" in --interactive|-i) @@ -127,11 +145,15 @@ case "${1:-}" in WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT BRANCH=$(git -C "$WORKTREE" branch --show-current) + TASK="$*" echo "Working in isolated worktree: $WORKTREE" >&2 echo "Branch: $BRANCH" >&2 + echo "Task: $TASK" >&2 + echo "---" >&2 + PROMPT=$(task_prompt "$BRANCH" "$TASK") build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$*" + "${CLAUDE_AUTONOMOUS[@]}" -p "$PROMPT" ;; --local|-l) @@ -158,7 +180,7 @@ case "${1:-}" in cat < Date: Wed, 18 Mar 2026 18:11:22 -0700 Subject: [PATCH 17/51] feat: add preflight checks with clear error messages Validate before launching the container: - Docker running and image exists (with build command hint) - gcloud ADC credentials file exists - Vertex AI env vars set (CLAUDE_CODE_USE_VERTEX, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION) - gh CLI authenticated (only for PR mode) - ~/.gitconfig and ~/.ssh exist (warnings) - git push and gh pr create errors are no longer silent Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 100 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 176000d262..54106eda86 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -24,6 +24,84 @@ PLUGIN_DIR="/workspace/.claude/plugins/collector-dev" CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR" --output-format stream-json --verbose) +# --- Preflight checks --- +check_docker() { + if ! command -v docker &>/dev/null; then + echo "ERROR: docker not found. Install Docker Desktop, OrbStack, or Colima." >&2 + exit 1 + fi + if ! docker info &>/dev/null 2>&1; then + echo "ERROR: Docker daemon not running." >&2 + exit 1 + fi +} + +check_image() { + if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then + echo "ERROR: Docker image '$IMAGE' not found." >&2 + echo "Build it with: docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 + exit 1 + fi +} + +check_gcloud() { + local adc="$HOME/.config/gcloud/application_default_credentials.json" + if [[ ! -f "$adc" ]]; then + echo "ERROR: GCloud application default credentials not found at $adc" >&2 + echo "Run: gcloud auth application-default login" >&2 + exit 1 + fi +} + +check_vertex_env() { + local missing=() + [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) + [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) + [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "ERROR: Missing Vertex AI environment variables: ${missing[*]}" >&2 + echo "Set them in your shell profile (see CLAUDE.md):" >&2 + echo " export CLAUDE_CODE_USE_VERTEX=1" >&2 + echo " export GOOGLE_CLOUD_PROJECT=" >&2 + echo " export GOOGLE_CLOUD_LOCATION= # e.g., us-east5" >&2 + echo " export ANTHROPIC_VERTEX_PROJECT_ID=" >&2 + exit 1 + fi +} + +check_gh() { + if ! command -v gh &>/dev/null; then + echo "ERROR: gh (GitHub CLI) not found. Install: brew install gh" >&2 + exit 1 + fi + if ! gh auth status &>/dev/null 2>&1; then + echo "ERROR: gh not authenticated. Run: gh auth login" >&2 + exit 1 + fi +} + +check_git_config() { + if [[ ! -f "$HOME/.gitconfig" ]]; then + echo "WARNING: ~/.gitconfig not found. Git operations inside container may fail." >&2 + fi + if [[ ! -d "$HOME/.ssh" ]]; then + echo "WARNING: ~/.ssh not found. Git push via SSH will not work." >&2 + fi +} + +preflight() { + local need_gh="${1:-false}" + check_docker + check_image + check_gcloud + check_vertex_env + check_git_config + if [[ "$need_gh" == "true" ]]; then + check_gh + fi +} + # --- Worktree isolation --- setup_worktree() { local task_id @@ -64,8 +142,13 @@ setup_pr() { local branch branch=$(git -C "$worktree_dir" branch --show-current) - git -C "$worktree_dir" push -u origin "$branch" >/dev/null 2>&1 + echo "Pushing branch $branch..." >&2 + if ! git -C "$worktree_dir" push -u origin "$branch" 2>&1 >&2; then + echo "ERROR: Failed to push branch $branch" >&2 + exit 1 + fi + echo "Creating draft PR..." >&2 local pr_url pr_url=$(gh pr create \ --repo stackrox/collector \ @@ -79,7 +162,13 @@ ${task} --- *Automated by Claude Code agent. Branch: \`${branch}\`* BODY -)" 2>&1) || true +)" 2>&1) + + if [[ $? -ne 0 || -z "$pr_url" ]]; then + echo "ERROR: Failed to create draft PR" >&2 + echo "$pr_url" >&2 + exit 1 + fi echo "$pr_url" } @@ -126,6 +215,7 @@ The branch is already pushed. Do not create new branches or PRs. Commit and push # --- Main --- case "${1:-}" in --interactive|-i) + preflight false WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT BRANCH=$(git -C "$WORKTREE" branch --show-current) @@ -142,6 +232,7 @@ case "${1:-}" in echo "Usage: $0 --headless \"task description\"" >&2 exit 1 fi + preflight false WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT BRANCH=$(git -C "$WORKTREE" branch --show-current) @@ -158,6 +249,7 @@ case "${1:-}" in --local|-l) shift + preflight false build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ @@ -169,6 +261,8 @@ case "${1:-}" in ;; --shell|-s) + check_docker + check_image WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT echo "Working in isolated worktree: $WORKTREE" @@ -199,6 +293,7 @@ USAGE ;; *) + preflight true WORKTREE=$(setup_worktree) BRANCH=$(git -C "$WORKTREE" branch --show-current) TASK="$*" @@ -207,7 +302,6 @@ USAGE echo "Branch: $BRANCH" >&2 echo "Task: $TASK" >&2 echo "---" >&2 - echo "Creating draft PR..." >&2 PR_URL=$(setup_pr "$WORKTREE" "$TASK") echo "PR: $PR_URL" >&2 echo "---" >&2 From 762e2ce85a4905f8f98e1d41e1feb04de7342422 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 18:43:14 -0700 Subject: [PATCH 18/51] refactor: remove gh CLI dependency, agent creates PR via GitHub MCP - Remove gh from run.sh entirely (no check_gh, no setup_pr) - Remove --headless (was identical to default mode without PR) - task skill now has create_pull_request and create_branch in allowed-tools - Agent pushes branch and creates draft PR via GitHub MCP server - iterate skill stays read-only on GitHub (only task can create PRs) - Simplified to 4 modes: default task, --interactive, --local, --shell Co-Authored-By: Claude Opus 4.6 (1M context) --- .../collector-dev/skills/task/SKILL.md | 14 +- .devcontainer/run.sh | 127 +++--------------- 2 files changed, 26 insertions(+), 115 deletions(-) diff --git a/.claude/plugins/collector-dev/skills/task/SKILL.md b/.claude/plugins/collector-dev/skills/task/SKILL.md index bda662fc99..b55935597b 100644 --- a/.claude/plugins/collector-dev/skills/task/SKILL.md +++ b/.claude/plugins/collector-dev/skills/task/SKILL.md @@ -1,17 +1,17 @@ --- name: task -description: End-to-end autonomous workflow — implement a task, push, monitor CI, fix failures until green +description: End-to-end autonomous workflow — implement a task, push, create PR, monitor CI, fix failures until green disable-model-invocation: true -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent, mcp__github__pull_request_read, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent, mcp__github__create_pull_request, mcp__github__create_branch, mcp__github__pull_request_read, mcp__github__update_pull_request, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs --- # Task -Complete a development task end-to-end: implement, build, test, push, and monitor CI until all checks pass. +Complete a development task end-to-end: implement, build, test, push, create PR, and monitor CI until all checks pass. ## Input -The task description is provided via $ARGUMENTS or in the initial prompt context (branch name, PR URL, task). +The task description is provided via $ARGUMENTS or in the initial prompt context (branch name, task). ## Workflow @@ -34,7 +34,11 @@ The task description is provided via $ARGUMENTS or in the initial prompt context 7. Commit and push: - `git add` the changed files - `git commit` with a descriptive message - - `git push` + - `git push -u origin HEAD` +8. Create a draft PR: + - Use `mcp__github__create_pull_request` to create a draft PR in `stackrox/collector` + - Title: brief summary of the task + - Body: describe what was changed and why ### Phase 2: Monitor CI diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 54106eda86..b2f23327b0 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -2,15 +2,13 @@ # Launch Claude Code in the collector devcontainer with a task. # # Usage: -# .devcontainer/run.sh "task description" Full: worktree + PR + CI loop -# .devcontainer/run.sh --headless "task description" Worktree + stream output, no PR -# .devcontainer/run.sh --interactive Worktree + TUI, no PR -# .devcontainer/run.sh --local ["task"] Edit working tree directly +# .devcontainer/run.sh "task description" Worktree + /collector-dev:task (stream-json) +# .devcontainer/run.sh --interactive Worktree + TUI +# .devcontainer/run.sh --local ["task"] Edit working tree directly, TUI # .devcontainer/run.sh --shell Shell into container # # Prerequisites: # - Docker -# - gh (GitHub CLI, authenticated — only needed for default task mode) # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) @@ -70,17 +68,6 @@ check_vertex_env() { fi } -check_gh() { - if ! command -v gh &>/dev/null; then - echo "ERROR: gh (GitHub CLI) not found. Install: brew install gh" >&2 - exit 1 - fi - if ! gh auth status &>/dev/null 2>&1; then - echo "ERROR: gh not authenticated. Run: gh auth login" >&2 - exit 1 - fi -} - check_git_config() { if [[ ! -f "$HOME/.gitconfig" ]]; then echo "WARNING: ~/.gitconfig not found. Git operations inside container may fail." >&2 @@ -91,15 +78,11 @@ check_git_config() { } preflight() { - local need_gh="${1:-false}" check_docker check_image check_gcloud check_vertex_env check_git_config - if [[ "$need_gh" == "true" ]]; then - check_gh - fi } # --- Worktree isolation --- @@ -111,7 +94,6 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - # Only init submodules needed for building collector (not builder/third_party) echo "Initializing submodules..." >&2 git -C "$worktree_dir" submodule update --init \ falcosecurity-libs \ @@ -135,44 +117,6 @@ cleanup_worktree() { fi } -# --- Create branch + draft PR --- -setup_pr() { - local worktree_dir="$1" - local task="$2" - local branch - branch=$(git -C "$worktree_dir" branch --show-current) - - echo "Pushing branch $branch..." >&2 - if ! git -C "$worktree_dir" push -u origin "$branch" 2>&1 >&2; then - echo "ERROR: Failed to push branch $branch" >&2 - exit 1 - fi - - echo "Creating draft PR..." >&2 - local pr_url - pr_url=$(gh pr create \ - --repo stackrox/collector \ - --head "$branch" \ - --draft \ - --title "claude: ${task:0:70}" \ - --body "$(cat <&1) - - if [[ $? -ne 0 || -z "$pr_url" ]]; then - echo "ERROR: Failed to create draft PR" >&2 - echo "$pr_url" >&2 - exit 1 - fi - - echo "$pr_url" -} - # --- Docker args --- build_docker_args() { local workspace="$1" @@ -194,28 +138,10 @@ build_docker_args() { done } -# --- Task prompt --- -task_prompt() { - local branch="$1" - local task="$2" - local pr_url="${3:-}" - - local prompt="/collector-dev:task You are working on branch '$branch'." - if [[ -n "$pr_url" ]]; then - prompt="$prompt A draft PR has been created at: $pr_url" - fi - prompt="$prompt - -Your task: $task - -The branch is already pushed. Do not create new branches or PRs. Commit and push with git." - echo "$prompt" -} - # --- Main --- case "${1:-}" in --interactive|-i) - preflight false + preflight WORKTREE=$(setup_worktree) trap "cleanup_worktree '$WORKTREE'" EXIT BRANCH=$(git -C "$WORKTREE" branch --show-current) @@ -226,30 +152,9 @@ case "${1:-}" in "${CLAUDE_INTERACTIVE[@]}" ;; - --headless|-H) - shift - if [[ -z "${1:-}" ]]; then - echo "Usage: $0 --headless \"task description\"" >&2 - exit 1 - fi - preflight false - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - BRANCH=$(git -C "$WORKTREE" branch --show-current) - TASK="$*" - echo "Working in isolated worktree: $WORKTREE" >&2 - echo "Branch: $BRANCH" >&2 - echo "Task: $TASK" >&2 - echo "---" >&2 - PROMPT=$(task_prompt "$BRANCH" "$TASK") - build_docker_args "$WORKTREE" - docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$PROMPT" - ;; - --local|-l) shift - preflight false + preflight build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ @@ -273,12 +178,13 @@ case "${1:-}" in ""|--help|-h) cat <&2 echo "Task: $TASK" >&2 echo "---" >&2 - PR_URL=$(setup_pr "$WORKTREE" "$TASK") - echo "PR: $PR_URL" >&2 - echo "---" >&2 trap "cleanup_worktree '$WORKTREE'" EXIT - PROMPT=$(task_prompt "$BRANCH" "$TASK" "$PR_URL") build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$PROMPT" + "${CLAUDE_AUTONOMOUS[@]}" -p \ + "/collector-dev:task You are working on branch '$BRANCH'. + +Your task: $TASK + +After implementing and testing, push with git and create a draft PR via the GitHub MCP server. Do not use gh CLI." ;; esac From 995b443fb88643868e416dc36360d1bfcc1f2167 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 21:34:58 -0700 Subject: [PATCH 19/51] refactor: move skills from plugin to standalone, simplify CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move skills from .claude/plugins/collector-dev/ to .claude/skills/ (standalone skills, no plugin wrapper). Fixes skills not loading in worktrees since the plugin directory was never committed. - Delete collector-dev plugin entirely (caused phantom GitHub MCP) - Remove --plugin-dir from run.sh - Add entrypoint.sh that creates .claude dirs and registers GitHub MCP server via claude mcp add-json when GITHUB_TOKEN is set - Add --skip-submodules and --debug flags to run.sh - Add COPY --chmod=755 for entrypoint.sh in Dockerfile - Simplify CLAUDE.md from 249 to 30 lines — just build commands, key paths, testing rules, and conventions Co-Authored-By: Claude Opus 4.6 (1M context) --- .../collector-dev/.claude-plugin/plugin.json | 9 - .claude/plugins/collector-dev/.mcp.json | 8 - .claude/settings.json | 1 - .../collector-dev => }/skills/build/SKILL.md | 0 .../skills/ci-status/SKILL.md | 13 +- .../skills/iterate/SKILL.md | 4 +- .../collector-dev => }/skills/task/SKILL.md | 10 +- .../skills/watch-ci/SKILL.md | 9 +- .devcontainer/Dockerfile | 10 +- .devcontainer/entrypoint.sh | 12 + .devcontainer/run.sh | 64 +++-- CLAUDE.md | 251 ++---------------- 12 files changed, 102 insertions(+), 289 deletions(-) delete mode 100644 .claude/plugins/collector-dev/.claude-plugin/plugin.json delete mode 100644 .claude/plugins/collector-dev/.mcp.json rename .claude/{plugins/collector-dev => }/skills/build/SKILL.md (100%) rename .claude/{plugins/collector-dev => }/skills/ci-status/SKILL.md (59%) rename .claude/{plugins/collector-dev => }/skills/iterate/SKILL.md (90%) rename .claude/{plugins/collector-dev => }/skills/task/SKILL.md (80%) rename .claude/{plugins/collector-dev => }/skills/watch-ci/SKILL.md (82%) create mode 100755 .devcontainer/entrypoint.sh diff --git a/.claude/plugins/collector-dev/.claude-plugin/plugin.json b/.claude/plugins/collector-dev/.claude-plugin/plugin.json deleted file mode 100644 index 0d38f68edb..0000000000 --- a/.claude/plugins/collector-dev/.claude-plugin/plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "collector-dev", - "description": "Collector development workflows — build, test, CI status, and PR management", - "version": "1.0.0", - "author": { - "name": "RHACS Collector Team" - }, - "repository": "https://github.com/stackrox/collector" -} diff --git a/.claude/plugins/collector-dev/.mcp.json b/.claude/plugins/collector-dev/.mcp.json deleted file mode 100644 index 209568c490..0000000000 --- a/.claude/plugins/collector-dev/.mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/" - } - } -} diff --git a/.claude/settings.json b/.claude/settings.json index f9820826aa..1f6f35d1f7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,6 @@ { "permissions": { "deny": [ - "Read(.devcontainer/**)", "mcp__github__merge_pull_request", "mcp__github__delete_file", "mcp__github__fork_repository", diff --git a/.claude/plugins/collector-dev/skills/build/SKILL.md b/.claude/skills/build/SKILL.md similarity index 100% rename from .claude/plugins/collector-dev/skills/build/SKILL.md rename to .claude/skills/build/SKILL.md diff --git a/.claude/plugins/collector-dev/skills/ci-status/SKILL.md b/.claude/skills/ci-status/SKILL.md similarity index 59% rename from .claude/plugins/collector-dev/skills/ci-status/SKILL.md rename to .claude/skills/ci-status/SKILL.md index b9576aebe9..c4f98d88c7 100644 --- a/.claude/plugins/collector-dev/skills/ci-status/SKILL.md +++ b/.claude/skills/ci-status/SKILL.md @@ -1,7 +1,7 @@ --- name: ci-status description: Check CI status on current PR, fetch failure logs, diagnose issues -allowed-tools: Bash(git branch *), Bash(git log *), mcp__github__search_pull_requests, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs, Read +allowed-tools: Bash(git branch *), Bash(git log *), Read --- # CI Status @@ -12,16 +12,15 @@ Check CI pipeline status for the current branch/PR and diagnose failures. 1. Get the current branch name from git. -2. Use `mcp__github__search_pull_requests` to find an open PR for this branch - in `stackrox/collector`. +2. Use the GitHub MCP server to search for an open PR for this branch + in stackrox/collector. -3. If a PR exists, use `mcp__github__pull_request_read` to get its check status. +3. If a PR exists, get its check status via the GitHub MCP server. -4. Use `mcp__github__actions_list` to get workflow runs for the branch. +4. Get workflow runs for the branch via the GitHub MCP server. 5. For any **failed runs**: - - Use `mcp__github__actions_get` to get the run details - - Use `mcp__github__get_job_logs` to fetch failure logs + - Get the run details and job logs via the GitHub MCP server - Identify which workflow failed (unit-tests, integration-tests, k8s-integration-tests, lint) - For integration test failures, identify which VM type and test suite failed diff --git a/.claude/plugins/collector-dev/skills/iterate/SKILL.md b/.claude/skills/iterate/SKILL.md similarity index 90% rename from .claude/plugins/collector-dev/skills/iterate/SKILL.md rename to .claude/skills/iterate/SKILL.md index 3aaf884f48..d1dad04f66 100644 --- a/.claude/plugins/collector-dev/skills/iterate/SKILL.md +++ b/.claude/skills/iterate/SKILL.md @@ -1,7 +1,7 @@ --- name: iterate description: Full development cycle — build, unit test, format check, commit, push to existing branch -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep --- # Iterate @@ -37,6 +37,6 @@ Stops at the first failure. - Do NOT create new branches or PRs 6. **Check CI**: - - Use `mcp__github__actions_list` to see if CI has started + - Use the GitHub MCP server to check if CI has started - Report the PR URL and note that CI is running - Use `/collector-dev:ci-status` for detailed CI results once checks complete diff --git a/.claude/plugins/collector-dev/skills/task/SKILL.md b/.claude/skills/task/SKILL.md similarity index 80% rename from .claude/plugins/collector-dev/skills/task/SKILL.md rename to .claude/skills/task/SKILL.md index b55935597b..07c0abef4c 100644 --- a/.claude/plugins/collector-dev/skills/task/SKILL.md +++ b/.claude/skills/task/SKILL.md @@ -2,7 +2,7 @@ name: task description: End-to-end autonomous workflow — implement a task, push, create PR, monitor CI, fix failures until green disable-model-invocation: true -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent, mcp__github__create_pull_request, mcp__github__create_branch, mcp__github__pull_request_read, mcp__github__update_pull_request, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent --- # Task @@ -36,7 +36,7 @@ The task description is provided via $ARGUMENTS or in the initial prompt context - `git commit` with a descriptive message - `git push -u origin HEAD` 8. Create a draft PR: - - Use `mcp__github__create_pull_request` to create a draft PR in `stackrox/collector` + - Use the GitHub MCP server to create a draft PR in stackrox/collector - Title: brief summary of the task - Body: describe what was changed and why @@ -49,9 +49,7 @@ After pushing, enter a monitoring loop. CI typically takes 30-90 minutes. 1. Wait 10 minutes: `sleep 600` 2. Check CI status: - Get current branch: `git branch --show-current` - - Use `mcp__github__search_pull_requests` to find the PR - - Use `mcp__github__actions_list` to get workflow runs - - Use `mcp__github__pull_request_read` for check status + - Use the GitHub MCP server to search for the PR and get workflow run status 3. Evaluate: @@ -60,7 +58,7 @@ After pushing, enter a monitoring loop. CI typically takes 30-90 minutes. **Checks still running** → report progress ("X of Y complete"), continue loop **Checks failed** → - - Use `mcp__github__actions_get` and `mcp__github__get_job_logs` to get failure logs + - Use the GitHub MCP server to get job logs for the failed run - Diagnose the failure: - Build failure: read error, fix code - Unit test failure: read assertion, fix code diff --git a/.claude/plugins/collector-dev/skills/watch-ci/SKILL.md b/.claude/skills/watch-ci/SKILL.md similarity index 82% rename from .claude/plugins/collector-dev/skills/watch-ci/SKILL.md rename to .claude/skills/watch-ci/SKILL.md index e73b843e14..f5d2382f62 100644 --- a/.claude/plugins/collector-dev/skills/watch-ci/SKILL.md +++ b/.claude/skills/watch-ci/SKILL.md @@ -1,7 +1,7 @@ --- name: watch-ci description: Check CI status and react to failures — diagnose, fix, rebuild, push. Designed to run in a loop. -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep, mcp__github__pull_request_read, mcp__github__search_pull_requests, mcp__github__actions_list, mcp__github__actions_get, mcp__github__get_job_logs +allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep --- # Watch CI @@ -13,12 +13,11 @@ with `/loop 30m /collector-dev:watch-ci`. 1. **Find the PR** for the current branch: - Get branch name: `git branch --show-current` - - Use `mcp__github__search_pull_requests` to find the open PR in `stackrox/collector` + - Use the GitHub MCP server to search for the open PR in stackrox/collector - If no PR found, report and stop 2. **Check CI status**: - - Use `mcp__github__pull_request_read` to get check status - - Use `mcp__github__actions_list` to get workflow runs + - Use the GitHub MCP server to get PR check status and workflow runs 3. **Evaluate state and act**: @@ -31,7 +30,7 @@ with `/loop 30m /collector-dev:watch-ci`. - Stop — wait for next loop iteration **If checks failed:** - - Use `mcp__github__actions_get` and `mcp__github__get_job_logs` to get failure details + - Use the GitHub MCP server to get job logs for the failed run - Identify the failure type: - **Build failure**: read compiler error, find the file:line, fix the code - **Unit test failure**: read the assertion, find the test and source, fix the code diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d5707e8ee9..14bae718f0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -61,14 +61,16 @@ ENV PATH="/opt/google-cloud-sdk/bin:${PATH}" # Create non-root dev user with passwordless sudo RUN useradd -m -s /bin/zsh dev \ - && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev + && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev \ + && mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory \ + && chown -R dev:dev /home/dev/.claude /home/dev/.commandhistory # Install ansible for VM-based testing (optional, lightweight) RUN pip3 install ansible-core # Firewall script for network isolation (optional, used with --dangerously-skip-permissions) -COPY init-firewall.sh /usr/local/bin/init-firewall.sh -RUN chmod +x /usr/local/bin/init-firewall.sh +COPY --chmod=755 init-firewall.sh /usr/local/bin/init-firewall.sh +COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh USER dev WORKDIR /workspace @@ -78,3 +80,5 @@ ENV HISTFILE=/home/dev/.commandhistory/.zsh_history ENV SHELL=/bin/zsh ENV DEVCONTAINER=true + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh new file mode 100755 index 0000000000..c8fd6019ae --- /dev/null +++ b/.devcontainer/entrypoint.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Ensure Claude Code directories exist (volumes may mount as empty) +mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory + +# Register GitHub MCP server if token is available +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + claude mcp add-json github \ + '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'"}}' \ + --scope user 2>/dev/null || true +fi + +exec "$@" diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index b2f23327b0..776fb523ff 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -2,11 +2,15 @@ # Launch Claude Code in the collector devcontainer with a task. # # Usage: -# .devcontainer/run.sh "task description" Worktree + /collector-dev:task (stream-json) +# .devcontainer/run.sh "task description" Worktree + /task (stream-json) # .devcontainer/run.sh --interactive Worktree + TUI -# .devcontainer/run.sh --local ["task"] Edit working tree directly, TUI +# .devcontainer/run.sh --local ["task"] Edit working tree directly # .devcontainer/run.sh --shell Shell into container # +# Options: +# --skip-submodules Skip submodule init (faster startup) +# --debug Pass --debug to claude for verbose logging +# # Prerequisites: # - Docker # - gcloud auth login && gcloud auth application-default login @@ -17,10 +21,27 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" -PLUGIN_DIR="/workspace/.claude/plugins/collector-dev" - -CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR") -CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --plugin-dir "$PLUGIN_DIR" --output-format stream-json --verbose) +SKIP_SUBMODULES=false +DEBUG=false + +# Parse global flags +ARGS=() +for arg in "$@"; do + case "$arg" in + --skip-submodules) SKIP_SUBMODULES=true ;; + --debug) DEBUG=true ;; + *) ARGS+=("$arg") ;; + esac +done +set -- "${ARGS[@]+"${ARGS[@]}"}" + +CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions) +CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --output-format stream-json --verbose) + +if [[ "$DEBUG" == "true" ]]; then + CLAUDE_INTERACTIVE+=(--debug) + CLAUDE_AUTONOMOUS+=(--debug) +fi # --- Preflight checks --- check_docker() { @@ -94,11 +115,15 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init \ - falcosecurity-libs \ - collector/proto/third_party/stackrox \ - >/dev/null 2>&1 + if [[ "$SKIP_SUBMODULES" != "true" ]]; then + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + >/dev/null 2>&1 + else + echo "Skipping submodule init (--skip-submodules)" >&2 + fi echo "$worktree_dir" } @@ -126,12 +151,13 @@ build_docker_args() { -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" -v "$HOME/.ssh:/home/dev/.ssh:ro" + -v "collector-dev-claude:/home/dev/.claude" -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json -w /workspace ) - for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID; do + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") fi @@ -178,21 +204,27 @@ case "${1:-}" in ""|--help|-h) cat < -export GOOGLE_CLOUD_LOCATION= # e.g., us-east5 -export ANTHROPIC_VERTEX_PROJECT_ID= -``` - -The devcontainer.json forwards these env vars into the container -automatically via `${localEnv:*}`. - -### Launch - -**VSCode:** Open the repo, click "Reopen in Container" when prompted. - -**CLI:** -```bash -# Build and start the devcontainer -devcontainer up --workspace-folder . - -# Or run Claude Code directly -docker run --rm \ - -v "$(pwd):/workspace" \ - -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" \ - -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" \ - -e CLAUDE_CODE_USE_VERTEX=1 \ - -e GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT \ - -e GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION \ - -e ANTHROPIC_VERTEX_PROJECT_ID=$ANTHROPIC_VERTEX_PROJECT_ID \ - -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud \ - -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json \ - -w /workspace \ - collector-dev:latest \ - claude --dangerously-skip-permissions -``` - -### Building Inside the Devcontainer - -The devcontainer IS the builder image — no nested Docker needed: +## Build (inside devcontainer) ```bash -# Configure (first time or after CMakeLists.txt changes) -cmake -S . -B cmake-build \ - -DCMAKE_BUILD_TYPE=Release \ +cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release \ -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) - -# Build (~30s incremental) cmake --build cmake-build -- -j$(nproc) - -# Unit tests (17 tests, ~13s) ctest --no-tests=error -V --test-dir cmake-build ``` -### Building on the Host (without devcontainer) - -## Quick Reference - -```bash -# Build (uses builder container with all C++ deps) -make start-builder # Start builder container (first time / after reboot) -make collector # Compile collector binary (~30s incremental) -make image # Build container image -make image-dev # Build dev image (with package manager, gdb) - -# Test -make unittest # C++ unit tests via ctest (~1 min) -CMAKE_BUILD_TYPE=Debug make unittest # With debug symbols +## Key Paths -# Lint -make check-clang-format-all # Check C++ formatting -make check-flake8-all # Check Python - -# Integration tests (local, requires Docker + privileged) -cd integration-tests -make TestProcessNetwork # Single test suite -make ci-integration-tests # Full suite (2h timeout) ``` - -## Architecture - +collector/lib/ C++ core library (~108 files) +collector/test/ Unit tests (GTest/GMock, 17 suites) +collector/collector.cpp Main entrypoint +falcosecurity-libs/ Submodule: eBPF engine + CO-RE BPF programs +integration-tests/ Go test framework (26 suites, needs privileged) ``` -collector/ # Main C++ application -├── lib/ # Core library (~108 files) -│ ├── KernelDriver.h # eBPF probe lifecycle (Setup/Start/Stop) -│ ├── CollectorService.cpp # Main service loop -│ ├── ConnTracker.cpp # Connection state machine -│ ├── NetworkConnection.h # IP/port/protocol structures -│ └── ProcessSignalHandler.h # Process event formatting -├── test/ # Unit tests (GTest/GMock) -├── container/Dockerfile # Production container (UBI minimal) -└── collector.cpp # Main entrypoint - -falcosecurity-libs/ # Submodule: eBPF engine -└── driver/modern_bpf/ # CO-RE BPF programs - ├── programs/attached/ # Tracepoint handlers (sys_enter, sys_exit, sched_*) - └── maps/ # BPF maps (ring buffers, tail call tables) - -builder/ # Builder image (CentOS Stream 10) -├── Dockerfile -└── install/ # Dependency build scripts (grpc, protobuf, libbpf, etc.) - -integration-tests/ # Go test framework (testify/suite) -├── suites/ # 26 test suites -├── pkg/mock_sensor/ # Mock gRPC sensor -└── pkg/executor/ # Container runtime abstraction - -ansible/ # VM lifecycle and test orchestration -├── integration-tests.yml # Create VM → provision → test → destroy -├── dev/ # Developer inventory (acs-team-sandbox) -└── roles/ # create-vm, provision-vm, run-test-target -``` - -## Development Workflow - -### For C++ / library changes (non-eBPF) - -Changes to `collector/lib/` that don't touch kernel interaction: - -1. Edit source files -2. `make collector` — compile (~30s incremental) -3. `make unittest` — run unit tests -4. Push PR — CI validates across platforms - -Unit tests cover: config parsing, connection tracking, network structures, -process filtering, event formatting, host info detection. - -### For eBPF / kernel driver changes - -Changes to `falcosecurity-libs/driver/modern_bpf/` or `collector/lib/KernelDriver.h`: - -1. Edit source files -2. `make collector` — compile (eBPF compiles to skeleton header) -3. `make unittest` — validates C++ logic only -4. **Push PR** — CI runs integration tests on real kernels -5. Monitor CI: `.github/workflows/integration-tests.yml` runs on - rhel, ubuntu, cos, flatcar, fedora-coreos across amd64/arm64/s390x/ppc64le - -**Unit tests CANNOT validate eBPF changes.** The BPF programs must load into -a real kernel with BTF support. CI handles this across 10+ VM types. - -### For integration test changes - -Changes to `integration-tests/`: - -1. Edit Go test files -2. Build test binary: `cd integration-tests && make build` -3. Run locally if Docker available: `make TestProcessNetwork` -4. Push PR — CI runs full matrix - -### Build Variables - -| Variable | Default | Purpose | -|----------|---------|---------| -| CMAKE_BUILD_TYPE | Release | Release or Debug | -| ADDRESS_SANITIZER | OFF | Enable AddressSanitizer | -| THREAD_SANITIZER | OFF | Enable ThreadSanitizer | -| USE_VALGRIND | OFF | Valgrind profiling | -| BPF_DEBUG_MODE | OFF | BPF debug output | -| COLLECTOR_BUILDER_TAG | master | Builder image version | - -### Running collector locally - -```yaml -# docker-compose.dev.yml pattern: -services: - collector: - image: quay.io/stackrox-io/collector:${COLLECTOR_TAG} - privileged: true - network_mode: host - environment: - - GRPC_SERVER=localhost:9999 - - COLLECTION_METHOD=core-bpf - - COLLECTOR_HOST_ROOT=/host - volumes: - - /var/run/docker.sock:/host/var/run/docker.sock:ro - - /proc:/host/proc:ro - - /etc:/host/etc:ro - - /sys/:/host/sys/:ro -``` - -Standalone mode (no gRPC server, outputs JSON to stdout): -```bash -collector --grpc-server= -``` - -### Hotreload on local K8s - -For rapid iteration without rebuilding the container image: -```bash -# Deploy stackrox to a local cluster first, then: -./utilities/hotreload.sh -# Recompile with: make -C collector container/bin/collector -``` - -## Key Dependencies - -- gRPC v1.67.0, Protobuf v28.3 -- libbpf v1.3.4, CO-RE BPF (kernel >= 5.8 with BTF) -- falcosecurity-libs (submodule, scap + sinsp) -- Google Test v1.15.2 - -## CI Pipeline - -Push to PR triggers `.github/workflows/main.yml`: -1. **init** — set tags, determine what to build -2. **build-collector** — multi-arch compile -3. **unit-tests** — ctest (Release, ASAN, Valgrind) -4. **integration-tests** — VM matrix (rhel, ubuntu, cos, flatcar, etc.) -5. **k8s-integration-tests** — KinD cluster tests -6. **benchmarks** — performance (master only or `run-benchmark` label) -### Triggering specific CI behavior +## Testing Rules -- Add `build-builder-image` label to rebuild the builder -- Add `run-benchmark` label for performance tests -- Add `update-baseline` label to update benchmark baseline +- Unit tests validate C++ logic only — no kernel needed +- eBPF changes CANNOT be tested locally — push PR, CI runs on real kernels +- CI matrix: rhel, ubuntu, cos, flatcar, fedora-coreos (amd64/arm64/s390x/ppc64le) -## File Conventions +## Conventions -- C++17, compiled with clang -- Format: `clang-format` (check with `make check-clang-format-all`) -- Integration tests: Go with testify/suite -- Shell scripts: `shfmt` + `shellcheck` -- Python: `flake8` -- Git hooks: `pre-commit` (run `make init-githook`) +- C++17, clang, `clang-format --style=file` +- Do not push to remote without explicit permission From bdb95eb420aefc70b7c25091f7d39417b96846ef Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 22:51:12 -0700 Subject: [PATCH 20/51] refactor: switch worktree to clone, consolidate to 2 skills, fix audit issues Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 2 + .claude/skills/build/SKILL.md | 44 --------- .claude/skills/ci-status/SKILL.md | 33 ------- .claude/skills/iterate/SKILL.md | 42 -------- .claude/skills/task/SKILL.md | 85 +++++------------ .claude/skills/watch-ci/SKILL.md | 84 ++++++++-------- .devcontainer/Dockerfile | 4 +- .devcontainer/devcontainer.json | 3 +- .devcontainer/entrypoint.sh | 8 +- .devcontainer/run.sh | 154 ++++++++++++------------------ CLAUDE.md | 7 +- 11 files changed, 142 insertions(+), 324 deletions(-) delete mode 100644 .claude/skills/build/SKILL.md delete mode 100644 .claude/skills/ci-status/SKILL.md delete mode 100644 .claude/skills/iterate/SKILL.md diff --git a/.claude/settings.json b/.claude/settings.json index 1f6f35d1f7..9d25a7a45d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,6 +1,8 @@ { "permissions": { "deny": [ + "Bash(git push *)", + "Bash(git push)", "mcp__github__merge_pull_request", "mcp__github__delete_file", "mcp__github__fork_repository", diff --git a/.claude/skills/build/SKILL.md b/.claude/skills/build/SKILL.md deleted file mode 100644 index ef2080aeaa..0000000000 --- a/.claude/skills/build/SKILL.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: build -description: Build collector binary with options (debug, asan, tsan, clean) -allowed-tools: Bash(cmake *), Bash(make *), Bash(nproc), Bash(git describe *), Bash(strip *), Read, Glob ---- - -# Build Collector - -Build the collector binary. Supports optional arguments: -- `debug` — Debug build with symbols -- `asan` — AddressSanitizer build -- `tsan` — ThreadSanitizer build -- `clean` — Clean build directory first -- (no args) — Release build - -## Steps - -1. Determine build environment: - - If inside the devcontainer (check: `DEVCONTAINER=true` env var), run cmake directly. - - If on the host (macOS), use `make start-builder && make collector`. - -2. If `clean` argument is provided, remove `cmake-build/` directory first. - -3. Set build variables based on arguments: - - `debug`: `CMAKE_BUILD_TYPE=Debug` - - `asan`: `CMAKE_BUILD_TYPE=Debug`, `ADDRESS_SANITIZER=ON` - - `tsan`: `CMAKE_BUILD_TYPE=Debug`, `THREAD_SANITIZER=ON` - - default: `CMAKE_BUILD_TYPE=Release` - -4. Run cmake configure (if `cmake-build/` doesn't exist or CMakeLists.txt changed): - ```bash - cmake -S . -B cmake-build \ - -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE \ - -DADDRESS_SANITIZER=$ADDRESS_SANITIZER \ - -DTHREAD_SANITIZER=$THREAD_SANITIZER \ - -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) - ``` - -5. Run cmake build: - ```bash - cmake --build cmake-build -- -j$(nproc) - ``` - -6. Report result: success with binary size, or failure with the first error and its file:line. diff --git a/.claude/skills/ci-status/SKILL.md b/.claude/skills/ci-status/SKILL.md deleted file mode 100644 index c4f98d88c7..0000000000 --- a/.claude/skills/ci-status/SKILL.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: ci-status -description: Check CI status on current PR, fetch failure logs, diagnose issues -allowed-tools: Bash(git branch *), Bash(git log *), Read ---- - -# CI Status - -Check CI pipeline status for the current branch/PR and diagnose failures. - -## Steps - -1. Get the current branch name from git. - -2. Use the GitHub MCP server to search for an open PR for this branch - in stackrox/collector. - -3. If a PR exists, get its check status via the GitHub MCP server. - -4. Get workflow runs for the branch via the GitHub MCP server. - -5. For any **failed runs**: - - Get the run details and job logs via the GitHub MCP server - - Identify which workflow failed (unit-tests, integration-tests, k8s-integration-tests, lint) - - For integration test failures, identify which VM type and test suite failed - -6. **Diagnose** the failure: - - Unit test failure: show the failing assertion and relevant source file - - Integration test failure: distinguish infra issues (VM creation, timeout) from test failures - - Lint failure: show which files need formatting - - Build failure: show the compiler error with file:line - -7. **Suggest next steps**: what code changes would fix the failure, or note if it's flaky/infra. diff --git a/.claude/skills/iterate/SKILL.md b/.claude/skills/iterate/SKILL.md deleted file mode 100644 index d1dad04f66..0000000000 --- a/.claude/skills/iterate/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: iterate -description: Full development cycle — build, unit test, format check, commit, push to existing branch -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep ---- - -# Iterate - -Run the full development inner loop. The branch and PR already exist — just build, test, and push. -Stops at the first failure. - -## Steps - -1. **Build** the collector: - - Detect environment (devcontainer vs host) - - In devcontainer: `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` - - On host: `make collector` - - **Stop on failure** — report the compiler error with file:line. - -2. **Unit test**: - - In devcontainer: `ctest --no-tests=error -V --test-dir cmake-build` - - On host: `make unittest` - - **Stop on failure** — report which test failed and the assertion. - -3. **Format check** (C++ files changed in this branch only): - - Get changed C++ files: `git diff --name-only origin/master...HEAD | grep -E '\.(cpp|h)$'` - - Run: `clang-format --style=file -n --Werror ` - - If formatting issues found, auto-fix them: `clang-format --style=file -i ` - - Report what was fixed. - -4. **Commit**: - - Stage changed files (source + any format fixes) - - Create a commit with a descriptive message summarizing the changes - -5. **Push**: - - `git push` to the existing branch (branch and PR already created by run.sh) - - Do NOT create new branches or PRs - -6. **Check CI**: - - Use the GitHub MCP server to check if CI has started - - Report the PR URL and note that CI is running - - Use `/collector-dev:ci-status` for detailed CI results once checks complete diff --git a/.claude/skills/task/SKILL.md b/.claude/skills/task/SKILL.md index 07c0abef4c..a3ec9779ef 100644 --- a/.claude/skills/task/SKILL.md +++ b/.claude/skills/task/SKILL.md @@ -1,84 +1,43 @@ --- name: task -description: End-to-end autonomous workflow — implement a task, push, create PR, monitor CI, fix failures until green +description: Implement a change — edit code, build, test, format, commit locally. No push. disable-model-invocation: true -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep, Agent +allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(clang-format *), Read, Write, Edit, Glob, Grep, Agent --- # Task -Complete a development task end-to-end: implement, build, test, push, create PR, and monitor CI until all checks pass. +Implement a change locally: edit, build, test, format, commit. +Do NOT push or create PRs — use /watch-ci for that. -## Input +## Steps -The task description is provided via $ARGUMENTS or in the initial prompt context (branch name, task). - -## Workflow - -### Phase 1: Implement - -1. Read and understand the task +1. Read and understand the task from $ARGUMENTS 2. Explore relevant code in the repository 3. Implement the changes -4. Build the collector: - - In devcontainer: `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` - - On host: `make collector` +4. Build: + - `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` - If build fails, fix and retry 5. Run unit tests: - - In devcontainer: `ctest --no-tests=error -V --test-dir cmake-build` - - On host: `make unittest` + - `ctest --no-tests=error -V --test-dir cmake-build` - If tests fail, fix and retry -6. Format check: - - `git diff --name-only origin/master...HEAD | grep -E '\.(cpp|h)$'` to find changed files - - `clang-format --style=file -i ` to fix formatting -7. Commit and push: +6. Format changed C++ files: + - `clang-format --style=file -i ` +7. Commit: - `git add` the changed files - `git commit` with a descriptive message - - `git push -u origin HEAD` -8. Create a draft PR: - - Use the GitHub MCP server to create a draft PR in stackrox/collector - - Title: brief summary of the task - - Body: describe what was changed and why - -### Phase 2: Monitor CI - -After pushing, enter a monitoring loop. CI typically takes 30-90 minutes. - -**Loop** (repeat until all checks pass or blocked): -1. Wait 10 minutes: `sleep 600` -2. Check CI status: - - Get current branch: `git branch --show-current` - - Use the GitHub MCP server to search for the PR and get workflow run status +## STOP here. Report and wait. -3. Evaluate: +Print this summary and then STOP. Do not continue with any other actions. - **All checks passed** → report success and stop - - **Checks still running** → report progress ("X of Y complete"), continue loop - - **Checks failed** → - - Use the GitHub MCP server to get job logs for the failed run - - Diagnose the failure: - - Build failure: read error, fix code - - Unit test failure: read assertion, fix code - - Lint failure: run clang-format - - Integration test infra flake (VM timeout, network): report as flake, continue loop - - Integration test real failure: analyze and fix code - - If fixable: fix → build → unit test → commit → push → continue loop - - If not fixable: report diagnosis and stop - -4. Safety limits: - - Maximum 6 CI cycles (about 3 hours of monitoring) - - If exceeded, report status and stop - -### Completion - -End with a summary: ``` -STATUS: PASSED | BLOCKED | TIMEOUT -Branch: claude/agent-xxx -PR: -Cycles: N -Changes: list of files modified +TASK COMPLETE +Branch: +Commit: +Files changed: +Tests: ``` + +The user will review and decide whether to run /watch-ci. +Do NOT push, create branches, or create PRs. diff --git a/.claude/skills/watch-ci/SKILL.md b/.claude/skills/watch-ci/SKILL.md index f5d2382f62..70ae6a32d2 100644 --- a/.claude/skills/watch-ci/SKILL.md +++ b/.claude/skills/watch-ci/SKILL.md @@ -1,53 +1,59 @@ --- name: watch-ci -description: Check CI status and react to failures — diagnose, fix, rebuild, push. Designed to run in a loop. -allowed-tools: Bash(cmake *), Bash(make *), Bash(ctest *), Bash(nproc), Bash(git *), Bash(clang-format *), Read, Write, Edit, Glob, Grep +description: Push to existing remote branch via GitHub MCP, create PR if needed, monitor CI, fix failures until green +disable-model-invocation: true +allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(git log *), Bash(git rev-parse *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep --- # Watch CI -Monitor CI for the current branch's PR and react to failures. Designed to be run -with `/loop 30m /collector-dev:watch-ci`. +Push commits via the GitHub MCP server, create PR if needed, and monitor CI until green. +Do NOT use `git push` — it is blocked. Use the GitHub MCP push_files tool instead. + +## Prerequisites + +The current branch must already have a remote tracking branch. Check with: +`git rev-parse --abbrev-ref --symbolic-full-name @{u}` +If this fails, stop and report: "No remote branch. Push from host first." ## Steps -1. **Find the PR** for the current branch: - - Get branch name: `git branch --show-current` - - Use the GitHub MCP server to search for the open PR in stackrox/collector - - If no PR found, report and stop +1. **Check remote branch exists**: + - `git rev-parse --abbrev-ref --symbolic-full-name @{u}` + - If this fails, stop -2. **Check CI status**: - - Use the GitHub MCP server to get PR check status and workflow runs +2. **Push** current commits: + - Use the GitHub MCP server push_files tool to push committed changes + - Do NOT use `git push` -3. **Evaluate state and act**: - - **If all checks pass:** - - Report: "All CI checks passed. PR is ready for review." - - Stop — no further action needed - - **If checks are still running:** - - Report: "CI still running (X of Y checks complete). Will check again next loop." - - Stop — wait for next loop iteration - - **If checks failed:** - - Use the GitHub MCP server to get job logs for the failed run - - Identify the failure type: - - **Build failure**: read compiler error, find the file:line, fix the code - - **Unit test failure**: read the assertion, find the test and source, fix the code - - **Integration test failure**: determine if it's a real failure or infra flake - - If infra flake (VM creation timeout, network issue): report and skip - - If real test failure: analyze the test expectation vs actual, fix the code - - **Lint failure**: run `clang-format --style=file -i` on the affected files - - After fixing: - - Build: `cmake --build cmake-build -- -j$(nproc)` - - Unit test: `ctest --no-tests=error -V --test-dir cmake-build` - - If build+test pass: `git add`, `git commit`, `git push` - - Report what was fixed and that a new CI run should start - - If the failure can't be fixed automatically, report the diagnosis and stop - -4. **Summary**: always end with a clear status line: +3. **Find or create PR**: + - Use the GitHub MCP server to search for an open PR for this branch + - If no PR exists, create a draft PR via the GitHub MCP server + +4. **Monitor CI loop** (repeat until all checks pass or blocked): + - Wait 10 minutes: `sleep 600` + - Use the GitHub MCP server to get PR check status and workflow runs + - Evaluate: + - **All checks passed** → report success and stop + - **Checks still running** → report progress, continue loop + - **Checks failed** → + - Get job logs via the GitHub MCP server + - Diagnose: + - Build failure: read error, fix code + - Unit test failure: read assertion, fix code + - Lint failure: run `clang-format --style=file -i` + - Integration test infra flake (VM timeout, network): report as flake, continue + - Integration test real failure: analyze and fix code + - If fixable: fix → build → test → commit → push via MCP → continue loop + - If not fixable: report diagnosis and stop + +5. **Safety limits**: + - Maximum 6 CI cycles (about 3 hours of monitoring) + - If exceeded, report status and stop + +6. **Summary**: end with a status line: - `PASSED` — all checks green - - `PENDING` — checks still running, will retry - - `FIXED` — failure diagnosed and fix pushed, awaiting new CI run + - `PENDING` — checks still running + - `FIXED` — failure diagnosed and fix pushed - `FLAKE` — infra failure, not a code issue - `BLOCKED` — failure requires human intervention diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 14bae718f0..c27184110d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,6 +13,7 @@ FROM quay.io/stackrox-io/collector-builder:${COLLECTOR_BUILDER_TAG} # bubblewrap: Claude Code uses this for built-in command sandboxing RUN dnf install -y \ bubblewrap \ + clang-tools-extra \ jq \ socat \ zsh \ @@ -56,9 +57,6 @@ RUN curl -fsSL https://sdk.cloud.google.com > /tmp/install-gcloud.sh \ && rm /tmp/install-gcloud.sh ENV PATH="/opt/google-cloud-sdk/bin:${PATH}" -# Pull GitHub MCP server image (used by Claude Code for GitHub operations) -# Configured in .claude/settings.json as an MCP server - # Create non-root dev user with passwordless sudo RUN useradd -m -s /bin/zsh dev \ && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 29c30ad638..8a0d841162 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -64,6 +64,7 @@ "CLAUDE_CODE_USE_VERTEX": "${localEnv:CLAUDE_CODE_USE_VERTEX}", "GOOGLE_CLOUD_PROJECT": "${localEnv:GOOGLE_CLOUD_PROJECT}", "GOOGLE_CLOUD_LOCATION": "${localEnv:GOOGLE_CLOUD_LOCATION}", - "ANTHROPIC_VERTEX_PROJECT_ID": "${localEnv:ANTHROPIC_VERTEX_PROJECT_ID}" + "ANTHROPIC_VERTEX_PROJECT_ID": "${localEnv:ANTHROPIC_VERTEX_PROJECT_ID}", + "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" } } diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index c8fd6019ae..6c104cb2a4 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -4,9 +4,13 @@ mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then - claude mcp add-json github \ + if ! claude mcp add-json github \ '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'"}}' \ - --scope user 2>/dev/null || true + --scope user 2>/dev/null; then + echo "WARNING: Failed to register GitHub MCP server" >&2 + fi +else + echo "NOTE: GITHUB_TOKEN not set — GitHub MCP tools unavailable" >&2 fi exec "$@" diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 776fb523ff..602fa9947f 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -2,19 +2,20 @@ # Launch Claude Code in the collector devcontainer with a task. # # Usage: -# .devcontainer/run.sh "task description" Worktree + /task (stream-json) -# .devcontainer/run.sh --interactive Worktree + TUI +# .devcontainer/run.sh "task description" Autonomous: /task then /watch-ci +# .devcontainer/run.sh --interactive Isolated clone + TUI # .devcontainer/run.sh --local ["task"] Edit working tree directly # .devcontainer/run.sh --shell Shell into container # # Options: -# --skip-submodules Skip submodule init (faster startup) -# --debug Pass --debug to claude for verbose logging +# --skip-submodules Skip submodule init +# --debug Verbose MCP/auth logging # # Prerequisites: # - Docker # - gcloud auth login && gcloud auth application-default login # - CLAUDE_CODE_USE_VERTEX=1 and related env vars (see CLAUDE.md) +# - GITHUB_TOKEN for GitHub MCP (PR creation, CI status) set -euo pipefail @@ -46,7 +47,7 @@ fi # --- Preflight checks --- check_docker() { if ! command -v docker &>/dev/null; then - echo "ERROR: docker not found. Install Docker Desktop, OrbStack, or Colima." >&2 + echo "ERROR: docker not found." >&2 exit 1 fi if ! docker info &>/dev/null 2>&1; then @@ -57,17 +58,15 @@ check_docker() { check_image() { if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then - echo "ERROR: Docker image '$IMAGE' not found." >&2 - echo "Build it with: docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 + echo "ERROR: Image '$IMAGE' not found. Build with:" >&2 + echo " docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 exit 1 fi } check_gcloud() { - local adc="$HOME/.config/gcloud/application_default_credentials.json" - if [[ ! -f "$adc" ]]; then - echo "ERROR: GCloud application default credentials not found at $adc" >&2 - echo "Run: gcloud auth application-default login" >&2 + if [[ ! -f "$HOME/.config/gcloud/application_default_credentials.json" ]]; then + echo "ERROR: Run: gcloud auth application-default login" >&2 exit 1 fi } @@ -77,72 +76,51 @@ check_vertex_env() { [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) - if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: Missing Vertex AI environment variables: ${missing[*]}" >&2 - echo "Set them in your shell profile (see CLAUDE.md):" >&2 - echo " export CLAUDE_CODE_USE_VERTEX=1" >&2 - echo " export GOOGLE_CLOUD_PROJECT=" >&2 - echo " export GOOGLE_CLOUD_LOCATION= # e.g., us-east5" >&2 - echo " export ANTHROPIC_VERTEX_PROJECT_ID=" >&2 + echo "ERROR: Missing env vars: ${missing[*]} (see CLAUDE.md)" >&2 exit 1 fi } -check_git_config() { - if [[ ! -f "$HOME/.gitconfig" ]]; then - echo "WARNING: ~/.gitconfig not found. Git operations inside container may fail." >&2 - fi - if [[ ! -d "$HOME/.ssh" ]]; then - echo "WARNING: ~/.ssh not found. Git push via SSH will not work." >&2 - fi -} - preflight() { check_docker check_image check_gcloud check_vertex_env - check_git_config + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "WARNING: GITHUB_TOKEN not set. GitHub MCP (PR creation, CI) will not work." >&2 + fi } -# --- Worktree isolation --- -setup_worktree() { - local task_id - task_id="agent-$(date +%s)-$$" +# --- Isolated clone (replaces worktree) --- +setup_clone() { + local task_id="agent-$(date +%s)-$$" local branch="claude/${task_id}" - local worktree_dir="/tmp/collector-${task_id}" + local clone_dir="/tmp/collector-${task_id}" - git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 + echo "Cloning repo..." >&2 + git clone --local --no-checkout "$REPO_ROOT" "$clone_dir" >/dev/null 2>&1 + git -C "$clone_dir" checkout -b "$branch" HEAD >/dev/null 2>&1 if [[ "$SKIP_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init \ + git -C "$clone_dir" submodule update --init \ falcosecurity-libs \ collector/proto/third_party/stackrox \ >/dev/null 2>&1 - else - echo "Skipping submodule init (--skip-submodules)" >&2 fi - echo "$worktree_dir" + echo "$clone_dir" } -cleanup_worktree() { - local worktree_dir="$1" - if [[ -d "$worktree_dir" ]]; then - local branch - branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) - git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true - if [[ -n "$branch" ]]; then - if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then - git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true - fi - fi +cleanup_clone() { + local clone_dir="$1" + if [[ -d "$clone_dir" ]]; then + rm -rf "$clone_dir" fi } -# --- Docker args --- +# --- Docker --- build_docker_args() { local workspace="$1" DOCKER_ARGS=( @@ -156,7 +134,6 @@ build_docker_args() { -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json -w /workspace ) - for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") @@ -168,14 +145,13 @@ build_docker_args() { case "${1:-}" in --interactive|-i) preflight - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - BRANCH=$(git -C "$WORKTREE" branch --show-current) - echo "Working in isolated worktree: $WORKTREE" + CLONE=$(setup_clone) + trap "cleanup_clone '$CLONE'" EXIT + BRANCH=$(git -C "$CLONE" branch --show-current) + echo "Working in isolated clone: $CLONE" echo "Branch: $BRANCH" - build_docker_args "$WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_INTERACTIVE[@]}" + build_docker_args "$CLONE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" ;; --local|-l) @@ -183,73 +159,61 @@ case "${1:-}" in preflight build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_INTERACTIVE[@]}" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_INTERACTIVE[@]}" -p "$*" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" -p "$*" fi ;; --shell|-s) - check_docker - check_image - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - echo "Working in isolated worktree: $WORKTREE" - build_docker_args "$WORKTREE" + check_docker; check_image + CLONE=$(setup_clone) + trap "cleanup_clone '$CLONE'" EXIT + echo "Working in isolated clone: $CLONE" + build_docker_args "$CLONE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh ;; ""|--help|-h) cat <&2 + echo "Working in isolated clone: $CLONE" >&2 echo "Branch: $BRANCH" >&2 echo "Task: $TASK" >&2 echo "---" >&2 - trap "cleanup_worktree '$WORKTREE'" EXIT + trap "cleanup_clone '$CLONE'" EXIT - build_docker_args "$WORKTREE" + build_docker_args "$CLONE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ "${CLAUDE_AUTONOMOUS[@]}" -p \ - "/task You are working on branch '$BRANCH'. - -Your task: $TASK + "/task $TASK -After implementing and testing, push with git and create a draft PR via the GitHub MCP server. Do not use gh CLI." +When /task completes, run /watch-ci to push, create a PR, and monitor CI until green." ;; esac diff --git a/CLAUDE.md b/CLAUDE.md index 8fc8371d9c..2ac6351353 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,10 @@ integration-tests/ Go test framework (26 suites, needs privileged) - eBPF changes CANNOT be tested locally — push PR, CI runs on real kernels - CI matrix: rhel, ubuntu, cos, flatcar, fedora-coreos (amd64/arm64/s390x/ppc64le) -## Conventions +## Git Rules +- NEVER run `git push` unless you are explicitly executing the /watch-ci skill +- NEVER create new branches +- You may use: git add, git commit, git diff, git status, git describe, git branch, git log +- Do NOT create PRs unless executing /watch-ci - C++17, clang, `clang-format --style=file` -- Do not push to remote without explicit permission From 1f8b8f6287454c26da904eb3c213b57981d8d73f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 22:52:19 -0700 Subject: [PATCH 21/51] fix: move git push deny to container-only settings The git push deny was blocking the host too. Move it to the entrypoint which writes /home/dev/.claude/settings.json (user scope) inside the container only. Project-level settings.json keeps only the MCP deny rules which apply everywhere. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.json | 2 -- .devcontainer/entrypoint.sh | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 9d25a7a45d..1f6f35d1f7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,8 +1,6 @@ { "permissions": { "deny": [ - "Bash(git push *)", - "Bash(git push)", "mcp__github__merge_pull_request", "mcp__github__delete_file", "mcp__github__fork_repository", diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 6c104cb2a4..6ed66531c3 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -2,6 +2,18 @@ # Ensure Claude Code directories exist (volumes may mount as empty) mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory +# Write container-only settings (deny git push — use GitHub MCP instead) +cat > /home/dev/.claude/settings.json <<'EOF' +{ + "permissions": { + "deny": [ + "Bash(git push *)", + "Bash(git push)" + ] + } +} +EOF + # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then if ! claude mcp add-json github \ From 768ebb2c81b0db4829c61782cb7b90bef08fdfa3 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 22:57:05 -0700 Subject: [PATCH 22/51] security: remove SSH mount, git push blocked by lack of credentials No SSH keys in container = git push fails at auth. No deny rule needed. GitHub MCP (PAT via GITHUB_TOKEN) is the only push path. Also removes git push deny from entrypoint settings. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/devcontainer.json | 1 - .devcontainer/entrypoint.sh | 12 ------------ .devcontainer/run.sh | 1 - 3 files changed, 14 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8a0d841162..32165a2ff0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,6 @@ "source=collector-dev-claude,target=/home/dev/.claude,type=volume", "source=${localEnv:HOME}/.gitconfig,target=/home/dev/.gitconfig,type=bind,readonly", "source=${localEnv:HOME}/.config/gcloud,target=/home/dev/.config/gcloud,type=bind,readonly", - "source=${localEnv:HOME}/.ssh,target=/home/dev/.ssh,type=bind,readonly", "source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly" ], diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 6ed66531c3..6c104cb2a4 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -2,18 +2,6 @@ # Ensure Claude Code directories exist (volumes may mount as empty) mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory -# Write container-only settings (deny git push — use GitHub MCP instead) -cat > /home/dev/.claude/settings.json <<'EOF' -{ - "permissions": { - "deny": [ - "Bash(git push *)", - "Bash(git push)" - ] - } -} -EOF - # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then if ! claude mcp add-json github \ diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 602fa9947f..08b8e0d098 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -128,7 +128,6 @@ build_docker_args() { -v "$workspace:/workspace" -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" - -v "$HOME/.ssh:/home/dev/.ssh:ro" -v "collector-dev-claude:/home/dev/.claude" -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json From dff9883535bf57006f3287cc9776eed2bae2d0eb Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 18 Mar 2026 23:06:04 -0700 Subject: [PATCH 23/51] fix: set clone remote to GitHub URL instead of local path git clone --local sets origin to the local filesystem path. Fix the remote to the real GitHub URL so git push works from the clone. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 08b8e0d098..dbf5431d66 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -100,6 +100,10 @@ setup_clone() { echo "Cloning repo..." >&2 git clone --local --no-checkout "$REPO_ROOT" "$clone_dir" >/dev/null 2>&1 + # Fix remote to point to GitHub, not the local path + local github_url + github_url=$(git -C "$REPO_ROOT" remote get-url origin) + git -C "$clone_dir" remote set-url origin "$github_url" git -C "$clone_dir" checkout -b "$branch" HEAD >/dev/null 2>&1 if [[ "$SKIP_SUBMODULES" != "true" ]]; then From 5c1b7996a6a440cf544ccf25f1d779b0314b5e95 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 09:57:27 -0700 Subject: [PATCH 24/51] refactor: switch back to worktrees, mount .git at same absolute path Mount the main repo's .git/ directory at its original absolute path inside the container (read-only). The worktree's .git file resolves correctly without any path patching. No cleanup hacks needed. Replaces the git clone --local approach which had issues with the remote URL pointing to the local filesystem. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 71 ++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index dbf5431d66..796ca22ffc 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -3,7 +3,7 @@ # # Usage: # .devcontainer/run.sh "task description" Autonomous: /task then /watch-ci -# .devcontainer/run.sh --interactive Isolated clone + TUI +# .devcontainer/run.sh --interactive Worktree + TUI # .devcontainer/run.sh --local ["task"] Edit working tree directly # .devcontainer/run.sh --shell Shell into container # @@ -92,35 +92,36 @@ preflight() { fi } -# --- Isolated clone (replaces worktree) --- -setup_clone() { +# --- Worktree --- +setup_worktree() { local task_id="agent-$(date +%s)-$$" local branch="claude/${task_id}" - local clone_dir="/tmp/collector-${task_id}" + local worktree_dir="/tmp/collector-${task_id}" - echo "Cloning repo..." >&2 - git clone --local --no-checkout "$REPO_ROOT" "$clone_dir" >/dev/null 2>&1 - # Fix remote to point to GitHub, not the local path - local github_url - github_url=$(git -C "$REPO_ROOT" remote get-url origin) - git -C "$clone_dir" remote set-url origin "$github_url" - git -C "$clone_dir" checkout -b "$branch" HEAD >/dev/null 2>&1 + git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 if [[ "$SKIP_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 - git -C "$clone_dir" submodule update --init \ + git -C "$worktree_dir" submodule update --init \ falcosecurity-libs \ collector/proto/third_party/stackrox \ >/dev/null 2>&1 fi - echo "$clone_dir" + echo "$worktree_dir" } -cleanup_clone() { - local clone_dir="$1" - if [[ -d "$clone_dir" ]]; then - rm -rf "$clone_dir" +cleanup_worktree() { + local worktree_dir="$1" + if [[ -d "$worktree_dir" ]]; then + local branch + branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) + git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true + if [[ -n "$branch" ]]; then + if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then + git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true + fi + fi fi } @@ -137,6 +138,10 @@ build_docker_args() { -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json -w /workspace ) + + # Mount .git at the same absolute path so worktree .git file resolves + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") @@ -148,12 +153,12 @@ build_docker_args() { case "${1:-}" in --interactive|-i) preflight - CLONE=$(setup_clone) - trap "cleanup_clone '$CLONE'" EXIT - BRANCH=$(git -C "$CLONE" branch --show-current) - echo "Working in isolated clone: $CLONE" + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + BRANCH=$(git -C "$WORKTREE" branch --show-current) + echo "Working in isolated worktree: $WORKTREE" echo "Branch: $BRANCH" - build_docker_args "$CLONE" + build_docker_args "$WORKTREE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" ;; @@ -170,18 +175,18 @@ case "${1:-}" in --shell|-s) check_docker; check_image - CLONE=$(setup_clone) - trap "cleanup_clone '$CLONE'" EXIT - echo "Working in isolated clone: $CLONE" - build_docker_args "$CLONE" + WORKTREE=$(setup_worktree) + trap "cleanup_worktree '$WORKTREE'" EXIT + echo "Working in isolated worktree: $WORKTREE" + build_docker_args "$WORKTREE" docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh ;; ""|--help|-h) cat <&2 + echo "Working in isolated worktree: $WORKTREE" >&2 echo "Branch: $BRANCH" >&2 echo "Task: $TASK" >&2 echo "---" >&2 - trap "cleanup_clone '$CLONE'" EXIT + trap "cleanup_worktree '$WORKTREE'" EXIT - build_docker_args "$CLONE" + build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ "${CLAUDE_AUTONOMOUS[@]}" -p \ "/task $TASK From ab2dd595fdff6a995ba9f6e09d67db325d3d583e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:05:33 -0700 Subject: [PATCH 25/51] perf: shallow submodule checkout in worktrees (--depth 1) Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 796ca22ffc..ba82f7bb9c 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -102,7 +102,7 @@ setup_worktree() { if [[ "$SKIP_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init \ + git -C "$worktree_dir" submodule update --init --depth 1 \ falcosecurity-libs \ collector/proto/third_party/stackrox \ >/dev/null 2>&1 From ef2fee91b0241f1ded1fad9a2aaae0cbe6d8fb3e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:06:56 -0700 Subject: [PATCH 26/51] perf: mount submodules from main repo instead of cloning per worktree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mount falcosecurity-libs and collector/proto/third_party/stackrox read-only from the main repo into the container. Eliminates submodule init entirely — worktree creation is now just git worktree add. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index ba82f7bb9c..877050c626 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -29,7 +29,7 @@ DEBUG=false ARGS=() for arg in "$@"; do case "$arg" in - --skip-submodules) SKIP_SUBMODULES=true ;; + --skip-submodules) ;; # no-op, submodules are mounted --debug) DEBUG=true ;; *) ARGS+=("$arg") ;; esac @@ -100,13 +100,8 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - if [[ "$SKIP_SUBMODULES" != "true" ]]; then - echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init --depth 1 \ - falcosecurity-libs \ - collector/proto/third_party/stackrox \ - >/dev/null 2>&1 - fi + # Submodules are mounted read-only into the container via docker args + # No need to init them in the worktree echo "$worktree_dir" } @@ -142,6 +137,10 @@ build_docker_args() { # Mount .git at the same absolute path so worktree .git file resolves DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") + # Mount submodules read-only from main repo (avoids cloning per worktree) + DOCKER_ARGS+=(-v "$REPO_ROOT/falcosecurity-libs:/workspace/falcosecurity-libs:ro") + DOCKER_ARGS+=(-v "$REPO_ROOT/collector/proto/third_party/stackrox:/workspace/collector/proto/third_party/stackrox:ro") + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") From 09bae7c7814eccf8dcb04ed3ec7b4954a1024b79 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:09:04 -0700 Subject: [PATCH 27/51] feat: make submodule mounting optional via --symlink-submodules Default: git submodule update --init --depth 1 (slower, independent copy) --symlink-submodules: mount from main repo read-only (instant, shared) Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 877050c626..ef63796622 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -8,7 +8,7 @@ # .devcontainer/run.sh --shell Shell into container # # Options: -# --skip-submodules Skip submodule init +# --symlink-submodules Mount submodules from main repo (fast, read-only) # --debug Verbose MCP/auth logging # # Prerequisites: @@ -22,14 +22,14 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" -SKIP_SUBMODULES=false +SYMLINK_SUBMODULES=false DEBUG=false # Parse global flags ARGS=() for arg in "$@"; do case "$arg" in - --skip-submodules) ;; # no-op, submodules are mounted + --symlink-submodules) SYMLINK_SUBMODULES=true ;; --debug) DEBUG=true ;; *) ARGS+=("$arg") ;; esac @@ -100,8 +100,13 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - # Submodules are mounted read-only into the container via docker args - # No need to init them in the worktree + if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init --depth 1 \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + >/dev/null 2>&1 + fi echo "$worktree_dir" } @@ -137,9 +142,11 @@ build_docker_args() { # Mount .git at the same absolute path so worktree .git file resolves DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") - # Mount submodules read-only from main repo (avoids cloning per worktree) - DOCKER_ARGS+=(-v "$REPO_ROOT/falcosecurity-libs:/workspace/falcosecurity-libs:ro") - DOCKER_ARGS+=(-v "$REPO_ROOT/collector/proto/third_party/stackrox:/workspace/collector/proto/third_party/stackrox:ro") + # Optionally mount submodules from main repo instead of cloning + if [[ "$SYMLINK_SUBMODULES" == "true" ]]; then + DOCKER_ARGS+=(-v "$REPO_ROOT/falcosecurity-libs:/workspace/falcosecurity-libs:ro") + DOCKER_ARGS+=(-v "$REPO_ROOT/collector/proto/third_party/stackrox:/workspace/collector/proto/third_party/stackrox:ro") + fi for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then @@ -190,7 +197,7 @@ Usage: $0 --shell Shell into the container Options: - --skip-submodules Skip submodule init (faster startup) + --symlink-submodules Mount submodules from main repo (faster, read-only) --debug Verbose MCP/auth logging Environment: From e2a40857cfada5cd781693d74bbe0984ce23e893 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:11:38 -0700 Subject: [PATCH 28/51] feat: add --branch flag, use /tmp/collector-worktrees/ for worktrees --branch sets a custom branch name instead of auto-generated. Worktrees now created under /tmp/collector-worktrees/ with slashes replaced by dashes in the directory name. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index ef63796622..0a38cdbc62 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -8,6 +8,7 @@ # .devcontainer/run.sh --shell Shell into container # # Options: +# --branch Branch name (default: claude/agent-) # --symlink-submodules Mount submodules from main repo (fast, read-only) # --debug Verbose MCP/auth logging # @@ -22,8 +23,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" +WORKTREE_BASE="/tmp/collector-worktrees" SYMLINK_SUBMODULES=false DEBUG=false +BRANCH_NAME="" # Parse global flags ARGS=() @@ -31,7 +34,15 @@ for arg in "$@"; do case "$arg" in --symlink-submodules) SYMLINK_SUBMODULES=true ;; --debug) DEBUG=true ;; - *) ARGS+=("$arg") ;; + --branch=*) BRANCH_NAME="${arg#--branch=}" ;; + --branch) BRANCH_NAME="__NEXT__" ;; + *) + if [[ "$BRANCH_NAME" == "__NEXT__" ]]; then + BRANCH_NAME="$arg" + else + ARGS+=("$arg") + fi + ;; esac done set -- "${ARGS[@]+"${ARGS[@]}"}" @@ -94,10 +105,16 @@ preflight() { # --- Worktree --- setup_worktree() { - local task_id="agent-$(date +%s)-$$" - local branch="claude/${task_id}" - local worktree_dir="/tmp/collector-${task_id}" + local branch + if [[ -n "$BRANCH_NAME" ]]; then + branch="$BRANCH_NAME" + else + branch="claude/agent-$(date +%s)-$$" + fi + local safe_name="${branch//\//-}" + local worktree_dir="${WORKTREE_BASE}/${safe_name}" + mkdir -p "$WORKTREE_BASE" git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then @@ -197,6 +214,7 @@ Usage: $0 --shell Shell into the container Options: + --branch Branch name (default: claude/agent-) --symlink-submodules Mount submodules from main repo (faster, read-only) --debug Verbose MCP/auth logging From 3295488cdb00e8b55a4edd3faedbd005509262ca Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:12:56 -0700 Subject: [PATCH 29/51] fix: reject --branch with --local mode Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 0a38cdbc62..7258d64380 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -187,6 +187,10 @@ case "${1:-}" in --local|-l) shift + if [[ -n "$BRANCH_NAME" ]]; then + echo "ERROR: --branch cannot be used with --local" >&2 + exit 1 + fi preflight build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then From bc74ac4f34f727b4f76d896d68eba59ae6571a9f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:15:24 -0700 Subject: [PATCH 30/51] fix: mount .git read-write so git commit works in container git commit writes to .git/worktrees// (HEAD, index, refs). Read-only mount was blocking commits. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 7258d64380..5d15a65b64 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -157,7 +157,8 @@ build_docker_args() { ) # Mount .git at the same absolute path so worktree .git file resolves - DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") + # Read-write needed: git commit writes to .git/worktrees// + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") # Optionally mount submodules from main repo instead of cloning if [[ "$SYMLINK_SUBMODULES" == "true" ]]; then From 48da9b7a00d557b2c2ac88a41a3eff940595c917 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:17:00 -0700 Subject: [PATCH 31/51] security: mount .git read-only with worktree subdir read-write .git/ is mounted read-only so the agent can't modify shared refs, objects, or config. Only .git/worktrees// is read-write, which is what git commit needs (HEAD, index, refs for that branch). Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 5d15a65b64..ed94e9ea9b 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -116,6 +116,8 @@ setup_worktree() { mkdir -p "$WORKTREE_BASE" git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 + # Export worktree name for docker mount scoping + WORKTREE_GIT_NAME=$(basename "$worktree_dir") if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 @@ -156,9 +158,12 @@ build_docker_args() { -w /workspace ) - # Mount .git at the same absolute path so worktree .git file resolves - # Read-write needed: git commit writes to .git/worktrees// - DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") + # Mount .git read-only, then override the worktree subdir as read-write + # so git commit works but the agent can't modify shared refs/objects/config + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") + if [[ -n "${WORKTREE_GIT_NAME:-}" ]]; then + DOCKER_ARGS+=(-v "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME:$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME") + fi # Optionally mount submodules from main repo instead of cloning if [[ "$SYMLINK_SUBMODULES" == "true" ]]; then From 356e1d8a05bbab8637b544eaa206f07febdb6183 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:18:27 -0700 Subject: [PATCH 32/51] fix: set theme and verbose defaults in entrypoint to skip startup prompts Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 6c104cb2a4..3dbaabf10a 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -2,6 +2,10 @@ # Ensure Claude Code directories exist (volumes may mount as empty) mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory +# Set defaults so Claude Code doesn't prompt on startup +claude config set --global theme dark 2>/dev/null || true +claude config set --global verbose false 2>/dev/null || true + # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then if ! claude mcp add-json github \ From 480e9aa673896503f6a6c753635771182718aa2c Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:28:57 -0700 Subject: [PATCH 33/51] fix: chmod worktree git dir for container uid mismatch Host uid (502) != container uid (1000). Make the worktree's .git/worktrees// world-writable so the container's dev user can write index.lock, HEAD, etc. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index ed94e9ea9b..29543bdf3c 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -118,6 +118,8 @@ setup_worktree() { git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 # Export worktree name for docker mount scoping WORKTREE_GIT_NAME=$(basename "$worktree_dir") + # Make worktree git dir writable by container user (different uid) + chmod -R a+rw "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME" if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 From e595f883846aa0a6f0f943a0979016557b24b9a6 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:37:17 -0700 Subject: [PATCH 34/51] fix: use chmod a+rwX to fix directory permissions in worktree git dir a+rw doesn't add execute on directories, so creating files inside modules/ subdirs fails. a+rwX adds execute on directories only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 29543bdf3c..536c8027bc 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -119,7 +119,7 @@ setup_worktree() { # Export worktree name for docker mount scoping WORKTREE_GIT_NAME=$(basename "$worktree_dir") # Make worktree git dir writable by container user (different uid) - chmod -R a+rw "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME" + chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME" if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 From 4683ad370bdc78ecfe74ef86c0fd1350e19090f6 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:43:39 -0700 Subject: [PATCH 35/51] fix: mount .git read-write, partial ro/rw overlay doesn't work Submodule init and git commit both need to write to various places under .git/ (worktrees/, modules/, index.lock). The ro mount with rw overlay on the worktree subdir is insufficient. Mount .git rw. Agent still can't push (no SSH keys). Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 536c8027bc..b758a35e6d 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -116,10 +116,10 @@ setup_worktree() { mkdir -p "$WORKTREE_BASE" git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - # Export worktree name for docker mount scoping - WORKTREE_GIT_NAME=$(basename "$worktree_dir") # Make worktree git dir writable by container user (different uid) - chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME" + local worktree_git_name + worktree_git_name=$(basename "$worktree_dir") + chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then echo "Initializing submodules..." >&2 @@ -160,12 +160,10 @@ build_docker_args() { -w /workspace ) - # Mount .git read-only, then override the worktree subdir as read-write - # so git commit works but the agent can't modify shared refs/objects/config - DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git:ro") - if [[ -n "${WORKTREE_GIT_NAME:-}" ]]; then - DOCKER_ARGS+=(-v "$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME:$REPO_ROOT/.git/worktrees/$WORKTREE_GIT_NAME") - fi + # Mount .git read-write so worktree git operations work + # (commits, submodule state, index.lock all write here) + # Agent can't push (no SSH keys) so risk is limited to local git state + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") # Optionally mount submodules from main repo instead of cloning if [[ "$SYMLINK_SUBMODULES" == "true" ]]; then From 94418f7418c09762a8195c45fd7ff4dda5b15ac6 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 10:51:07 -0700 Subject: [PATCH 36/51] security: remove SYS_PTRACE/NET_ADMIN/NET_RAW caps, drop --symlink-submodules - Remove SYS_PTRACE (container escape risk), NET_ADMIN, NET_RAW from devcontainer.json runArgs - Remove --symlink-submodules flag entirely (broke cmake builds, added complexity for marginal speedup) - Show submodule init progress instead of silencing output Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/devcontainer.json | 3 --- .devcontainer/run.sh | 27 +++++++-------------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 32165a2ff0..863b8976da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,9 +19,6 @@ ], "runArgs": [ - "--cap-add=SYS_PTRACE", - "--cap-add=NET_ADMIN", - "--cap-add=NET_RAW", "--init" ], diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index b758a35e6d..5f8b51a8a1 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -9,7 +9,6 @@ # # Options: # --branch Branch name (default: claude/agent-) -# --symlink-submodules Mount submodules from main repo (fast, read-only) # --debug Verbose MCP/auth logging # # Prerequisites: @@ -24,7 +23,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" WORKTREE_BASE="/tmp/collector-worktrees" -SYMLINK_SUBMODULES=false DEBUG=false BRANCH_NAME="" @@ -32,7 +30,6 @@ BRANCH_NAME="" ARGS=() for arg in "$@"; do case "$arg" in - --symlink-submodules) SYMLINK_SUBMODULES=true ;; --debug) DEBUG=true ;; --branch=*) BRANCH_NAME="${arg#--branch=}" ;; --branch) BRANCH_NAME="__NEXT__" ;; @@ -116,18 +113,17 @@ setup_worktree() { mkdir -p "$WORKTREE_BASE" git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 + # Make worktree git dir writable by container user (different uid) local worktree_git_name worktree_git_name=$(basename "$worktree_dir") chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" - if [[ "$SYMLINK_SUBMODULES" != "true" ]]; then - echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init --depth 1 \ - falcosecurity-libs \ - collector/proto/third_party/stackrox \ - >/dev/null 2>&1 - fi + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init --depth 1 \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + 2>&1 | sed 's/^/ /' >&2 echo "$worktree_dir" } @@ -160,17 +156,9 @@ build_docker_args() { -w /workspace ) - # Mount .git read-write so worktree git operations work - # (commits, submodule state, index.lock all write here) - # Agent can't push (no SSH keys) so risk is limited to local git state + # Mount .git so worktree resolves (agent can't push — no SSH keys) DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") - # Optionally mount submodules from main repo instead of cloning - if [[ "$SYMLINK_SUBMODULES" == "true" ]]; then - DOCKER_ARGS+=(-v "$REPO_ROOT/falcosecurity-libs:/workspace/falcosecurity-libs:ro") - DOCKER_ARGS+=(-v "$REPO_ROOT/collector/proto/third_party/stackrox:/workspace/collector/proto/third_party/stackrox:ro") - fi - for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do if [[ -n "${!var:-}" ]]; then DOCKER_ARGS+=(-e "$var=${!var}") @@ -225,7 +213,6 @@ Usage: Options: --branch Branch name (default: claude/agent-) - --symlink-submodules Mount submodules from main repo (faster, read-only) --debug Verbose MCP/auth logging Environment: From db98d81837b915142024bf7d27807450e5abb4fc Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 11:01:22 -0700 Subject: [PATCH 37/51] feat: specify GitHub MCP toolsets via X-MCP-Toolsets header Request repos, pull_requests, and actions toolsets so the agent can create PRs, push files, list workflow runs, and download job logs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 3dbaabf10a..a9da08a618 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -9,7 +9,7 @@ claude config set --global verbose false 2>/dev/null || true # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then if ! claude mcp add-json github \ - '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'"}}' \ + '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'","X-MCP-Toolsets":"repos,pull_requests,actions"}}' \ --scope user 2>/dev/null; then echo "WARNING: Failed to register GitHub MCP server" >&2 fi From 0c538c46745944d197ca1aaecedd8c84c1b7a024 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 12:52:12 -0700 Subject: [PATCH 38/51] feat: watch-ci updates PR body with agent status after each CI cycle Adds an Agent Status section to the PR description with timestamp, last commit SHA, cycle count, status, and one-line details. Keeps the original PR description above the status section. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/watch-ci/SKILL.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.claude/skills/watch-ci/SKILL.md b/.claude/skills/watch-ci/SKILL.md index 70ae6a32d2..a1fb775eef 100644 --- a/.claude/skills/watch-ci/SKILL.md +++ b/.claude/skills/watch-ci/SKILL.md @@ -2,7 +2,7 @@ name: watch-ci description: Push to existing remote branch via GitHub MCP, create PR if needed, monitor CI, fix failures until green disable-model-invocation: true -allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(git log *), Bash(git rev-parse *), Bash(clang-format *), Bash(sleep *), Read, Write, Edit, Glob, Grep +allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(git log *), Bash(git rev-parse *), Bash(clang-format *), Bash(sleep *), Bash(date *), Read, Write, Edit, Glob, Grep --- # Watch CI @@ -34,8 +34,8 @@ If this fails, stop and report: "No remote branch. Push from host first." - Wait 10 minutes: `sleep 600` - Use the GitHub MCP server to get PR check status and workflow runs - Evaluate: - - **All checks passed** → report success and stop - - **Checks still running** → report progress, continue loop + - **All checks passed** → update PR body, report success and stop + - **Checks still running** → update PR body, report progress, continue loop - **Checks failed** → - Get job logs via the GitHub MCP server - Diagnose: @@ -44,14 +44,27 @@ If this fails, stop and report: "No remote branch. Push from host first." - Lint failure: run `clang-format --style=file -i` - Integration test infra flake (VM timeout, network): report as flake, continue - Integration test real failure: analyze and fix code - - If fixable: fix → build → test → commit → push via MCP → continue loop - - If not fixable: report diagnosis and stop + - If fixable: fix → build → test → commit → push via MCP → update PR body → continue loop + - If not fixable: update PR body, report diagnosis and stop -5. **Safety limits**: +5. **Update PR body** after each CI check with a status section at the bottom: + - Use the GitHub MCP server to update the PR description + - Append or replace a `## Agent Status` section with: + ``` + ## Agent Status + **Last updated:** + **Last commit:** + **CI cycle:** N of 6 + **Status:** PENDING | PASSED | FIXED | FLAKE | BLOCKED + **Details:** + ``` + - Keep the original PR description above the Agent Status section + +6. **Safety limits**: - Maximum 6 CI cycles (about 3 hours of monitoring) - - If exceeded, report status and stop + - If exceeded, update PR body and stop -6. **Summary**: end with a status line: +7. **Summary**: end with a status line: - `PASSED` — all checks green - `PENDING` — checks still running - `FIXED` — failure diagnosed and fix pushed From 4cf15e24a23b7efb132f6f7fe0904d4a43166c5c Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 13:40:08 -0700 Subject: [PATCH 39/51] docs: add MCP Context Protector integration plan Planning doc for Trail of Bits mcp-context-protector as a security proxy for GitHub MCP. Covers architecture, open questions, threat model comparison, and phased implementation approach. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/mcp_protector_plan.md | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .devcontainer/mcp_protector_plan.md diff --git a/.devcontainer/mcp_protector_plan.md b/.devcontainer/mcp_protector_plan.md new file mode 100644 index 0000000000..5475ccdd51 --- /dev/null +++ b/.devcontainer/mcp_protector_plan.md @@ -0,0 +1,122 @@ +# MCP Context Protector Integration Plan + +Trail of Bits [MCP Context Protector](https://github.com/trailofbits/mcp-context-protector) +as a security proxy between Claude Code and the GitHub MCP server. + +## Architecture + +``` +Claude Code → MCP Context Protector (stdio) → GitHub MCP (HTTP) + │ + ├── TOFU pinning: detect tool description changes + ├── ANSI sanitization: strip escape sequences from responses + ├── Guardrails (optional): filter prompt injection in responses + └── Quarantine: flag suspicious content for review +``` + +## Value for Our Setup + +1. **CI log injection defense** — failed CI job logs could contain crafted text + that manipulates the agent (e.g., fake error messages suggesting `git push --force`). + The guardrail would detect and filter these. + +2. **TOFU pinning** — alerts if the GitHub MCP server changes its tool definitions + between sessions. Detects supply chain changes to the MCP server. + +3. **ANSI sanitization** — strips hidden terminal escape sequences from GitHub API + responses that could hide instructions from the user but be visible to the model. + +4. **Audit trail** — quarantined responses are stored and reviewable via + `--review-quarantine`. + +## Integration Approach + +### Current (direct HTTP) +```bash +claude mcp add-json github \ + '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{...}}' +``` + +### With Protector (stdio wrapping HTTP) +```bash +claude mcp add-json github \ + '{"command":"mcp-context-protector","args":["--url","https://api.githubcopilot.com/mcp","--headers","Authorization: Bearer TOKEN","--headers","X-MCP-Toolsets: repos,pull_requests,actions"]}' +``` + +## Open Questions + +### 1. Header forwarding +Does `--url` mode support custom headers (`Authorization`, `X-MCP-Toolsets`)? +The docs show `--url` but no header examples. Need to check source code or test. + +### 2. LlamaFirewall dependency +Full guardrail protection requires LlamaFirewall (`--guardrail-provider LlamaFirewall`). +This is a separate ML model — likely large, adds significant image size and startup time. +Without it, only TOFU pinning and ANSI sanitization are available. +- Is TOFU + ANSI enough for our threat model? +- Can we run LlamaFirewall as a sidecar instead of in the same container? + +### 3. Python / uv dependency +The protector uses Python + uv. We have Python 3 in the builder image but not uv. +Options: +- `pip install mcp-context-protector` (if published to PyPI) +- `uv sync` from git clone in Dockerfile +- Vendor the protector as a script in .devcontainer/ + +### 4. State persistence +TOFU pinned configs must persist across container restarts. +Options: +- Store in the `collector-dev-claude` volume (`/home/dev/.claude/`) +- Separate volume for protector state +- Environment variable for state directory + +### 5. Startup latency +Adds another process to the MCP chain. Need to measure: +- Protector initialization time +- Per-request overhead (especially for large CI log responses) +- Impact on `/watch-ci` polling loop + +### 6. Toolset filtering compatibility +We use `X-MCP-Toolsets: repos,pull_requests,actions` to limit which tools +the GitHub MCP server exposes. Need to verify the protector passes this +header through and doesn't interfere with tool discovery. + +### 7. Quarantine review in headless mode +`--review-quarantine` is interactive. In autonomous mode (stream-json), +how would quarantined responses be surfaced? Options: +- Log to stderr (visible in stream output) +- Write to a file in /workspace for post-session review +- Fail the MCP call so the agent reports it as a blocker + +## Threat Model Comparison + +| Threat | Without Protector | With Protector | +|--------|------------------|----------------| +| CI log prompt injection | Unmitigated | Guardrail filters (with LlamaFirewall) | +| GitHub MCP tool definition change | Undetected | TOFU alerts on change | +| ANSI hidden instructions in responses | Unmitigated | Stripped and replaced | +| Token exfiltration via MCP responses | Unmitigated | Guardrail may detect | +| Credential in PR body/comments | Unmitigated | Guardrail may detect | +| MCP server impersonation | Undetected | TOFU pinning prevents | + +## Recommendation + +**Phase 1 (low effort):** Add TOFU pinning + ANSI sanitization only. +No LlamaFirewall. Validates tool definitions haven't changed, strips +escape sequences. Minimal dependencies, minimal latency. + +**Phase 2 (medium effort):** Add LlamaFirewall as a sidecar container. +Full prompt injection defense for CI logs and GitHub API responses. +Requires docker-compose or multi-container setup. + +**Phase 3 (high effort):** Custom guardrail rules specific to collector +development — detect patterns like "ignore previous instructions", +credential patterns in PR bodies, suspicious git commands in CI logs. + +## Prerequisites Before Starting + +- [ ] Verify `--url` supports custom headers (check source) +- [ ] Test basic TOFU mode without LlamaFirewall +- [ ] Measure startup latency overhead +- [ ] Confirm TOFU state directory is configurable +- [ ] Test with `X-MCP-Toolsets` header passthrough From 5369867ebbdac7ab741185a56e4e01a2122fb898 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 13:59:22 -0700 Subject: [PATCH 40/51] fix: autonomous mode inlines task instructions instead of invoking /task skill The /task skill's STOP condition caused the agent to exit before running /watch-ci. Instead, give the agent inline instructions for step 1 (implement) and explicitly tell it to run /watch-ci for step 2. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 5f8b51a8a1..9e53b25e9c 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -241,8 +241,11 @@ USAGE build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ "${CLAUDE_AUTONOMOUS[@]}" -p \ - "/task $TASK + "Complete this task end-to-end. Do NOT stop until CI is green. -When /task completes, run /watch-ci to push, create a PR, and monitor CI until green." +Step 1: Implement the task — edit code, build, test, format, commit locally. +Step 2: Run /watch-ci to push via GitHub MCP, create a PR, and monitor CI until green. + +Your task: $TASK" ;; esac From d1c71dcf7b635a7cb8be6f17368267a1867b1341 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:01:32 -0700 Subject: [PATCH 41/51] feat: add /dev-loop skill combining task + watch-ci + PR comment handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single skill for the full autonomous cycle: implement → build → test → commit → push via MCP → create PR → monitor CI → address review comments → loop until green. run.sh autonomous mode now invokes /dev-loop directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/dev-loop/SKILL.md | 76 ++++++++++++++++++++++++++++++++ .devcontainer/run.sh | 8 +--- 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/dev-loop/SKILL.md diff --git a/.claude/skills/dev-loop/SKILL.md b/.claude/skills/dev-loop/SKILL.md new file mode 100644 index 0000000000..ad48a386a2 --- /dev/null +++ b/.claude/skills/dev-loop/SKILL.md @@ -0,0 +1,76 @@ +--- +name: dev-loop +description: Full autonomous development loop — implement, build, test, commit, push, create PR, monitor CI, fix failures until green +disable-model-invocation: true +allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(git log *), Bash(git rev-parse *), Bash(clang-format *), Bash(sleep *), Bash(date *), Read, Write, Edit, Glob, Grep, Agent +--- + +# Dev Loop + +Complete a development task end-to-end: implement, build, test, push, create PR, monitor CI, fix failures. +Do NOT stop until CI is green or you are blocked. + +## Phase 1: Implement + +1. Read and understand the task from $ARGUMENTS +2. Explore relevant code +3. Implement the changes +4. Build: `cmake -S . -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCOLLECTOR_VERSION=$(git describe --tags --abbrev=10 --long) && cmake --build cmake-build -- -j$(nproc)` + - If build fails, fix and retry +5. Test: `ctest --no-tests=error -V --test-dir cmake-build` + - If tests fail, fix and retry +6. Format: `clang-format --style=file -i ` +7. Commit: `git add` changed files, `git commit` with a descriptive message + +## Phase 2: Push and create PR + +1. Check if remote branch exists: `git rev-parse --abbrev-ref --symbolic-full-name @{u}` + - If no remote branch, stop and report: "No remote branch. Push from host first." +2. Push via the GitHub MCP server push_files tool (do NOT use `git push`) +3. Search for an open PR for this branch via GitHub MCP +4. If no PR exists, create a draft PR via GitHub MCP + +## Phase 3: Monitor CI + +Loop until all checks pass or blocked (max 6 cycles, ~3 hours): + +1. Wait 10 minutes: `sleep 600` +2. Check CI status via GitHub MCP (PR checks, workflow runs) +3. Update PR body with an `## Agent Status` section: + ``` + ## Agent Status + **Last updated:** <`date -u +"%Y-%m-%d %H:%M UTC"`> + **Last commit:** <`git rev-parse --short HEAD`> + **CI cycle:** N of 6 + **Status:** PENDING | PASSED | FIXED | FLAKE | BLOCKED + **Details:** + ``` +4. Evaluate: + - **All checks passed** → update PR body, report success, stop + - **Still running** → continue loop + - **Failed** → + - Get job logs via GitHub MCP + - Diagnose: build error, test assertion, lint, infra flake + - If fixable: fix → build → test → commit → push via MCP → continue + - If infra flake: note as FLAKE, continue + - If not fixable: update PR body, report BLOCKED, stop + +## Phase 4: Check PR comments + +Before each CI cycle, check if there are new PR review comments via GitHub MCP. +If a reviewer left feedback: +- Address the feedback (edit code, fix issues) +- Build and test +- Commit and push via MCP +- Note in the Agent Status section what feedback was addressed + +## Completion + +Print summary: +``` +STATUS: PASSED | BLOCKED | TIMEOUT +Branch: +PR: +Cycles: N +Changes: +``` diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 9e53b25e9c..918eb794a4 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -240,12 +240,6 @@ USAGE build_docker_args "$WORKTREE" docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p \ - "Complete this task end-to-end. Do NOT stop until CI is green. - -Step 1: Implement the task — edit code, build, test, format, commit locally. -Step 2: Run /watch-ci to push via GitHub MCP, create a PR, and monitor CI until green. - -Your task: $TASK" + "${CLAUDE_AUTONOMOUS[@]}" -p "/dev-loop $TASK" ;; esac From 69d62c248f4ca5079bfb871e44588b4d4d24b31f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:05:59 -0700 Subject: [PATCH 42/51] feat: allow specifying skill in autonomous mode, default to /dev-loop If the task starts with /, pass it as-is (e.g., /task, /watch-ci). Otherwise default to /dev-loop. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 918eb794a4..53d73d03c3 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -239,7 +239,14 @@ USAGE trap "cleanup_worktree '$WORKTREE'" EXIT build_docker_args "$WORKTREE" + # If task starts with /, treat as a skill invocation; otherwise default to /dev-loop + local prompt + if [[ "$TASK" == /* ]]; then + prompt="$TASK" + else + prompt="/dev-loop $TASK" + fi docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "/dev-loop $TASK" + "${CLAUDE_AUTONOMOUS[@]}" -p "$prompt" ;; esac From 9835cd8ae364cc146cbe966333291555826a0727 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:08:27 -0700 Subject: [PATCH 43/51] fix: remove local keyword from case branch (not a function) Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 53d73d03c3..9c878964ac 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -240,13 +240,11 @@ USAGE build_docker_args "$WORKTREE" # If task starts with /, treat as a skill invocation; otherwise default to /dev-loop - local prompt + PROMPT="/dev-loop $TASK" if [[ "$TASK" == /* ]]; then - prompt="$TASK" - else - prompt="/dev-loop $TASK" + PROMPT="$TASK" fi docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$prompt" + "${CLAUDE_AUTONOMOUS[@]}" -p "$PROMPT" ;; esac From 8672c7764a223efa30ef543cc5d65d0347c47a25 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:17:42 -0700 Subject: [PATCH 44/51] =?UTF-8?q?docs:=20update=20MCP=20security=20plan=20?= =?UTF-8?q?=E2=80=94=20recommend=20mcp-watchdog=20over=20mcp-context-prote?= =?UTF-8?q?ctor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcp-watchdog is lightweight (pattern-based, no ML), covers 70+ attack patterns including credential redaction, prompt injection, rug pull detection, SSRF blocking. mcp-context-protector requires PyTorch/ LlamaFirewall (~3GB), better suited for centralized deployment. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/mcp_protector_plan.md | 206 ++++++++++++++-------------- 1 file changed, 105 insertions(+), 101 deletions(-) diff --git a/.devcontainer/mcp_protector_plan.md b/.devcontainer/mcp_protector_plan.md index 5475ccdd51..08cd5279b0 100644 --- a/.devcontainer/mcp_protector_plan.md +++ b/.devcontainer/mcp_protector_plan.md @@ -1,122 +1,126 @@ -# MCP Context Protector Integration Plan +# MCP Security Proxy Plan -Trail of Bits [MCP Context Protector](https://github.com/trailofbits/mcp-context-protector) -as a security proxy between Claude Code and the GitHub MCP server. +Security proxy options for protecting Claude Code ↔ GitHub MCP communication. -## Architecture +## Option 1: mcp-watchdog (Recommended) -``` -Claude Code → MCP Context Protector (stdio) → GitHub MCP (HTTP) - │ - ├── TOFU pinning: detect tool description changes - ├── ANSI sanitization: strip escape sequences from responses - ├── Guardrails (optional): filter prompt injection in responses - └── Quarantine: flag suspicious content for review -``` +[bountyyfi/mcp-watchdog](https://github.com/bountyyfi/mcp-watchdog) — lightweight, +pattern-based security proxy. No ML models required. 273+ tests. -## Value for Our Setup +### What it does -1. **CI log injection defense** — failed CI job logs could contain crafted text - that manipulates the agent (e.g., fake error messages suggesting `git push --force`). - The guardrail would detect and filter these. +- **Credential redaction** — catches 30+ secret patterns (GitHub PATs, AWS keys, JWTs, + etc.) in MCP responses. Prevents token leakage via PR bodies, commit messages, CI logs. +- **Prompt injection detection** — `` tag injection, role injection markers, + SANDWORM-style instructions, homoglyph evasion, HTML-encoded variants. +- **Tool integrity (rug pull detection)** — hashes tool definitions, alerts on schema changes. +- **ANSI/Unicode sanitization** — strips zero-width chars, bidirectional overrides, + escape sequences that hide instructions from users. +- **Command/SQL/SSRF injection** — shell metacharacters, reverse shell patterns, + cloud metadata access (AWS IMDS, GCP, Azure). +- **Filesystem scope enforcement** — blocks writes to `.git/config`, `.ssh/`, `.aws/`. +- **Rate limiting** — consent fatigue protection, notification injection blocking. +- **Cross-server flow tracking** — detects token propagation between MCP servers. -2. **TOFU pinning** — alerts if the GitHub MCP server changes its tool definitions - between sessions. Detects supply chain changes to the MCP server. +### Dependencies -3. **ANSI sanitization** — strips hidden terminal escape sequences from GitHub API - responses that could hide instructions from the user but be visible to the model. +- Python 3.10+ (already in our container) +- `pip install mcp-watchdog` — core is pattern matching + entropy, no ML +- Optional `[semantic]` extra — adds Claude Haiku classifier (opt-in, not needed) +- Optional `[filesystem]` extra — adds inotify/FSEvents monitoring -4. **Audit trail** — quarantined responses are stored and reviewable via - `--review-quarantine`. +### Integration -## Integration Approach - -### Current (direct HTTP) ```bash +# In entrypoint.sh, wrap the GitHub MCP server: claude mcp add-json github \ - '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{...}}' + '{"command":"mcp-watchdog","args":["--verbose","--url","https://api.githubcopilot.com/mcp","--headers","Authorization: Bearer TOKEN","--headers","X-MCP-Toolsets: repos,pull_requests,actions"]}' ``` -### With Protector (stdio wrapping HTTP) -```bash -claude mcp add-json github \ - '{"command":"mcp-context-protector","args":["--url","https://api.githubcopilot.com/mcp","--headers","Authorization: Bearer TOKEN","--headers","X-MCP-Toolsets: repos,pull_requests,actions"]}' +### Open questions + +- [ ] Does `--url` mode support custom headers for HTTP upstream? +- [ ] Does `X-MCP-Toolsets` pass through the proxy correctly? +- [ ] Where does watchdog store tool integrity hashes? (needs persistence in volume) +- [ ] What's the per-request latency overhead for large CI log responses? +- [ ] How does `--verbose` output surface in headless/stream-json mode? + +### Install in Dockerfile + +```dockerfile +RUN pip3 install mcp-watchdog ``` -## Open Questions - -### 1. Header forwarding -Does `--url` mode support custom headers (`Authorization`, `X-MCP-Toolsets`)? -The docs show `--url` but no header examples. Need to check source code or test. - -### 2. LlamaFirewall dependency -Full guardrail protection requires LlamaFirewall (`--guardrail-provider LlamaFirewall`). -This is a separate ML model — likely large, adds significant image size and startup time. -Without it, only TOFU pinning and ANSI sanitization are available. -- Is TOFU + ANSI enough for our threat model? -- Can we run LlamaFirewall as a sidecar instead of in the same container? - -### 3. Python / uv dependency -The protector uses Python + uv. We have Python 3 in the builder image but not uv. -Options: -- `pip install mcp-context-protector` (if published to PyPI) -- `uv sync` from git clone in Dockerfile -- Vendor the protector as a script in .devcontainer/ - -### 4. State persistence -TOFU pinned configs must persist across container restarts. -Options: -- Store in the `collector-dev-claude` volume (`/home/dev/.claude/`) -- Separate volume for protector state -- Environment variable for state directory - -### 5. Startup latency -Adds another process to the MCP chain. Need to measure: -- Protector initialization time -- Per-request overhead (especially for large CI log responses) -- Impact on `/watch-ci` polling loop - -### 6. Toolset filtering compatibility -We use `X-MCP-Toolsets: repos,pull_requests,actions` to limit which tools -the GitHub MCP server exposes. Need to verify the protector passes this -header through and doesn't interfere with tool discovery. - -### 7. Quarantine review in headless mode -`--review-quarantine` is interactive. In autonomous mode (stream-json), -how would quarantined responses be surfaced? Options: -- Log to stderr (visible in stream output) -- Write to a file in /workspace for post-session review -- Fail the MCP call so the agent reports it as a blocker - -## Threat Model Comparison - -| Threat | Without Protector | With Protector | -|--------|------------------|----------------| -| CI log prompt injection | Unmitigated | Guardrail filters (with LlamaFirewall) | -| GitHub MCP tool definition change | Undetected | TOFU alerts on change | -| ANSI hidden instructions in responses | Unmitigated | Stripped and replaced | -| Token exfiltration via MCP responses | Unmitigated | Guardrail may detect | -| Credential in PR body/comments | Unmitigated | Guardrail may detect | -| MCP server impersonation | Undetected | TOFU pinning prevents | +Estimated size: ~10-20MB (pattern matching only, no ML). -## Recommendation +--- + +## Option 2: mcp-context-protector (Heavy) + +[trailofbits/mcp-context-protector](https://github.com/trailofbits/mcp-context-protector) — +Trail of Bits security wrapper with ML-based guardrails. + +### What it does + +- **TOFU pinning** — records tool definitions on first use, alerts on changes. +- **ANSI sanitization** — strips escape sequences. +- **LlamaFirewall guardrails** — ML-based prompt injection detection in responses. +- **Quarantine** — flags suspicious responses for manual review. -**Phase 1 (low effort):** Add TOFU pinning + ANSI sanitization only. -No LlamaFirewall. Validates tool definitions haven't changed, strips -escape sequences. Minimal dependencies, minimal latency. +### Why not (for now) -**Phase 2 (medium effort):** Add LlamaFirewall as a sidecar container. -Full prompt injection defense for CI logs and GitHub API responses. -Requires docker-compose or multi-container setup. +- **LlamaFirewall is a hard dependency** — `llamafirewall>=1.0.3` in pyproject.toml, + not optional. Pulls in PyTorch, transformers, huggingface_hub, semgrep. +- **Adds ~3GB** to the container image. +- **Heavy per-request cost** — ML inference on every MCP response. +- **Better suited for centralized deployment** (shared proxy for team) rather + than per-devcontainer. + +### When to reconsider + +- If Trail of Bits makes LlamaFirewall optional (`pip install mcp-context-protector[guardrails]`) +- If deploying a shared MCP proxy for the whole team (not per-container) +- If prompt injection via CI logs becomes a demonstrated threat (not just theoretical) + +--- + +## Option 3: open-mcp-guardrails + +[interactive-inc/open-mcp-guardrails](https://github.com/interactive-inc/open-mcp-guardrails) — +policy-based guardrails proxy. Early stage (0 stars). + +- PII leak detection +- Secret exposure prevention +- Prompt injection blocking +- Policy-based access control + +Too early to evaluate. Worth watching. + +--- + +## Threat Model + +| Threat | No proxy | mcp-watchdog | mcp-context-protector | +|--------|----------|-------------|----------------------| +| Credential leak in MCP response | Unmitigated | 30+ patterns redacted | Guardrail may detect | +| Prompt injection in CI logs | Unmitigated | 70+ patterns blocked | ML-based detection | +| Tool definition change (rug pull) | Undetected | Hash-based detection | TOFU pinning | +| ANSI/Unicode hidden instructions | Unmitigated | Stripped | Stripped | +| SSRF to cloud metadata | Unmitigated | Blocked | Not covered | +| Command injection via MCP | Unmitigated | Shell/SQL patterns blocked | Not covered | +| Cross-server token propagation | Unmitigated | Flow tracking | Not covered | + +## Recommendation -**Phase 3 (high effort):** Custom guardrail rules specific to collector -development — detect patterns like "ignore previous instructions", -credential patterns in PR bodies, suspicious git commands in CI logs. +**Start with mcp-watchdog.** It covers more attack vectors than mcp-context-protector, +has no ML dependencies, and is designed for exactly our use case (wrapping MCP servers +for AI coding assistants). The credential redaction alone justifies the ~10MB install. -## Prerequisites Before Starting +### Implementation steps -- [ ] Verify `--url` supports custom headers (check source) -- [ ] Test basic TOFU mode without LlamaFirewall -- [ ] Measure startup latency overhead -- [ ] Confirm TOFU state directory is configurable -- [ ] Test with `X-MCP-Toolsets` header passthrough +1. Test `mcp-watchdog --url` with custom headers locally +2. Add `pip3 install mcp-watchdog` to Dockerfile +3. Update entrypoint.sh to wrap GitHub MCP with watchdog +4. Verify `X-MCP-Toolsets` header passes through +5. Configure watchdog state persistence in `collector-dev-claude` volume +6. Test with `/watch-ci` to ensure CI log scanning works From d7d42a033b3ce20662beb13f9cae76a422dd6d2e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:18:15 -0700 Subject: [PATCH 45/51] fix: push branch from host before launching autonomous agent The agent can't git push (no SSH keys) and GitHub MCP push_files requires an existing remote branch. Push from the host in run.sh so /watch-ci and /dev-loop can push via MCP immediately. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 9c878964ac..59696d7d05 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -231,6 +231,10 @@ USAGE BRANCH=$(git -C "$WORKTREE" branch --show-current) TASK="$*" + # Push branch so the agent can use GitHub MCP push_files + echo "Pushing branch $BRANCH..." >&2 + git -C "$WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 + echo "Working in isolated worktree: $WORKTREE" >&2 echo "Branch: $BRANCH" >&2 echo "Task: $TASK" >&2 From bc0825ac784b7c286a15e864226e9849924bae0c Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:18:42 -0700 Subject: [PATCH 46/51] fix: only push branch in autonomous /dev-loop mode, not for explicit skills Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 59696d7d05..9cefe186a3 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -231,9 +231,11 @@ USAGE BRANCH=$(git -C "$WORKTREE" branch --show-current) TASK="$*" - # Push branch so the agent can use GitHub MCP push_files - echo "Pushing branch $BRANCH..." >&2 - git -C "$WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 + # Push branch for /dev-loop so the agent can use GitHub MCP push_files + if [[ "$TASK" != /* ]]; then + echo "Pushing branch $BRANCH..." >&2 + git -C "$WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 + fi echo "Working in isolated worktree: $WORKTREE" >&2 echo "Branch: $BRANCH" >&2 From df9991a6a25d60d2fc8b1af22fbe89ff41f6a91f Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:27:13 -0700 Subject: [PATCH 47/51] feat: TUI is now default for all modes, --no-tui for stream-json All modes now use interactive TUI by default, including autonomous task mode. This means you can watch, interrupt, and interact with the agent even in /dev-loop mode. Use --no-tui to get the old stream-json output for piping or logging. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index 9cefe186a3..e99e149521 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -9,6 +9,7 @@ # # Options: # --branch Branch name (default: claude/agent-) +# --no-tui Stream JSON instead of TUI # --debug Verbose MCP/auth logging # # Prerequisites: @@ -24,6 +25,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" IMAGE="${COLLECTOR_DEV_IMAGE:-collector-dev:test}" WORKTREE_BASE="/tmp/collector-worktrees" DEBUG=false +NO_TUI=false BRANCH_NAME="" # Parse global flags @@ -31,6 +33,7 @@ ARGS=() for arg in "$@"; do case "$arg" in --debug) DEBUG=true ;; + --no-tui) NO_TUI=true ;; --branch=*) BRANCH_NAME="${arg#--branch=}" ;; --branch) BRANCH_NAME="__NEXT__" ;; *) @@ -44,12 +47,14 @@ for arg in "$@"; do done set -- "${ARGS[@]+"${ARGS[@]}"}" -CLAUDE_INTERACTIVE=(claude --dangerously-skip-permissions) -CLAUDE_AUTONOMOUS=(claude --dangerously-skip-permissions --output-format stream-json --verbose) +CLAUDE_CMD=(claude --dangerously-skip-permissions) if [[ "$DEBUG" == "true" ]]; then - CLAUDE_INTERACTIVE+=(--debug) - CLAUDE_AUTONOMOUS+=(--debug) + CLAUDE_CMD+=(--debug) +fi + +if [[ "$NO_TUI" == "true" ]]; then + CLAUDE_CMD+=(--output-format stream-json --verbose) fi # --- Preflight checks --- @@ -176,7 +181,7 @@ case "${1:-}" in echo "Working in isolated worktree: $WORKTREE" echo "Branch: $BRANCH" build_docker_args "$WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" ;; --local|-l) @@ -188,9 +193,9 @@ case "${1:-}" in preflight build_docker_args "$REPO_ROOT" if [[ -z "${1:-}" ]]; then - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_INTERACTIVE[@]}" -p "$*" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$*" fi ;; @@ -206,13 +211,15 @@ case "${1:-}" in ""|--help|-h) cat < Branch name (default: claude/agent-) + --no-tui Stream JSON output instead of TUI --debug Verbose MCP/auth logging Environment: @@ -237,20 +244,23 @@ USAGE git -C "$WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 fi - echo "Working in isolated worktree: $WORKTREE" >&2 - echo "Branch: $BRANCH" >&2 - echo "Task: $TASK" >&2 - echo "---" >&2 + echo "Working in isolated worktree: $WORKTREE" + echo "Branch: $BRANCH" + echo "Task: $TASK" + echo "---" trap "cleanup_worktree '$WORKTREE'" EXIT build_docker_args "$WORKTREE" - # If task starts with /, treat as a skill invocation; otherwise default to /dev-loop PROMPT="/dev-loop $TASK" if [[ "$TASK" == /* ]]; then PROMPT="$TASK" fi - docker run "${DOCKER_ARGS[@]}" "$IMAGE" \ - "${CLAUDE_AUTONOMOUS[@]}" -p "$PROMPT" + + if [[ "$NO_TUI" == "true" ]]; then + docker run "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + else + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + fi ;; esac From cb9610cd717fe1f0e876b68820ff0ef5a6aba035 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:32:32 -0700 Subject: [PATCH 48/51] docs: clarify push_files usage in dev-loop and watch-ci skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explain that push_files sends file contents via GitHub API — it does not sync local git history. Provide explicit steps: get branch name, get changed files list, read contents, call push_files. Remove the remote branch prerequisite from watch-ci since run.sh handles it. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/dev-loop/SKILL.md | 20 +++++++---- .claude/skills/watch-ci/SKILL.md | 62 +++++++++++++++----------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/.claude/skills/dev-loop/SKILL.md b/.claude/skills/dev-loop/SKILL.md index ad48a386a2..6b56237e3f 100644 --- a/.claude/skills/dev-loop/SKILL.md +++ b/.claude/skills/dev-loop/SKILL.md @@ -24,9 +24,18 @@ Do NOT stop until CI is green or you are blocked. ## Phase 2: Push and create PR -1. Check if remote branch exists: `git rev-parse --abbrev-ref --symbolic-full-name @{u}` - - If no remote branch, stop and report: "No remote branch. Push from host first." -2. Push via the GitHub MCP server push_files tool (do NOT use `git push`) +Use the GitHub MCP server to push files and create a PR. +Do NOT use `git push` — it will fail (no SSH keys in this container). + +1. Get the current branch name and the list of changed files: + - `git branch --show-current` for the branch + - `git diff --name-only origin/HEAD..HEAD` for changed files +2. Use the GitHub MCP `push_files` tool to push the changed files directly to + the remote branch. This creates a commit via the GitHub API using the file + contents from your local workspace — it does not sync git history. + - owner: stackrox, repo: collector, branch: + - Read each changed file and include its content + - Provide a commit message 3. Search for an open PR for this branch via GitHub MCP 4. If no PR exists, create a draft PR via GitHub MCP @@ -40,7 +49,6 @@ Loop until all checks pass or blocked (max 6 cycles, ~3 hours): ``` ## Agent Status **Last updated:** <`date -u +"%Y-%m-%d %H:%M UTC"`> - **Last commit:** <`git rev-parse --short HEAD`> **CI cycle:** N of 6 **Status:** PENDING | PASSED | FIXED | FLAKE | BLOCKED **Details:** @@ -51,7 +59,7 @@ Loop until all checks pass or blocked (max 6 cycles, ~3 hours): - **Failed** → - Get job logs via GitHub MCP - Diagnose: build error, test assertion, lint, infra flake - - If fixable: fix → build → test → commit → push via MCP → continue + - If fixable: fix → build → test → push changed files via MCP → continue - If infra flake: note as FLAKE, continue - If not fixable: update PR body, report BLOCKED, stop @@ -61,7 +69,7 @@ Before each CI cycle, check if there are new PR review comments via GitHub MCP. If a reviewer left feedback: - Address the feedback (edit code, fix issues) - Build and test -- Commit and push via MCP +- Push changed files via MCP - Note in the Agent Status section what feedback was addressed ## Completion diff --git a/.claude/skills/watch-ci/SKILL.md b/.claude/skills/watch-ci/SKILL.md index a1fb775eef..b9e6cc8772 100644 --- a/.claude/skills/watch-ci/SKILL.md +++ b/.claude/skills/watch-ci/SKILL.md @@ -1,41 +1,50 @@ --- name: watch-ci -description: Push to existing remote branch via GitHub MCP, create PR if needed, monitor CI, fix failures until green +description: Push files to existing remote branch via GitHub MCP, create PR if needed, monitor CI, fix failures until green disable-model-invocation: true allowed-tools: Bash(cmake *), Bash(ctest *), Bash(nproc), Bash(git add *), Bash(git commit *), Bash(git diff *), Bash(git describe *), Bash(git branch *), Bash(git status), Bash(git log *), Bash(git rev-parse *), Bash(clang-format *), Bash(sleep *), Bash(date *), Read, Write, Edit, Glob, Grep --- # Watch CI -Push commits via the GitHub MCP server, create PR if needed, and monitor CI until green. -Do NOT use `git push` — it is blocked. Use the GitHub MCP push_files tool instead. +Push changed files via the GitHub MCP server, create PR if needed, and monitor CI until green. +Do NOT use `git push` — it will fail (no SSH keys in this container). -## Prerequisites +## How pushing works -The current branch must already have a remote tracking branch. Check with: -`git rev-parse --abbrev-ref --symbolic-full-name @{u}` -If this fails, stop and report: "No remote branch. Push from host first." +Use the GitHub MCP `push_files` tool to send file contents directly to the remote +branch via the GitHub API. This does NOT sync local git history — it creates a new +commit on the remote with the file contents you provide. -## Steps +1. Get the branch name: `git branch --show-current` +2. Get changed files: `git diff --name-only origin/HEAD..HEAD` +3. Read each changed file's content +4. Call `push_files` with owner: stackrox, repo: collector, branch, files, and commit message -1. **Check remote branch exists**: - - `git rev-parse --abbrev-ref --symbolic-full-name @{u}` - - If this fails, stop +## Steps -2. **Push** current commits: - - Use the GitHub MCP server push_files tool to push committed changes - - Do NOT use `git push` +1. **Push** changed files: + - Use the GitHub MCP `push_files` tool as described above + - If no files have changed since last push, skip -3. **Find or create PR**: +2. **Find or create PR**: - Use the GitHub MCP server to search for an open PR for this branch - If no PR exists, create a draft PR via the GitHub MCP server -4. **Monitor CI loop** (repeat until all checks pass or blocked): +3. **Monitor CI loop** (repeat until all checks pass or blocked): - Wait 10 minutes: `sleep 600` - Use the GitHub MCP server to get PR check status and workflow runs + - Update PR body with an `## Agent Status` section: + ``` + ## Agent Status + **Last updated:** <`date -u +"%Y-%m-%d %H:%M UTC"`> + **CI cycle:** N of 6 + **Status:** PENDING | PASSED | FIXED | FLAKE | BLOCKED + **Details:** + ``` - Evaluate: - **All checks passed** → update PR body, report success and stop - - **Checks still running** → update PR body, report progress, continue loop + - **Checks still running** → report progress, continue loop - **Checks failed** → - Get job logs via the GitHub MCP server - Diagnose: @@ -44,27 +53,14 @@ If this fails, stop and report: "No remote branch. Push from host first." - Lint failure: run `clang-format --style=file -i` - Integration test infra flake (VM timeout, network): report as flake, continue - Integration test real failure: analyze and fix code - - If fixable: fix → build → test → commit → push via MCP → update PR body → continue loop + - If fixable: fix → build → test → push changed files via MCP → continue loop - If not fixable: update PR body, report diagnosis and stop -5. **Update PR body** after each CI check with a status section at the bottom: - - Use the GitHub MCP server to update the PR description - - Append or replace a `## Agent Status` section with: - ``` - ## Agent Status - **Last updated:** - **Last commit:** - **CI cycle:** N of 6 - **Status:** PENDING | PASSED | FIXED | FLAKE | BLOCKED - **Details:** - ``` - - Keep the original PR description above the Agent Status section - -6. **Safety limits**: +4. **Safety limits**: - Maximum 6 CI cycles (about 3 hours of monitoring) - If exceeded, update PR body and stop -7. **Summary**: end with a status line: +5. **Summary**: end with a status line: - `PASSED` — all checks green - `PENDING` — checks still running - `FIXED` — failure diagnosed and fix pushed From 8feb2b2a35006a4dc7ca41952b2558bb6b9d322b Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:42:42 -0700 Subject: [PATCH 49/51] docs: add devcontainer README with GitHub PAT permissions and setup guide Covers: quick start, modes, skills, token permissions (required vs optional vs not needed), security model, env vars, worktree management. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/README.md | 140 ++++++++++++++++++++++++++++++++++++ .devcontainer/entrypoint.sh | 2 +- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/README.md diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000000..45c525f594 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,140 @@ +# Collector Devcontainer + +Sandboxed Claude Code environment for developing collector. The agent works +in an isolated git worktree inside a container with no SSH keys — code is +pushed via GitHub MCP only. + +## Quick Start + +```bash +# 1. Build the image (one time) +docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + -t collector-dev:test -f .devcontainer/Dockerfile .devcontainer/ + +# 2. Set environment (add to shell profile) +export CLAUDE_CODE_USE_VERTEX=1 +export GOOGLE_CLOUD_PROJECT= +export GOOGLE_CLOUD_LOCATION=us-east5 +export ANTHROPIC_VERTEX_PROJECT_ID= +export GITHUB_TOKEN=github_pat_... + +# 3. Authenticate GCP +gcloud auth login +gcloud auth application-default login + +# 4. Run +.devcontainer/run.sh --interactive # manual control +.devcontainer/run.sh "add unit tests for ExternalIPsConfig" # autonomous +``` + +## Modes + +| Command | Description | +|---------|-------------| +| `run.sh "task"` | Autonomous `/dev-loop`: implement → push → PR → CI loop | +| `run.sh "/skill args"` | Run a specific skill | +| `run.sh --interactive` | Worktree + TUI, you drive | +| `run.sh --local` | Edit working tree directly, no worktree | +| `run.sh --shell` | Shell into the container | + +### Options + +| Flag | Description | +|------|-------------| +| `--branch ` | Custom branch name (default: `claude/agent-`) | +| `--no-tui` | Stream JSON output instead of TUI | +| `--debug` | Verbose MCP/auth logging | + +## Skills + +| Skill | Purpose | +|-------|---------| +| `/task` | Implement, build, test, format, commit. Stops after commit. | +| `/watch-ci` | Push via MCP, create PR, monitor CI, fix failures until green. | +| `/dev-loop` | Full cycle: `/task` then `/watch-ci` in one run. | + +## GitHub Token Setup + +Create a **fine-grained Personal Access Token** at: +https://github.com/settings/tokens?type=beta + +### Repository access + +Select the repositories the agent should work on (e.g., `stackrox/collector`). + +### Required permissions + +| Permission | Access | Why | +|-----------|--------|-----| +| **Contents** | Read and write | Push files to branches via MCP | +| **Pull requests** | Read and write | Create/update PRs, read PR status | +| **Actions** | Read and write | List workflow runs, get job logs | +| **Commit statuses** | Read-only | Check CI check status | +| **Metadata** | Read-only | Required by GitHub for all PATs | + +### Optional permissions + +| Permission | Access | Why | +|-----------|--------|-----| +| Issues | Read-only | Read issue context if task references one | +| Discussions | Read-only | Read discussion context | + +### Permissions NOT needed + +Do not grant these — they are denied in `.claude/settings.json`: + +- ~~Administration~~ — agent should not manage repo settings +- ~~Merge queues~~ — agent cannot merge PRs +- ~~Pages~~ — not relevant +- ~~Environments~~ — not relevant +- ~~Secrets~~ — agent should not access repo secrets + +## Security Model + +| Layer | Protection | +|-------|-----------| +| Container isolation | Agent can't access host filesystem | +| No SSH keys | `git push` fails — only MCP `push_files` works | +| Read-only mounts | gcloud credentials, gitconfig can't be modified | +| MCP deny rules | merge, delete, fork, create-repo, trigger-actions blocked | +| Worktree isolation | Agent works on a separate branch, can't touch your checkout | +| .git mount scoping | Worktree git dir writable, shared objects read-only* | + +*Note: `.git` is currently mounted read-write due to submodule init requirements. +The agent can't push (no SSH keys) so risk is limited to local git state. + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `GITHUB_TOKEN` | Yes (for MCP) | Fine-grained PAT (see above) | +| `CLAUDE_CODE_USE_VERTEX` | Yes | Set to `1` | +| `GOOGLE_CLOUD_PROJECT` | Yes | GCP project ID | +| `GOOGLE_CLOUD_LOCATION` | Yes | Vertex AI region (e.g., `us-east5`) | +| `ANTHROPIC_VERTEX_PROJECT_ID` | Yes | Usually same as `GOOGLE_CLOUD_PROJECT` | +| `COLLECTOR_DEV_IMAGE` | No | Docker image name (default: `collector-dev:test`) | + +## Worktree Management + +Worktrees are created in `/tmp/collector-worktrees/` and cleaned up on exit. + +```bash +# List active worktrees +git worktree list + +# Clean up stale worktrees +git worktree prune + +# Remove a specific worktree +git worktree remove /tmp/collector-worktrees/ +``` + +## Rebuilding the Image + +```bash +docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + -t collector-dev:test -f .devcontainer/Dockerfile .devcontainer/ + +# Clear cached Claude state (MCP registrations, theme, etc.) +docker volume rm collector-dev-claude +``` diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index a9da08a618..8e0ae9f0c5 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -9,7 +9,7 @@ claude config set --global verbose false 2>/dev/null || true # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then if ! claude mcp add-json github \ - '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'","X-MCP-Toolsets":"repos,pull_requests,actions"}}' \ + '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'","X-MCP-Toolsets":"context,repos,pull_requests,issues,actions,git"}}' \ --scope user 2>/dev/null; then echo "WARNING: Failed to register GitHub MCP server" >&2 fi From c938ea8877fe117742a634c90b31936dc5a277f5 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 14:56:29 -0700 Subject: [PATCH 50/51] fix: shfmt formatting and shellcheck SC2064 warning in run.sh - Use tabs for indentation (shfmt default) - Fix SC2064: replace trap with quoted variable expansion with an on_exit function that reads ACTIVE_WORKTREE global - Consistent formatting throughout Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/run.sh | 354 ++++++++++++++++++++++--------------------- 1 file changed, 181 insertions(+), 173 deletions(-) diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index e99e149521..fec6384005 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -27,189 +27,197 @@ WORKTREE_BASE="/tmp/collector-worktrees" DEBUG=false NO_TUI=false BRANCH_NAME="" +ACTIVE_WORKTREE="" # Parse global flags ARGS=() for arg in "$@"; do - case "$arg" in - --debug) DEBUG=true ;; - --no-tui) NO_TUI=true ;; - --branch=*) BRANCH_NAME="${arg#--branch=}" ;; - --branch) BRANCH_NAME="__NEXT__" ;; - *) - if [[ "$BRANCH_NAME" == "__NEXT__" ]]; then - BRANCH_NAME="$arg" - else - ARGS+=("$arg") - fi - ;; - esac + case "$arg" in + --debug) DEBUG=true ;; + --no-tui) NO_TUI=true ;; + --branch=*) BRANCH_NAME="${arg#--branch=}" ;; + --branch) BRANCH_NAME="__NEXT__" ;; + *) + if [[ "$BRANCH_NAME" == "__NEXT__" ]]; then + BRANCH_NAME="$arg" + else + ARGS+=("$arg") + fi + ;; + esac done set -- "${ARGS[@]+"${ARGS[@]}"}" CLAUDE_CMD=(claude --dangerously-skip-permissions) if [[ "$DEBUG" == "true" ]]; then - CLAUDE_CMD+=(--debug) + CLAUDE_CMD+=(--debug) fi if [[ "$NO_TUI" == "true" ]]; then - CLAUDE_CMD+=(--output-format stream-json --verbose) + CLAUDE_CMD+=(--output-format stream-json --verbose) fi # --- Preflight checks --- check_docker() { - if ! command -v docker &>/dev/null; then - echo "ERROR: docker not found." >&2 - exit 1 - fi - if ! docker info &>/dev/null 2>&1; then - echo "ERROR: Docker daemon not running." >&2 - exit 1 - fi + if ! command -v docker &>/dev/null; then + echo "ERROR: docker not found." >&2 + exit 1 + fi + if ! docker info &>/dev/null 2>&1; then + echo "ERROR: Docker daemon not running." >&2 + exit 1 + fi } check_image() { - if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then - echo "ERROR: Image '$IMAGE' not found. Build with:" >&2 - echo " docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 - exit 1 - fi + if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then + echo "ERROR: Image '$IMAGE' not found. Build with:" >&2 + echo " docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 + exit 1 + fi } check_gcloud() { - if [[ ! -f "$HOME/.config/gcloud/application_default_credentials.json" ]]; then - echo "ERROR: Run: gcloud auth application-default login" >&2 - exit 1 - fi + if [[ ! -f "$HOME/.config/gcloud/application_default_credentials.json" ]]; then + echo "ERROR: Run: gcloud auth application-default login" >&2 + exit 1 + fi } check_vertex_env() { - local missing=() - [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) - [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) - [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) - if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: Missing env vars: ${missing[*]} (see CLAUDE.md)" >&2 - exit 1 - fi + local missing=() + [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) + [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) + [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) + if [[ ${#missing[@]} -gt 0 ]]; then + echo "ERROR: Missing env vars: ${missing[*]} (see CLAUDE.md)" >&2 + exit 1 + fi } preflight() { - check_docker - check_image - check_gcloud - check_vertex_env - if [[ -z "${GITHUB_TOKEN:-}" ]]; then - echo "WARNING: GITHUB_TOKEN not set. GitHub MCP (PR creation, CI) will not work." >&2 - fi + check_docker + check_image + check_gcloud + check_vertex_env + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "WARNING: GITHUB_TOKEN not set. GitHub MCP (PR creation, CI) will not work." >&2 + fi } # --- Worktree --- setup_worktree() { - local branch - if [[ -n "$BRANCH_NAME" ]]; then - branch="$BRANCH_NAME" - else - branch="claude/agent-$(date +%s)-$$" - fi - local safe_name="${branch//\//-}" - local worktree_dir="${WORKTREE_BASE}/${safe_name}" - - mkdir -p "$WORKTREE_BASE" - git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - - # Make worktree git dir writable by container user (different uid) - local worktree_git_name - worktree_git_name=$(basename "$worktree_dir") - chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" - - echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init --depth 1 \ - falcosecurity-libs \ - collector/proto/third_party/stackrox \ - 2>&1 | sed 's/^/ /' >&2 - - echo "$worktree_dir" + local branch + if [[ -n "$BRANCH_NAME" ]]; then + branch="$BRANCH_NAME" + else + branch="claude/agent-$(date +%s)-$$" + fi + local safe_name="${branch//\//-}" + local worktree_dir="${WORKTREE_BASE}/${safe_name}" + + mkdir -p "$WORKTREE_BASE" + git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 + + # Make worktree git dir writable by container user (different uid) + local worktree_git_name + worktree_git_name=$(basename "$worktree_dir") + chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" + + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init --depth 1 \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + 2>&1 | sed 's/^/ /' >&2 + + echo "$worktree_dir" } cleanup_worktree() { - local worktree_dir="$1" - if [[ -d "$worktree_dir" ]]; then - local branch - branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) - git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true - if [[ -n "$branch" ]]; then - if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then - git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true - fi - fi - fi + local worktree_dir="$1" + if [[ -d "$worktree_dir" ]]; then + local branch + branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) + git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true + if [[ -n "$branch" ]]; then + if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then + git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true + fi + fi + fi +} + +on_exit() { + if [[ -n "$ACTIVE_WORKTREE" ]]; then + cleanup_worktree "$ACTIVE_WORKTREE" + fi } # --- Docker --- build_docker_args() { - local workspace="$1" - DOCKER_ARGS=( - --rm - -v "$workspace:/workspace" - -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" - -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" - -v "collector-dev-claude:/home/dev/.claude" - -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud - -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json - -w /workspace - ) - - # Mount .git so worktree resolves (agent can't push — no SSH keys) - DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") - - for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do - if [[ -n "${!var:-}" ]]; then - DOCKER_ARGS+=(-e "$var=${!var}") - fi - done + local workspace="$1" + DOCKER_ARGS=( + --rm + -v "$workspace:/workspace" + -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" + -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" + -v "collector-dev-claude:/home/dev/.claude" + -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud + -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json + -w /workspace + ) + + # Mount .git so worktree resolves (agent can't push — no SSH keys) + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") + + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do + if [[ -n "${!var:-}" ]]; then + DOCKER_ARGS+=(-e "$var=${!var}") + fi + done } # --- Main --- case "${1:-}" in - --interactive|-i) - preflight - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - BRANCH=$(git -C "$WORKTREE" branch --show-current) - echo "Working in isolated worktree: $WORKTREE" - echo "Branch: $BRANCH" - build_docker_args "$WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" - ;; - - --local|-l) - shift - if [[ -n "$BRANCH_NAME" ]]; then - echo "ERROR: --branch cannot be used with --local" >&2 - exit 1 - fi - preflight - build_docker_args "$REPO_ROOT" - if [[ -z "${1:-}" ]]; then - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" - else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$*" - fi - ;; - - --shell|-s) - check_docker; check_image - WORKTREE=$(setup_worktree) - trap "cleanup_worktree '$WORKTREE'" EXIT - echo "Working in isolated worktree: $WORKTREE" - build_docker_args "$WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh - ;; - - ""|--help|-h) - cat <&2 + exit 1 + fi + preflight + build_docker_args "$REPO_ROOT" + if [[ -z "${1:-}" ]]; then + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" + else + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$*" + fi + ;; + +--shell | -s) + check_docker + check_image + ACTIVE_WORKTREE=$(setup_worktree) + trap on_exit EXIT + echo "Working in isolated worktree: $ACTIVE_WORKTREE" + build_docker_args "$ACTIVE_WORKTREE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh + ;; + +"" | --help | -h) + cat <&2 - git -C "$WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 - fi - - echo "Working in isolated worktree: $WORKTREE" - echo "Branch: $BRANCH" - echo "Task: $TASK" - echo "---" - - trap "cleanup_worktree '$WORKTREE'" EXIT - - build_docker_args "$WORKTREE" - PROMPT="/dev-loop $TASK" - if [[ "$TASK" == /* ]]; then - PROMPT="$TASK" - fi - - if [[ "$NO_TUI" == "true" ]]; then - docker run "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" - else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" - fi - ;; + exit 0 + ;; + +*) + preflight + ACTIVE_WORKTREE=$(setup_worktree) + BRANCH=$(git -C "$ACTIVE_WORKTREE" branch --show-current) + TASK="$*" + + # Push branch for /dev-loop so the agent can use GitHub MCP push_files + if [[ "$TASK" != /* ]]; then + echo "Pushing branch $BRANCH..." >&2 + git -C "$ACTIVE_WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 + fi + + echo "Working in isolated worktree: $ACTIVE_WORKTREE" + echo "Branch: $BRANCH" + echo "Task: $TASK" + echo "---" + + trap on_exit EXIT + + build_docker_args "$ACTIVE_WORKTREE" + PROMPT="/dev-loop $TASK" + if [[ "$TASK" == /* ]]; then + PROMPT="$TASK" + fi + + if [[ "$NO_TUI" == "true" ]]; then + docker run "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + else + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + fi + ;; esac From 76f1e6ccfd177b72db2b56ba31057fb2eec4350e Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 19 Mar 2026 15:30:44 -0700 Subject: [PATCH 51/51] =?UTF-8?q?fix:=20shfmt=20formatting=20=E2=80=94=20u?= =?UTF-8?q?se=204-space=20indent=20per=20.editorconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/entrypoint.sh | 16 +- .devcontainer/init-firewall.sh | 4 +- .devcontainer/run.sh | 354 ++++++++++++++++----------------- 3 files changed, 187 insertions(+), 187 deletions(-) diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index 8e0ae9f0c5..2531b727ba 100755 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -3,18 +3,18 @@ mkdir -p /home/dev/.claude/debug /home/dev/.commandhistory # Set defaults so Claude Code doesn't prompt on startup -claude config set --global theme dark 2>/dev/null || true -claude config set --global verbose false 2>/dev/null || true +claude config set --global theme dark 2> /dev/null || true +claude config set --global verbose false 2> /dev/null || true # Register GitHub MCP server if token is available if [[ -n "${GITHUB_TOKEN:-}" ]]; then - if ! claude mcp add-json github \ - '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'","X-MCP-Toolsets":"context,repos,pull_requests,issues,actions,git"}}' \ - --scope user 2>/dev/null; then - echo "WARNING: Failed to register GitHub MCP server" >&2 - fi + if ! claude mcp add-json github \ + '{"type":"http","url":"https://api.githubcopilot.com/mcp","headers":{"Authorization":"Bearer '"$GITHUB_TOKEN"'","X-MCP-Toolsets":"context,repos,pull_requests,issues,actions,git"}}' \ + --scope user 2> /dev/null; then + echo "WARNING: Failed to register GitHub MCP server" >&2 + fi else - echo "NOTE: GITHUB_TOKEN not set — GitHub MCP tools unavailable" >&2 + echo "NOTE: GITHUB_TOKEN not set — GitHub MCP tools unavailable" >&2 fi exec "$@" diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh index 391088536b..b518fe41fd 100644 --- a/.devcontainer/init-firewall.sh +++ b/.devcontainer/init-firewall.sh @@ -6,12 +6,12 @@ set -euo pipefail -if ! command -v iptables &>/dev/null; then +if ! command -v iptables &> /dev/null; then echo "iptables not available, skipping firewall setup" exit 0 fi -if ! iptables -L &>/dev/null 2>&1; then +if ! iptables -L &> /dev/null 2>&1; then echo "No NET_ADMIN capability, skipping firewall setup" exit 0 fi diff --git a/.devcontainer/run.sh b/.devcontainer/run.sh index fec6384005..b4dabc79e6 100755 --- a/.devcontainer/run.sh +++ b/.devcontainer/run.sh @@ -32,192 +32,192 @@ ACTIVE_WORKTREE="" # Parse global flags ARGS=() for arg in "$@"; do - case "$arg" in - --debug) DEBUG=true ;; - --no-tui) NO_TUI=true ;; - --branch=*) BRANCH_NAME="${arg#--branch=}" ;; - --branch) BRANCH_NAME="__NEXT__" ;; - *) - if [[ "$BRANCH_NAME" == "__NEXT__" ]]; then - BRANCH_NAME="$arg" - else - ARGS+=("$arg") - fi - ;; - esac + case "$arg" in + --debug) DEBUG=true ;; + --no-tui) NO_TUI=true ;; + --branch=*) BRANCH_NAME="${arg#--branch=}" ;; + --branch) BRANCH_NAME="__NEXT__" ;; + *) + if [[ "$BRANCH_NAME" == "__NEXT__" ]]; then + BRANCH_NAME="$arg" + else + ARGS+=("$arg") + fi + ;; + esac done set -- "${ARGS[@]+"${ARGS[@]}"}" CLAUDE_CMD=(claude --dangerously-skip-permissions) if [[ "$DEBUG" == "true" ]]; then - CLAUDE_CMD+=(--debug) + CLAUDE_CMD+=(--debug) fi if [[ "$NO_TUI" == "true" ]]; then - CLAUDE_CMD+=(--output-format stream-json --verbose) + CLAUDE_CMD+=(--output-format stream-json --verbose) fi # --- Preflight checks --- check_docker() { - if ! command -v docker &>/dev/null; then - echo "ERROR: docker not found." >&2 - exit 1 - fi - if ! docker info &>/dev/null 2>&1; then - echo "ERROR: Docker daemon not running." >&2 - exit 1 - fi + if ! command -v docker &> /dev/null; then + echo "ERROR: docker not found." >&2 + exit 1 + fi + if ! docker info &> /dev/null 2>&1; then + echo "ERROR: Docker daemon not running." >&2 + exit 1 + fi } check_image() { - if ! docker image inspect "$IMAGE" &>/dev/null 2>&1; then - echo "ERROR: Image '$IMAGE' not found. Build with:" >&2 - echo " docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 - exit 1 - fi + if ! docker image inspect "$IMAGE" &> /dev/null 2>&1; then + echo "ERROR: Image '$IMAGE' not found. Build with:" >&2 + echo " docker build --platform linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -t $IMAGE -f .devcontainer/Dockerfile .devcontainer/" >&2 + exit 1 + fi } check_gcloud() { - if [[ ! -f "$HOME/.config/gcloud/application_default_credentials.json" ]]; then - echo "ERROR: Run: gcloud auth application-default login" >&2 - exit 1 - fi + if [[ ! -f "$HOME/.config/gcloud/application_default_credentials.json" ]]; then + echo "ERROR: Run: gcloud auth application-default login" >&2 + exit 1 + fi } check_vertex_env() { - local missing=() - [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) - [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) - [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) - if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: Missing env vars: ${missing[*]} (see CLAUDE.md)" >&2 - exit 1 - fi + local missing=() + [[ -z "${CLAUDE_CODE_USE_VERTEX:-}" ]] && missing+=(CLAUDE_CODE_USE_VERTEX) + [[ -z "${GOOGLE_CLOUD_PROJECT:-}" ]] && missing+=(GOOGLE_CLOUD_PROJECT) + [[ -z "${GOOGLE_CLOUD_LOCATION:-}" ]] && missing+=(GOOGLE_CLOUD_LOCATION) + if [[ ${#missing[@]} -gt 0 ]]; then + echo "ERROR: Missing env vars: ${missing[*]} (see CLAUDE.md)" >&2 + exit 1 + fi } preflight() { - check_docker - check_image - check_gcloud - check_vertex_env - if [[ -z "${GITHUB_TOKEN:-}" ]]; then - echo "WARNING: GITHUB_TOKEN not set. GitHub MCP (PR creation, CI) will not work." >&2 - fi + check_docker + check_image + check_gcloud + check_vertex_env + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "WARNING: GITHUB_TOKEN not set. GitHub MCP (PR creation, CI) will not work." >&2 + fi } # --- Worktree --- setup_worktree() { - local branch - if [[ -n "$BRANCH_NAME" ]]; then - branch="$BRANCH_NAME" - else - branch="claude/agent-$(date +%s)-$$" - fi - local safe_name="${branch//\//-}" - local worktree_dir="${WORKTREE_BASE}/${safe_name}" - - mkdir -p "$WORKTREE_BASE" - git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD >/dev/null 2>&1 - - # Make worktree git dir writable by container user (different uid) - local worktree_git_name - worktree_git_name=$(basename "$worktree_dir") - chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" - - echo "Initializing submodules..." >&2 - git -C "$worktree_dir" submodule update --init --depth 1 \ - falcosecurity-libs \ - collector/proto/third_party/stackrox \ - 2>&1 | sed 's/^/ /' >&2 - - echo "$worktree_dir" + local branch + if [[ -n "$BRANCH_NAME" ]]; then + branch="$BRANCH_NAME" + else + branch="claude/agent-$(date +%s)-$$" + fi + local safe_name="${branch//\//-}" + local worktree_dir="${WORKTREE_BASE}/${safe_name}" + + mkdir -p "$WORKTREE_BASE" + git -C "$REPO_ROOT" worktree add -b "$branch" "$worktree_dir" HEAD > /dev/null 2>&1 + + # Make worktree git dir writable by container user (different uid) + local worktree_git_name + worktree_git_name=$(basename "$worktree_dir") + chmod -R a+rwX "$REPO_ROOT/.git/worktrees/$worktree_git_name" + + echo "Initializing submodules..." >&2 + git -C "$worktree_dir" submodule update --init --depth 1 \ + falcosecurity-libs \ + collector/proto/third_party/stackrox \ + 2>&1 | sed 's/^/ /' >&2 + + echo "$worktree_dir" } cleanup_worktree() { - local worktree_dir="$1" - if [[ -d "$worktree_dir" ]]; then - local branch - branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || true) - git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2>/dev/null || true - if [[ -n "$branch" ]]; then - if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &>/dev/null; then - git -C "$REPO_ROOT" branch -D "$branch" 2>/dev/null || true - fi - fi - fi + local worktree_dir="$1" + if [[ -d "$worktree_dir" ]]; then + local branch + branch=$(git -C "$worktree_dir" branch --show-current 2> /dev/null || true) + git -C "$REPO_ROOT" worktree remove --force "$worktree_dir" 2> /dev/null || true + if [[ -n "$branch" ]]; then + if ! git -C "$REPO_ROOT" config "branch.${branch}.remote" &> /dev/null; then + git -C "$REPO_ROOT" branch -D "$branch" 2> /dev/null || true + fi + fi + fi } on_exit() { - if [[ -n "$ACTIVE_WORKTREE" ]]; then - cleanup_worktree "$ACTIVE_WORKTREE" - fi + if [[ -n "$ACTIVE_WORKTREE" ]]; then + cleanup_worktree "$ACTIVE_WORKTREE" + fi } # --- Docker --- build_docker_args() { - local workspace="$1" - DOCKER_ARGS=( - --rm - -v "$workspace:/workspace" - -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" - -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" - -v "collector-dev-claude:/home/dev/.claude" - -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud - -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json - -w /workspace - ) - - # Mount .git so worktree resolves (agent can't push — no SSH keys) - DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") - - for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do - if [[ -n "${!var:-}" ]]; then - DOCKER_ARGS+=(-e "$var=${!var}") - fi - done + local workspace="$1" + DOCKER_ARGS=( + --rm + -v "$workspace:/workspace" + -v "$HOME/.config/gcloud:/home/dev/.config/gcloud:ro" + -v "$HOME/.gitconfig:/home/dev/.gitconfig:ro" + -v "collector-dev-claude:/home/dev/.claude" + -e CLOUDSDK_CONFIG=/home/dev/.config/gcloud + -e GOOGLE_APPLICATION_CREDENTIALS=/home/dev/.config/gcloud/application_default_credentials.json + -w /workspace + ) + + # Mount .git so worktree resolves (agent can't push — no SSH keys) + DOCKER_ARGS+=(-v "$REPO_ROOT/.git:$REPO_ROOT/.git") + + for var in CLAUDE_CODE_USE_VERTEX GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_LOCATION ANTHROPIC_VERTEX_PROJECT_ID GITHUB_TOKEN; do + if [[ -n "${!var:-}" ]]; then + DOCKER_ARGS+=(-e "$var=${!var}") + fi + done } # --- Main --- case "${1:-}" in ---interactive | -i) - preflight - ACTIVE_WORKTREE=$(setup_worktree) - trap on_exit EXIT - BRANCH=$(git -C "$ACTIVE_WORKTREE" branch --show-current) - echo "Working in isolated worktree: $ACTIVE_WORKTREE" - echo "Branch: $BRANCH" - build_docker_args "$ACTIVE_WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" - ;; - ---local | -l) - shift - if [[ -n "$BRANCH_NAME" ]]; then - echo "ERROR: --branch cannot be used with --local" >&2 - exit 1 - fi - preflight - build_docker_args "$REPO_ROOT" - if [[ -z "${1:-}" ]]; then - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" - else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$*" - fi - ;; - ---shell | -s) - check_docker - check_image - ACTIVE_WORKTREE=$(setup_worktree) - trap on_exit EXIT - echo "Working in isolated worktree: $ACTIVE_WORKTREE" - build_docker_args "$ACTIVE_WORKTREE" - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh - ;; - -"" | --help | -h) - cat <&2 + exit 1 + fi + preflight + build_docker_args "$REPO_ROOT" + if [[ -z "${1:-}" ]]; then + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" + else + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$*" + fi + ;; + + --shell | -s) + check_docker + check_image + ACTIVE_WORKTREE=$(setup_worktree) + trap on_exit EXIT + echo "Working in isolated worktree: $ACTIVE_WORKTREE" + build_docker_args "$ACTIVE_WORKTREE" + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" zsh + ;; + + "" | --help | -h) + cat << USAGE Usage: $0 "task" Run /dev-loop with TUI (implement → PR → CI green) $0 "/skill args" Run a specific skill with TUI @@ -237,38 +237,38 @@ Environment: GOOGLE_CLOUD_PROJECT GCP project ID GOOGLE_CLOUD_LOCATION Vertex AI region (e.g., us-east5) USAGE - exit 0 - ;; - -*) - preflight - ACTIVE_WORKTREE=$(setup_worktree) - BRANCH=$(git -C "$ACTIVE_WORKTREE" branch --show-current) - TASK="$*" - - # Push branch for /dev-loop so the agent can use GitHub MCP push_files - if [[ "$TASK" != /* ]]; then - echo "Pushing branch $BRANCH..." >&2 - git -C "$ACTIVE_WORKTREE" push -u origin "$BRANCH" >/dev/null 2>&1 - fi - - echo "Working in isolated worktree: $ACTIVE_WORKTREE" - echo "Branch: $BRANCH" - echo "Task: $TASK" - echo "---" - - trap on_exit EXIT - - build_docker_args "$ACTIVE_WORKTREE" - PROMPT="/dev-loop $TASK" - if [[ "$TASK" == /* ]]; then - PROMPT="$TASK" - fi - - if [[ "$NO_TUI" == "true" ]]; then - docker run "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" - else - docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" - fi - ;; + exit 0 + ;; + + *) + preflight + ACTIVE_WORKTREE=$(setup_worktree) + BRANCH=$(git -C "$ACTIVE_WORKTREE" branch --show-current) + TASK="$*" + + # Push branch for /dev-loop so the agent can use GitHub MCP push_files + if [[ "$TASK" != /* ]]; then + echo "Pushing branch $BRANCH..." >&2 + git -C "$ACTIVE_WORKTREE" push -u origin "$BRANCH" > /dev/null 2>&1 + fi + + echo "Working in isolated worktree: $ACTIVE_WORKTREE" + echo "Branch: $BRANCH" + echo "Task: $TASK" + echo "---" + + trap on_exit EXIT + + build_docker_args "$ACTIVE_WORKTREE" + PROMPT="/dev-loop $TASK" + if [[ "$TASK" == /* ]]; then + PROMPT="$TASK" + fi + + if [[ "$NO_TUI" == "true" ]]; then + docker run "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + else + docker run -it "${DOCKER_ARGS[@]}" "$IMAGE" "${CLAUDE_CMD[@]}" -p "$PROMPT" + fi + ;; esac