diff --git a/.devcontainer/.claude-config-resolved b/.devcontainer/.claude-config-resolved new file mode 120000 index 00000000000..11300fd7e21 --- /dev/null +++ b/.devcontainer/.claude-config-resolved @@ -0,0 +1 @@ +/Users/ivanshumkov/.claude \ No newline at end of file diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example new file mode 100644 index 00000000000..d46c177e4a2 --- /dev/null +++ b/.devcontainer/.env.example @@ -0,0 +1,6 @@ +# Claude Code: agents and skills to copy from ~/.claude/ into the container. +# Copy this file to .env and customize. Plugins are always synced automatically. +# +# Comma-separated names (agents without .md extension, skills as directory names) +CLAUDE_AGENTS= +CLAUDE_SKILLS= diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 00000000000..f1228114391 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,3 @@ +.env +.claude-host-config/ +.main-git-resolved diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3d82e9f34ae..7e63c7995b7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,11 @@ -# Use the official VS Code base image for dev containers +# Dash Platform Dev Container with Claude Code sandbox support FROM mcr.microsoft.com/devcontainers/base:ubuntu -# Install dependencies -RUN apt-get update && apt-get install -y \ +ARG TZ=UTC +ENV TZ="$TZ" + +# System dependencies for Rust, RocksDB, and protobuf +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libssl-dev \ pkg-config \ @@ -13,58 +16,90 @@ RUN apt-get update && apt-get install -y \ gnupg \ lsb-release \ software-properties-common \ - unzip + unzip \ + libsnappy-dev \ + # Developer tools + less \ + procps \ + fzf \ + man-db \ + ripgrep \ + fd-find \ + nano \ + vim \ + && apt-get clean && rm -rf /var/lib/apt/lists/* -# Switch to clang -RUN rm /usr/bin/cc && ln -s /usr/bin/clang /usr/bin/cc +# Use clang as default C compiler +RUN rm -f /usr/bin/cc && ln -s /usr/bin/clang /usr/bin/cc -# Install protoc - protobuf compiler (pin to 32.0) -# Alpine/system protoc may be outdated; install from releases +# Install protoc (pinned to 32.0) ARG TARGETARCH ARG PROTOC_VERSION=32.0 -RUN if [[ "$TARGETARCH" == "arm64" ]] ; then export PROTOC_ARCH=aarch_64; else export PROTOC_ARCH=x86_64; fi; \ - curl -Ls https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip \ +RUN if [ "$TARGETARCH" = "arm64" ]; then export PROTOC_ARCH=aarch_64; else export PROTOC_ARCH=x86_64; fi; \ + curl -Ls "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip" \ -o /tmp/protoc.zip && \ unzip -qd /opt/protoc /tmp/protoc.zip && \ rm /tmp/protoc.zip && \ ln -s /opt/protoc/bin/protoc /usr/bin/ -# Remove duplicate install; single install above is sufficient +# Install git-delta for better diffs +ARG GIT_DELTA_VERSION=0.18.2 +RUN ARCH=$(dpkg --print-architecture) && \ + curl -L "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ + -o /tmp/git-delta.deb && \ + dpkg -i /tmp/git-delta.deb && \ + rm /tmp/git-delta.deb + +# Persist bash/zsh history +RUN mkdir -p /commandhistory && \ + touch /commandhistory/.bash_history /commandhistory/.zsh_history && \ + chown -R vscode:vscode /commandhistory -# Switch to vscode user +# Switch to vscode user for Rust and cargo tools USER vscode ENV CARGO_HOME=/home/vscode/.cargo ENV PATH=$CARGO_HOME/bin:$PATH -# TODO: It doesn't sharing PATH between stages, so we need "source $HOME/.cargo/env" everywhere -COPY rust-toolchain.toml . -RUN TOOLCHAIN_VERSION="$(grep channel rust-toolchain.toml | awk '{print $3}' | tr -d '"')" && \ +# Install Rust toolchain from rust-toolchain.toml +COPY --chown=vscode:vscode rust-toolchain.toml /tmp/rust-toolchain.toml +RUN TOOLCHAIN_VERSION="$(grep channel /tmp/rust-toolchain.toml | awk '{print $3}' | tr -d '"')" && \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ -y \ --default-toolchain "${TOOLCHAIN_VERSION}" \ - --target wasm32-unknown-unknown + --target wasm32-unknown-unknown && \ + rm /tmp/rust-toolchain.toml -# Download and install cargo-binstall -ENV BINSTALL_VERSION=1.10.11 +# Install cargo-binstall for fast binary installs +ARG BINSTALL_VERSION=1.10.11 RUN set -ex; \ - if [ "$TARGETARCH" = "amd64" ]; then \ + if [ "$(uname -m)" = "x86_64" ]; then \ CARGO_BINSTALL_ARCH="x86_64-unknown-linux-musl"; \ - elif [ "$TARGETARCH" = "arm64" ] || [ "$TARGETARCH" = "aarch64" ]; then \ + elif [ "$(uname -m)" = "aarch64" ]; then \ CARGO_BINSTALL_ARCH="aarch64-unknown-linux-musl"; \ else \ - echo "Unsupported architecture: $TARGETARCH"; exit 1; \ + echo "Unsupported architecture: $(uname -m)"; exit 1; \ fi; \ - DOWNLOAD_URL="https://github.com/cargo-bins/cargo-binstall/releases/download/v${BINSTALL_VERSION}/cargo-binstall-${CARGO_BINSTALL_ARCH}.tgz"; \ - curl -L --fail --show-error "$DOWNLOAD_URL" -o /tmp/cargo-binstall.tgz; \ - tar -xzf /tmp/cargo-binstall.tgz -C /tmp cargo-binstall; \ - chmod +x /tmp/cargo-binstall; \ - /tmp/cargo-binstall -y --force cargo-binstall; \ - rm /tmp/cargo-binstall; \ - cargo binstall -V + curl -L --fail --show-error \ + "https://github.com/cargo-bins/cargo-binstall/releases/download/v${BINSTALL_VERSION}/cargo-binstall-${CARGO_BINSTALL_ARCH}.tgz" \ + -o /tmp/cargo-binstall.tgz && \ + tar -xzf /tmp/cargo-binstall.tgz -C /tmp cargo-binstall && \ + chmod +x /tmp/cargo-binstall && \ + /tmp/cargo-binstall -y --force cargo-binstall && \ + rm /tmp/cargo-binstall.tgz /tmp/cargo-binstall + +# Install wasm tools +RUN cargo binstall wasm-bindgen-cli@0.2.108 --locked \ + --no-discover-github-token --disable-telemetry --no-track --no-confirm && \ + cargo binstall wasm-pack --locked \ + --no-discover-github-token --disable-telemetry --no-track --no-confirm + +# Prepare directories (need root for /workspace) +USER root +RUN mkdir -p /home/vscode/.claude /workspace && \ + chown -R vscode:vscode /home/vscode/.claude /workspace +USER vscode -RUN cargo binstall wasm-bindgen-cli@0.2.103 --locked \ - --no-discover-github-token \ - --disable-telemetry \ - --no-track \ - --no-confirm +ENV SHELL=/bin/zsh +ENV EDITOR=nano +ENV VISUAL=nano diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000000..d40d6deba90 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,203 @@ +# Dev Container + +Sandboxed development environment for Dash Platform with Claude Code pre-configured for autonomous work. + +## What's Included + +- **Rust 1.92** with `wasm32-unknown-unknown` target +- **Node.js 24** with yarn 4.12.0 (via corepack) +- **Docker-in-Docker** for dashmate +- **Claude Code** with `bypassPermissions` mode +- protoc 32.0, wasm-bindgen-cli 0.2.108, wasm-pack, cargo-binstall +- Developer tools: git-delta, ripgrep, fd, fzf + +## Prerequisites + +### SSH keys (for git push/pull) + +VS Code forwards your host's SSH agent into the container automatically. Make sure your key is loaded: + +```bash +ssh-add --apple-use-keychain ~/.ssh/id_rsa # macOS +ssh-add ~/.ssh/id_rsa # Linux +``` + +Without this, `git push`/`git pull` will fail with `Permission denied (publickey)`. + +### Claude Code authentication + +Authenticate using **one or both** methods. + +### Option A: OAuth (recommended) + +Run on your **host machine** before opening the devcontainer: + +```bash +claude login +``` + +Your OAuth credentials (`~/.claude/.credentials.json`) and enabled plugins are copied into the container. Optionally, agents and skills listed in `.devcontainer/.env` are also copied. No conversation history, project memories, or host settings are transferred. If tokens expire, re-run `claude login` on the host and rebuild. + +You can also log in from inside the container using the print-link flow (no browser redirect needed): + +```bash +claude login --print-link +``` + +### Option B: OAuth Token + +```bash +export CLAUDE_CODE_OAUTH_TOKEN= +``` + +Set this in your shell profile. The token is forwarded into the container automatically. + +### Option C: API Key + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +``` + +Set this in your shell profile so it's available when VS Code launches. + +## Usage with VS Code + +1. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension +2. Open this repository in VS Code +3. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) and select **Dev Containers: Reopen in Container** +4. Wait for the build (first time takes a while — Rust toolchain, etc.) +5. Claude Code is ready in the integrated terminal: + + ```bash + claude # runs with full permissions, no prompts + ``` + +### Personal extensions + +The `devcontainer.json` includes shared team extensions (rust-analyzer, eslint, Claude Code, etc.). To add your own extensions to every dev container, set this in your **host** VS Code settings (`Cmd+,` → search "defaultExtensions"): + +```json +{ + "dev.containers.defaultExtensions": [ + "github.copilot", + "vscodevim.vim" + ] +} +``` + +## Usage with CLI (no VS Code) + +You can use the [devcontainer CLI](https://github.com/devcontainers/cli) directly: + +```bash +# Install the CLI +npm install -g @devcontainers/cli + +# Build the container +devcontainer build --workspace-folder . + +# Start and enter the container +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash + +# Run Claude Code directly +devcontainer exec --workspace-folder . claude --dangerously-skip-permissions +``` + +Or use Docker Compose / `docker exec` if you prefer: + +```bash +# Build +devcontainer build --workspace-folder . + +# Start in background +devcontainer up --workspace-folder . + +# Run Claude in headless mode (for CI/automation) +devcontainer exec --workspace-folder . claude -p "run the test suite" --dangerously-skip-permissions +``` + +## Claude Code customization + +### Plugins + +Enabled plugins from your host `~/.claude/settings.json` are automatically synced into the container. No configuration needed — plugin IDs contain no secrets. + +### Agents & skills + +To copy personal agents or skills from `~/.claude/` into the container, create a `.env` file: + +```bash +cp .devcontainer/.env.example .devcontainer/.env +``` + +Edit `.env` with comma-separated names: + +```bash +# Agents from ~/.claude/agents/ (without .md extension) +CLAUDE_AGENTS=blockchain-security-auditor,rust-engineer + +# Skills from ~/.claude/skills/ (directory names) +CLAUDE_SKILLS=my-custom-skill +``` + +The `.env` file is gitignored — each developer configures their own. + +### Project-level settings + +The project's `.claude/` directory (containing `settings.local.json` and `skills/`) is automatically available inside the container via the workspace bind mount. No extra configuration needed. + +## Security Model + +Claude Code runs with `bypassPermissions` inside the container — it can read, write, and execute anything. The container is the sandbox boundary. To minimize exposure: + +- **Only OAuth credentials** are copied from the host (`~/.claude/.credentials.json`). No conversation history, project memories, settings, hooks, scripts, or debug logs are transferred. +- **Enabled plugins** (just plugin IDs) are synced from host settings. Optionally, listed agents/skills (markdown files only) are copied. +- **A clean `settings.json`** is generated inside the container with `bypassPermissions` and enabled plugins — your host's permission allowlists, MCP server configs, and hooks are not copied. +- **No shell history** is persisted or shared with the container. +- **The `.git` directory** is mounted read-write (required for commits/pushes). This is the main trust boundary — Claude can push code. + +## Network Firewall (optional) + +By default, the container has unrestricted network access. To enable a restrictive firewall that only allows whitelisted services, add the following to `devcontainer.json`: + +```jsonc +"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"], +"postStartCommand": "sudo /usr/local/bin/init-firewall.sh", +"waitFor": "postStartCommand" +``` + +You'll also need to add `iptables ipset iproute2 dnsutils` to the `apt-get install` in the Dockerfile and uncomment the firewall COPY/sudoers block. See `init-firewall.sh` for the domain whitelist. + +## Persistent Data + +These items survive container rebuilds (stored in Docker named volumes): + +- `~/.cargo/registry` and `~/.cargo/git` — Rust dependency cache +- `target/` — Rust build artifacts +- `~/.claude/` — Claude Code credentials, settings, and optionally agents/skills from host + +## Troubleshooting + +### Git worktrees + +Git worktrees are supported automatically. The `init-host.sh` script (runs on the host) detects whether you opened a worktree or the main repo and mounts the main `.git` directory into the container. The `post-create.sh` script creates the necessary symlinks so git resolves the worktree paths correctly. Commits and pushes from inside the container work as expected. + +### Claude says "not authenticated" + +- Ensure `ANTHROPIC_API_KEY` is set in your host shell, **or** +- Run `claude login` on your host and rebuild the container, **or** +- Run `claude login --print-link` inside the container (no browser redirect needed) + +### `yarn install` fails + +- Run `corepack enable` first (should be done by `post-create.sh`) + +### Docker commands fail inside the container + +- Docker-in-Docker starts automatically. If it didn't, check `docker info`. + +### Firewall too restrictive (if enabled) + +- Edit `.devcontainer/init-firewall.sh` to add domains +- Or temporarily flush rules: `sudo iptables -F OUTPUT` diff --git a/.devcontainer/devcontainer-build.json b/.devcontainer/devcontainer-build.json deleted file mode 100644 index df88d659340..00000000000 --- a/.devcontainer/devcontainer-build.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "Dash Platform Dev Container", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - "customizations": { - "vscode": { - "settings": {}, - "extensions": [ - "arcanis.vscode-zipfs", - "chrmarti.regex", - "davidanson.vscode-markdownlint", - "ms-vscode.cmake-tools", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "vadimcn.vscode-lldb", - "rust-lang.rust-analyzer", - "tamasfe.even-better-toml", - "zhangyue.rust-mod-generator", - "ms-azuretools.vscode-docker" - ] - } - }, - "remoteUser": "vscode", - "mounts": [ - { - "source": "devcontainer-platform-cargo-registry-index-${devcontainerId}", - "target": "/home/vscode/.cargo/registry", - "type": "volume" - }, - { - "source": "devcontainer-platform-cargo-registry-cache-${devcontainerId}", - "target": "/home/vscode/.cargo/registry/cache", - "type": "volume" - }, - { - "source": "devcontainer-platform-cargo-git-db-${devcontainerId}", - "target": "/home/vscode/.cargo/git/db", - "type": "volume" - }, - { - "source": "devcontainer-platform-target-${devcontainerId}", - "target": "${containerWorkspaceFolder}/target", - "type": "volume" - } - ], - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "true", - "username": "vscode", - "userUid": "1000", - "userGid": "1000", - "upgradePackages": "true" - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "latest", - "ppa": "false" - }, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/node:1": { - "version": 20, - "installYarnUsingApt": false - }, - "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/schlich/devcontainer-features/starship:0": {}, - }, - "postCreateCommand": { - "git-safe": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "cargo-permissions": "sudo chown -R vscode:vscode /home/vscode/.cargo ${containerWorkspaceFolder}/target" - } -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 586571ba2b1..91676502a43 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,95 @@ { "name": "Dash Platform Dev Container", - "image": "ghcr.io/dashpay/platform/devcontainer:0.1.0" + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "TZ": "${localEnv:TZ:UTC}", + "GIT_DELTA_VERSION": "0.18.2" + } + }, + "init": true, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "editor.formatOnSave": true + }, + "extensions": [ + "anthropic.claude-code", + "arcanis.vscode-zipfs", + "chrmarti.regex", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "ms-azuretools.vscode-docker", + "eamodio.gitlens" + ] + } + }, + "remoteUser": "vscode", + "mounts": [ + { + "source": "devcontainer-platform-cargo-registry-${devcontainerId}", + "target": "/home/vscode/.cargo/registry", + "type": "volume" + }, + { + "source": "devcontainer-platform-cargo-git-${devcontainerId}", + "target": "/home/vscode/.cargo/git", + "type": "volume" + }, + { + "source": "devcontainer-platform-target-${devcontainerId}", + "target": "${containerWorkspaceFolder}/target", + "type": "volume" + }, + { + "source": "devcontainer-platform-claude-config-${devcontainerId}", + "target": "/home/vscode/.claude", + "type": "volume" + }, + { + "source": "${localWorkspaceFolder}/.devcontainer/.main-git-resolved", + "target": "/workspace/.host-main-git", + "type": "bind" + } + ], + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "24" + }, + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1": {}, + "ghcr.io/schlich/devcontainer-features/starship:0": {} + }, + "containerEnv": { + "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", + "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}", + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", + "NODE_OPTIONS": "--max-old-space-size=4096", + "DEVCONTAINER": "true" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace/platform,type=bind,consistency=delegated", + "workspaceFolder": "/workspace/platform", + "initializeCommand": "bash .devcontainer/init-host.sh", + "postCreateCommand": "bash .devcontainer/post-create.sh" } diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100755 index 00000000000..96e12ae0be5 --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# Network firewall for Claude Code devcontainer sandbox. +# Restricts outbound traffic to only necessary services. +# Based on Anthropic's official init-firewall.sh pattern. +set -euo pipefail + +# Skip if not running as root +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: init-firewall.sh must run as root (use sudo)" + exit 1 +fi + +# Skip if iptables is not available +if ! command -v iptables &>/dev/null; then + echo "WARNING: iptables not found, skipping firewall setup" + exit 0 +fi + +echo "Configuring devcontainer firewall..." + +# Flush existing rules +iptables -F OUTPUT 2>/dev/null || true +ipset destroy allowed_hosts 2>/dev/null || true + +# Create ipset for allowed hosts +ipset create allowed_hosts hash:ip hashsize 4096 + +# --- Resolve and allow domains --- + +# NOTE: DNS resolution is point-in-time. CDN-backed services rotate IPs. +# The ESTABLISHED,RELATED rule helps for long-lived connections. +# This script re-runs on every container start (postStartCommand). +resolve_and_allow() { + local domain="$1" + local ips + ips=$(dig +short "$domain" A 2>/dev/null | grep -E '^[0-9]+\.' || true) + for ip in $ips; do + ipset add allowed_hosts "$ip" 2>/dev/null || true + done +} + +# Claude / Anthropic API +resolve_and_allow "api.anthropic.com" +resolve_and_allow "sentry.io" +resolve_and_allow "statsig.anthropic.com" +resolve_and_allow "statsig.com" +resolve_and_allow "featuregates.org" +resolve_and_allow "prodregistryv2.org" + +# npm registry +resolve_and_allow "registry.npmjs.org" +resolve_and_allow "registry.yarnpkg.com" + +# Rust / crates.io +resolve_and_allow "crates.io" +resolve_and_allow "static.crates.io" +resolve_and_allow "index.crates.io" +resolve_and_allow "static.rust-lang.org" +resolve_and_allow "sh.rustup.rs" + +# GitHub (dynamic IP ranges - added as CIDR rules below since ipset doesn't support /16 etc.) +GITHUB_IPS=$(curl -s https://api.github.com/meta 2>/dev/null | jq -r '.web[], .api[], .git[], .actions[]' 2>/dev/null || true) +resolve_and_allow "github.com" +resolve_and_allow "api.github.com" +resolve_and_allow "raw.githubusercontent.com" +resolve_and_allow "objects.githubusercontent.com" +resolve_and_allow "codeload.github.com" +resolve_and_allow "ghcr.io" + +# VS Code marketplace +resolve_and_allow "marketplace.visualstudio.com" +resolve_and_allow "vscode.blob.core.windows.net" +resolve_and_allow "update.code.visualstudio.com" +resolve_and_allow "az764295.vo.msecnd.net" + +# Protobuf releases (via GitHub, already covered) + +# Docker Hub (for dashmate Docker-in-Docker) +resolve_and_allow "registry-1.docker.io" +resolve_and_allow "auth.docker.io" +resolve_and_allow "production.cloudflare.docker.com" + +# Dash-specific +resolve_and_allow "testnet.platform-explorer.com" + +# --- Apply iptables rules --- + +# Allow loopback +iptables -A OUTPUT -o lo -j ACCEPT + +# Allow established/related connections +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow DNS (UDP/TCP 53) +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT + +# Allow SSH +iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT + +# Allow private networks: Docker-in-Docker (172.x), dashmate local nodes, host services. +# Broad ranges are intentional — dashmate orchestrates multiple containers on Docker networks. +iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT +iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT +iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT + +# Allow resolved hosts +iptables -A OUTPUT -m set --match-set allowed_hosts dst -j ACCEPT + +# Allow GitHub CIDR ranges directly +for cidr in $GITHUB_IPS; do + iptables -A OUTPUT -d "$cidr" -j ACCEPT 2>/dev/null || true +done + +# Default deny all other outbound +iptables -A OUTPUT -j REJECT --reject-with icmp-port-unreachable + +echo "Firewall configured. Verifying..." + +# Verify: allowed domain should work +if curl -sf --max-time 5 -o /dev/null "https://api.github.com" 2>/dev/null; then + echo " [OK] api.github.com is reachable" +else + echo " [WARN] api.github.com is not reachable - firewall may be too restrictive" +fi + +# Verify: blocked domain should fail +if curl -sf --max-time 3 -o /dev/null "https://example.com" 2>/dev/null; then + echo " [WARN] example.com is reachable - firewall may not be working" +else + echo " [OK] example.com is blocked" +fi + +echo "Firewall setup complete." diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh new file mode 100755 index 00000000000..63214220c04 --- /dev/null +++ b/.devcontainer/init-host.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Runs on the HOST before container creation. +# Resolves git worktree paths so git works inside the container. +set -euo pipefail + +test -f "$HOME/.gitconfig" || touch "$HOME/.gitconfig" +mkdir -p "$HOME/.claude" + +# Stage ONLY the minimum Claude config needed for authentication. +# We deliberately skip: conversation history, project memories, debug logs, +# shell snapshots, plans, scripts, plugins cache, and other data that could +# leak information from other projects into the sandboxed container. +CLAUDE_STAGING=".devcontainer/.claude-host-config" +rm -rf "$CLAUDE_STAGING" +if [ -d "$HOME/.claude" ]; then + mkdir -p "$CLAUDE_STAGING" + # Credentials (OAuth tokens) — required for authentication + [ -f "$HOME/.claude/.credentials.json" ] && \ + cp -a "$HOME/.claude/.credentials.json" "$CLAUDE_STAGING/.credentials.json" 2>/dev/null || true + # Onboarding state — prevents setup wizard + [ -f "$HOME/.claude.json" ] && \ + cp -a "$HOME/.claude.json" "$CLAUDE_STAGING/.claude.json.root" 2>/dev/null || true + + # Plugins: always copy (just IDs, no secrets) + if [ -f "$HOME/.claude/settings.json" ]; then + jq '{enabledPlugins: .enabledPlugins}' "$HOME/.claude/settings.json" \ + > "$CLAUDE_STAGING/enabled-plugins.json" 2>/dev/null || true + fi + + # Agents & skills: copy only what the user listed in .env + CLAUDE_AGENTS="" + CLAUDE_SKILLS="" + ENV_FILE=".devcontainer/.env" + [ -f "$ENV_FILE" ] && source "$ENV_FILE" + + if [ -n "$CLAUDE_AGENTS" ]; then + mkdir -p "$CLAUDE_STAGING/agents" + IFS=',' read -ra AGENT_LIST <<< "$CLAUDE_AGENTS" + for agent in "${AGENT_LIST[@]}"; do + agent=$(echo "$agent" | xargs) + src="$HOME/.claude/agents/${agent}.md" + [ -f "$src" ] && cp -a "$src" "$CLAUDE_STAGING/agents/" 2>/dev/null || true + done + fi + + if [ -n "$CLAUDE_SKILLS" ]; then + mkdir -p "$CLAUDE_STAGING/skills" + IFS=',' read -ra SKILL_LIST <<< "$CLAUDE_SKILLS" + for skill in "${SKILL_LIST[@]}"; do + skill=$(echo "$skill" | xargs) + src="$HOME/.claude/skills/${skill}" + [ -d "$src" ] && cp -a "$src" "$CLAUDE_STAGING/skills/" 2>/dev/null || true + done + fi +fi + +# Resolve main .git directory for worktree support. +# Docker follows symlinks in bind mount sources, so we create a symlink +# at a known path that always points to the real .git directory. +# This way the same devcontainer.json works for both worktrees and main repo. +RESOLVED=".devcontainer/.main-git-resolved" + +if [ -f .git ]; then + # Worktree: .git file contains "gitdir: /path/to/main/.git/worktrees/name" + GITDIR=$(sed 's/gitdir: //' .git) + # Strip /worktrees/name to get the main .git directory + MAIN_GIT="${GITDIR%/worktrees/*}" + if [ -d "$MAIN_GIT" ]; then + ln -sfn "$MAIN_GIT" "$RESOLVED" + else + mkdir -p "$RESOLVED" + fi +elif [ -d .git ]; then + # Main repo: just point to our own .git + ln -sfn "$(pwd)/.git" "$RESOLVED" +else + # No git at all — empty dir so the mount doesn't fail + mkdir -p "$RESOLVED" +fi diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 00000000000..d63a69dffda --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Post-create setup for Dash Platform devcontainer with Claude Code. +# Runs once after the container is created. +set -euo pipefail + +WORKSPACE="/workspace/platform" + +echo "=== Dash Platform devcontainer post-create setup ===" + +# --- Git worktree support --- +# Git worktrees use a .git FILE pointing to an absolute host path like +# /Users/you/.../platform/.git/worktrees/v3. That path doesn't exist inside +# the container. init-host.sh mounted the main .git at /workspace/.host-main-git. +# We create a symlink from the absolute host path so git can follow it. +if [ -f "$WORKSPACE/.git" ] && [ -d "/workspace/.host-main-git" ]; then + GITDIR=$(sed 's/gitdir: //' "$WORKSPACE/.git") + if [ ! -d "$GITDIR" ]; then + # e.g. /Users/you/Projects/dashpay/platform/.git + MAIN_GIT_HOST_PATH="$(dirname "$(dirname "$GITDIR")")" + sudo mkdir -p "$(dirname "$MAIN_GIT_HOST_PATH")" + sudo ln -sfn /workspace/.host-main-git "$MAIN_GIT_HOST_PATH" + echo "Git worktree: linked $MAIN_GIT_HOST_PATH -> /workspace/.host-main-git" + fi +fi + +# --- Git configuration --- +git config --global --add safe.directory "$WORKSPACE" + +# --- Cargo permissions --- +sudo chown -R vscode:vscode /home/vscode/.cargo "$WORKSPACE/target" 2>/dev/null || true + +# --- Enable corepack for yarn --- +corepack enable 2>/dev/null || true + +# --- Claude Code: copy config staged by init-host.sh --- +# init-host.sh stages credentials, plugin list, and optionally agents/skills. +# We copy into the persistent volume, create a minimal settings.json with +# plugins merged in, then clean up the staging copy. +CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-/home/vscode/.claude}" +HOST_CONFIG="$WORKSPACE/.devcontainer/.claude-host-config" + +mkdir -p "$CLAUDE_DIR" + +if [ -d "$HOST_CONFIG" ] && [ "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then + echo "Copying Claude config from host..." + # OAuth credentials + if [ -f "$HOST_CONFIG/.credentials.json" ]; then + cp -a "$HOST_CONFIG/.credentials.json" "$CLAUDE_DIR/.credentials.json" + chmod 600 "$CLAUDE_DIR/.credentials.json" + fi + # Onboarding state (prevents setup wizard) + if [ -f "$HOST_CONFIG/.claude.json.root" ]; then + cp -a "$HOST_CONFIG/.claude.json.root" /home/vscode/.claude.json + chown vscode:vscode /home/vscode/.claude.json + fi + echo "Host Claude credentials copied." +else + echo "No host Claude credentials found. Use ANTHROPIC_API_KEY or 'claude login'." +fi + +# Write a clean settings.json with bypassPermissions (no host settings leak) +SETTINGS_FILE="$CLAUDE_DIR/settings.json" +cat > "$SETTINGS_FILE" <<'SETTINGS' +{ + "permissions": { + "defaultMode": "bypassPermissions" + }, + "skipDangerousModePermissionPrompt": true +} +SETTINGS + +# Merge host's enabledPlugins into settings (plugin IDs only, no secrets) +if [ -f "$HOST_CONFIG/enabled-plugins.json" ]; then + TMP=$(mktemp) + jq -s '.[0] * .[1]' "$SETTINGS_FILE" "$HOST_CONFIG/enabled-plugins.json" \ + > "$TMP" 2>/dev/null && mv "$TMP" "$SETTINGS_FILE" || true +fi + +# Copy host agent definitions +if [ -d "$HOST_CONFIG/agents" ] && [ "$(ls -A "$HOST_CONFIG/agents" 2>/dev/null)" ]; then + mkdir -p "$CLAUDE_DIR/agents" + cp -a "$HOST_CONFIG/agents/"* "$CLAUDE_DIR/agents/" + echo "Host Claude agents copied." +fi + +# Copy host skill definitions +if [ -d "$HOST_CONFIG/skills" ] && [ "$(ls -A "$HOST_CONFIG/skills" 2>/dev/null)" ]; then + mkdir -p "$CLAUDE_DIR/skills" + cp -a "$HOST_CONFIG/skills/"* "$CLAUDE_DIR/skills/" + echo "Host Claude skills copied." +fi + +chown -R vscode:vscode "$CLAUDE_DIR" + +# Clean up staged config from the workspace +rm -rf "$HOST_CONFIG" + +echo "=== Post-create setup complete ===" +echo "Claude Code is configured with bypassPermissions mode." +echo "Set ANTHROPIC_API_KEY in your host environment before opening this devcontainer." diff --git a/.github/workflows/prebuild-devcontainers.yml b/.github/workflows/prebuild-devcontainers.yml deleted file mode 100644 index dcf2dd5a49f..00000000000 --- a/.github/workflows/prebuild-devcontainers.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Prebuild Dev Containers - -on: - push: - paths: - - '.devcontainer/**' - - '.github/workflows/prebuild-devcontainers.yml' - - rust-toolchain.toml - - Dockerfile - branches: - - master - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Build and push devcontainer - runs-on: ubuntu-24.04 - timeout-minutes: 240 - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Setup Node.JS - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Install skopeo - run: | - sudo apt-get update - sudo apt-get install -y skopeo - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - with: - use: true - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: dashpay - password: ${{ secrets.GHCR_TOKEN }} - - - name: Build and push Platform devcontainer - uses: devcontainers/ci@v0.3 - with: - imageName: ghcr.io/dashpay/platform/devcontainer - imageTag: 0.1.0 - platform: linux/amd64,linux/arm64 - configFile: .devcontainer/devcontainer-build.json - push: always - cacheFrom: ghcr.io/dashpay/platform/devcontainer diff --git a/.gitignore b/.gitignore index c1874047d34..0d98090dd40 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ # ignore VSCode project specific files .vscode +# Devcontainer host-resolved symlink (machine-specific) +.devcontainer/.main-git-resolved +.devcontainer/.claude-host-config + # Env file .env diff --git a/README.md b/README.md index 36b7b96f90d..da17ae14691 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,28 @@ this repository may be used on the following networks: ### How to build and set up a node from the code in this repo? +#### Using Dev Container (recommended) + +The easiest way to get started is with a [Dev Container](.devcontainer/README.md) +which provides a pre-configured environment with all dependencies and +[Claude Code](https://claude.ai/code) sandboxed for autonomous development. + +Open this repo in VS Code and select **Dev Containers: Reopen in Container**, or +use the CLI: + +```bash +npm install -g @devcontainers/cli +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +See [.devcontainer/README.md](.devcontainer/README.md) for full details. + +#### Manual setup + - Clone the repo - Install prerequisites: - - [node.js](https://nodejs.org/) v20 + - [node.js](https://nodejs.org/) v24 - [docker](https://docs.docker.com/get-docker/) v20.10+ - [rust](https://www.rust-lang.org/tools/install) v1.92+, with wasm32 target (`rustup target add wasm32-unknown-unknown`) - [protoc - protobuf compiler](https://github.com/protocolbuffers/protobuf/releases) v32.0+ @@ -78,7 +97,7 @@ this repository may be used on the following networks: in terminal run `echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc` or `echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile` depending on your default shell. You can find your default shell with `echo $SHELL` - Reload your shell with `source ~/.zshrc` or `source ~/.bash_profile` - - `cargo install wasm-bindgen-cli@0.2.103` + - `cargo install wasm-bindgen-cli@0.2.108` - *double-check that wasm-bindgen-cli version above matches wasm-bindgen version in Cargo.lock file* - *Depending on system, additional packages may need to be installed as a prerequisite for wasm-bindgen-cli. If anything is missing, installation will error and prompt what packages are missing (i.e. clang, llvm, libssl-dev)* - essential build tools - example for Debian/Ubuntu: `apt install -y build-essential libssl-dev pkg-config clang cmake llvm`