diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14515f43..04b3aaaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,5 @@ name: CI -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: push: branches: [main, staging] @@ -17,11 +14,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-2022] go-version: ['1.25.9'] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 @@ -40,6 +37,17 @@ jobs: - name: Build run: go build -v ./cmd/rampart + - name: Installer scripts are synced + if: runner.os == 'Linux' + run: | + cmp -s install.sh docs/install + cmp -s install.sh docs/install.sh + cmp -s install.sh scripts/install.sh + sh -n install.sh + sh -n docs/install + sh -n docs/install.sh + sh -n scripts/install.sh + - name: OpenClaw plugin regression tests if: runner.os != 'Windows' run: | @@ -65,11 +73,65 @@ jobs: run: go test -bench=. -benchmem ./internal/engine/ ./internal/audit/ - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-${{ matrix.os }} path: coverage.out + docker-smoke: + name: docker smoke (amd64 runtime + arm64 build) + runs-on: ubuntu-latest + needs: test + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - uses: docker/setup-buildx-action@v4 + + - name: Build amd64 image + run: | + set -euo pipefail + docker buildx build \ + --platform linux/amd64 \ + --load \ + --build-arg VERSION=ci-smoke \ + --build-arg COMMIT=${GITHUB_SHA::12} \ + --build-arg DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ + -t rampart:ci-smoke \ + . + + - name: Smoke test amd64 image + run: | + set -euo pipefail + + docker run --rm rampart:ci-smoke version | tee /tmp/rampart-docker-version.txt + grep -F "rampart ci-smoke" /tmp/rampart-docker-version.txt + + cid="$(docker run -d -p 19090:9090 rampart:ci-smoke)" + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + + for _ in $(seq 1 20); do + if curl -fsS http://127.0.0.1:19090/healthz >/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + + docker logs "$cid" + exit 1 + + - name: Build arm64 image + run: | + set -euo pipefail + docker buildx build \ + --platform linux/arm64 \ + --build-arg VERSION=ci-smoke \ + --build-arg COMMIT=${GITHUB_SHA::12} \ + --build-arg DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ + --output=type=cacheonly \ + . + release-dry-run: name: goreleaser snapshot (cross-platform build check) runs-on: ubuntu-latest @@ -77,7 +139,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/community-policy-ci.yml b/.github/workflows/community-policy-ci.yml index cc8d9cb4..357f9f33 100644 --- a/.github/workflows/community-policy-ci.yml +++ b/.github/workflows/community-policy-ci.yml @@ -1,8 +1,5 @@ name: Community Policy CI -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: pull_request: paths: @@ -17,7 +14,7 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6cf7e529..0ebfe407 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,8 +1,5 @@ name: Docker -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: push: tags: ['v*'] @@ -15,33 +12,88 @@ jobs: docker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - - uses: docker/setup-qemu-action@v4 + - uses: actions/checkout@v6 - uses: docker/setup-buildx-action@v4 - - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@v6 id: meta with: images: ghcr.io/peg/rampart tags: | type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }} type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} - - uses: docker/build-push-action@v7 + - name: Compute build metadata + id: build + run: | + echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + echo "commit=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" + echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + - name: Build local image for smoke test + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64 + load: true + tags: rampart:release-smoke + build-args: | + VERSION=${{ steps.build.outputs.version }} + COMMIT=${{ steps.build.outputs.commit }} + DATE=${{ steps.build.outputs.date }} + cache-from: type=gha + + - name: Smoke test local Docker image + env: + IMAGE: rampart:release-smoke + VERSION: ${{ steps.build.outputs.version }} + run: | + set -euo pipefail + + docker run --rm "$IMAGE" version | tee /tmp/rampart-docker-version.txt + grep -F "rampart ${VERSION}" /tmp/rampart-docker-version.txt + + cid="$(docker run -d -p 19090:9090 "$IMAGE")" + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + + for _ in $(seq 1 20); do + if curl -fsS http://127.0.0.1:19090/healthz >/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + + docker logs "$cid" + exit 1 + + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.build.outputs.version }} + COMMIT=${{ steps.build.outputs.commit }} + DATE=${{ steps.build.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max + + - name: Verify multi-arch manifest + env: + IMAGE: ghcr.io/peg/rampart:${{ steps.build.outputs.version }} + run: | + set -euo pipefail + docker buildx imagetools inspect "$IMAGE" | tee /tmp/rampart-docker-manifest.txt + grep -F 'linux/amd64' /tmp/rampart-docker-manifest.txt + grep -F 'linux/arm64' /tmp/rampart-docker-manifest.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0195da1e..b1c74568 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,5 @@ name: Deploy Docs -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: workflow_dispatch: push: @@ -19,7 +16,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb97030c..e8be2ca0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,5 @@ name: Release -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: push: tags: @@ -15,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/render-diagrams.yml b/.github/workflows/render-diagrams.yml index 6b2b7576..f079324e 100644 --- a/.github/workflows/render-diagrams.yml +++ b/.github/workflows/render-diagrams.yml @@ -1,8 +1,5 @@ name: Render Diagrams -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - on: push: branches: [main, staging] @@ -16,7 +13,7 @@ jobs: render: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install D2 run: curl -fsSL https://d2lang.com/install.sh | sh -s -- diff --git a/CHANGELOG.md b/CHANGELOG.md index 6029fe53..84a94eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] - 2026-05-06 + +### Fixed + +- **Docker images now boot and report release metadata** — The Dockerfile uses the current `serve --addr/--port` flags, injects version/commit/date ldflags, aligns its Go toolchain with the release workflow, and sets a writable runtime home for the nonroot distroless container. +- **Installer surfaces are canonical again** — `install.sh`, `docs/install`, `docs/install.sh`, and `scripts/install.sh` are byte-for-byte synced, with CI checks to prevent future drift. + +### Changed + +- **1.0 launch metadata is aligned** — The embedded OpenClaw plugin manifest, runtime export, package metadata, landing structured data, docs homepage, support matrix, and roadmap now use final `1.0.0` launch language instead of stale RC labels. +- **Release docs point at the live package channels** — Homebrew examples use `peg/tap/rampart`, binary download docs describe the actual archive formats, and Docker docs describe stable/minor/prerelease tags accurately. + ## [1.0.0-rc.2] - 2026-05-04 ### Fixed diff --git a/Dockerfile b/Dockerfile index 0107a0d7..7c1c3eef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,32 @@ -# Build stage -FROM golang:1.24-bookworm AS build +# Build stage. Build on the native runner platform and cross-compile for the +# requested image platform so linux/arm64 releases don't depend on slow QEMU +# emulation for the Go build itself. +ARG BUILDPLATFORM +FROM --platform=$BUILDPLATFORM golang:1.25.9-bookworm AS build WORKDIR /src + +ARG TARGETOS +ARG TARGETARCH +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown + COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /rampart ./cmd/rampart +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -trimpath \ + -ldflags="-s -w \ + -X github.com/peg/rampart/internal/build.versionFromLDFlags=${VERSION} \ + -X github.com/peg/rampart/internal/build.Commit=${COMMIT} \ + -X github.com/peg/rampart/internal/build.Date=${DATE}" \ + -o /rampart ./cmd/rampart # Runtime stage — distroless for minimal attack surface FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /rampart /rampart USER nonroot:nonroot +ENV HOME=/tmp +WORKDIR /tmp EXPOSE 9090 ENTRYPOINT ["/rampart"] -CMD ["serve", "--bind", "0.0.0.0:9090"] +CMD ["serve", "--addr", "0.0.0.0", "--port", "9090"] diff --git a/Makefile b/Makefile index 25cf5b67..e0004caf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS = -s -w \ - -X github.com/peg/rampart/internal/build.Version=$(VERSION) \ + -X github.com/peg/rampart/internal/build.versionFromLDFlags=$(VERSION) \ -X github.com/peg/rampart/internal/build.Commit=$(COMMIT) \ -X github.com/peg/rampart/internal/build.Date=$(DATE) diff --git a/SECURITY.md b/SECURITY.md index 1f583057..ee304f93 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,9 +4,9 @@ | Version | Supported | |---------|--------------------| -| 0.7.x | ✅ Current release | -| 0.6.x | ⚠️ Critical fixes only | -| < 0.6 | ❌ No longer supported | +| 1.0.x | ✅ Current release | +| 0.9.x | ⚠️ Critical fixes only | +| < 0.9 | ❌ No longer supported | ## Reporting a Vulnerability diff --git a/cmd/rampart/cli/doctor.go b/cmd/rampart/cli/doctor.go index b1768d11..a9a965f9 100644 --- a/cmd/rampart/cli/doctor.go +++ b/cmd/rampart/cli/doctor.go @@ -1635,23 +1635,15 @@ func normalizedReleaseVersion(version string) (string, bool) { if strings.Contains(version, "-g") { return "", false } - base := strings.SplitN(version, "+", 2)[0] - version = strings.SplitN(base, "-", 2)[0] - parts := strings.Split(version, ".") - if len(parts) != 3 { + parsed, ok := parseReleaseVersion(version) + if !ok { return "", false } - for _, part := range parts { - if part == "" { - return "", false - } - for _, r := range part { - if r < '0' || r > '9' { - return "", false - } - } + normalized := fmt.Sprintf("%d.%d.%d", parsed.major, parsed.minor, parsed.patch) + if len(parsed.prerelease) > 0 { + normalized += "-" + strings.Join(parsed.prerelease, ".") } - return version, true + return normalized, true } func isGoPseudoVersion(version string) bool { diff --git a/cmd/rampart/cli/doctor_test.go b/cmd/rampart/cli/doctor_test.go index b27e3696..89191c3f 100644 --- a/cmd/rampart/cli/doctor_test.go +++ b/cmd/rampart/cli/doctor_test.go @@ -682,6 +682,9 @@ func TestPluginVersionMatchesBuildVersion(t *testing.T) { {"0.9.22", "0.9.22", true}, {"0.9.22", "v0.9.23", false}, {"1.0.0-rc.1", "v1.0.0-rc.1", true}, + {"1.0.0-rc.2", "v1.0.0-rc.3", false}, + {"1.0.0-rc.2", "v1.0.0", false}, + {"1.0.0", "v1.0.0-rc.2", false}, {"0.9.22", "v1.0.0-rc.1", false}, {"0.9.22", "v0.9.22-staging-47fa0cf", true}, {"0.9.22", "v0.9.22-33-g47fa0cf", true}, diff --git a/cmd/rampart/cli/setup_openclaw_plugin_test.go b/cmd/rampart/cli/setup_openclaw_plugin_test.go index 4cd4fa7c..49d70151 100644 --- a/cmd/rampart/cli/setup_openclaw_plugin_test.go +++ b/cmd/rampart/cli/setup_openclaw_plugin_test.go @@ -9,10 +9,10 @@ import ( func TestExtractOpenClawPluginRuntimeVersion(t *testing.T) { got := extractOpenClawPluginRuntimeVersion(`export const id = "rampart"; -export const version = "1.0.0-rc.2"; +export const version = "1.0.0"; `) - if got != "1.0.0-rc.2" { - t.Fatalf("runtime version = %q, want 1.0.0-rc.2", got) + if got != "1.0.0" { + t.Fatalf("runtime version = %q, want 1.0.0", got) } } diff --git a/docs-site/features/mcp-proxy.md b/docs-site/features/mcp-proxy.md index 80215f1f..59179503 100644 --- a/docs-site/features/mcp-proxy.md +++ b/docs-site/features/mcp-proxy.md @@ -43,8 +43,7 @@ In your agent's MCP config (Claude Desktop, Cursor, etc.): ```d2 direction: right -client: "MCP Client -(Claude Desktop, Cursor…)" {shape: oval} +client: "MCP Client\\n(Claude Desktop, Cursor…)" {shape: oval} rampart: "rampart mcp" {style.border-radius: 8} engine: "Policy Engine" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 8 @@ -128,7 +127,7 @@ rampart mcp -- npx -y @modelcontextprotocol/server-postgres postgres://localhost ### 1. Install Rampart ```bash -brew tap peg/rampart && brew install rampart +brew install peg/tap/rampart ``` ### 2. Create a Policy diff --git a/docs-site/features/policy-engine.md b/docs-site/features/policy-engine.md index f4db61ab..3872020f 100644 --- a/docs-site/features/policy-engine.md +++ b/docs-site/features/policy-engine.md @@ -14,11 +14,9 @@ direction: down call: "Tool Call" {shape: oval} -match: "Match policies -by tool type" {shape: diamond} +match: "Match policies\\nby tool type" {shape: diamond} -rules: "Evaluate rules -top-to-bottom" {shape: diamond} +rules: "Evaluate rules\\ntop-to-bottom" {shape: diamond} deny: "Denied" { style.fill: "#2d1b1b" @@ -52,8 +50,7 @@ allow: "Allowed" { style.border-radius: 6 } -default: "Default Action -(allow or deny)" { +default: "Default Action\\n(allow or deny)" { style.border-radius: 6 style.stroke-dash: 4 } diff --git a/docs-site/features/semantic-verification.md b/docs-site/features/semantic-verification.md index 2bc1c462..292a7743 100644 --- a/docs-site/features/semantic-verification.md +++ b/docs-site/features/semantic-verification.md @@ -129,8 +129,7 @@ policies: direction: right agent: "AI Agent" {shape: oval} -rampart: "Rampart -Policy Engine" { +rampart: "Rampart\\nPolicy Engine" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 8 } @@ -146,10 +145,8 @@ verify: { style.stroke-dash: 4 style.border-radius: 8 - redact: "Redact secrets -from command" {style.border-radius: 6} - llm: "LLM -(gpt-4o-mini / Haiku / Ollama)" {style.border-radius: 6} + redact: "Redact secrets\\nfrom command" {style.border-radius: 6} + llm: "LLM\\n(gpt-4o-mini / Haiku / Ollama)" {style.border-radius: 6} redact -> llm: "sanitized command" } diff --git a/docs-site/getting-started/installation.md b/docs-site/getting-started/installation.md index 928826b9..8c92b948 100644 --- a/docs-site/getting-started/installation.md +++ b/docs-site/getting-started/installation.md @@ -23,7 +23,7 @@ description: "Install Rampart on Windows, macOS, or Linux. Get the security laye **Homebrew (recommended):** ```bash - brew tap peg/rampart && brew install rampart + brew install peg/tap/rampart ``` **One-liner:** @@ -36,7 +36,7 @@ description: "Install Rampart on Windows, macOS, or Linux. Get the security laye The fastest way to install Rampart: ```bash -brew tap peg/rampart && brew install rampart +brew install peg/tap/rampart ``` This installs the `rampart` binary. @@ -60,15 +60,15 @@ go install github.com/peg/rampart/cmd/rampart@latest Download pre-built binaries from [GitHub Releases](https://github.com/peg/rampart/releases). -Binaries are available for Linux (amd64/arm64) as `.tar.gz` and macOS (amd64/arm64) as `.zip`: +Binaries are available for Linux and macOS (amd64/arm64) as `.tar.gz` archives. Windows builds are published as `.zip` archives: ```bash # Example: Linux amd64 tar xzf rampart_*_linux_amd64.tar.gz sudo mv rampart /usr/local/bin/ -# Example: macOS (unzip, then move) -unzip rampart_*_darwin_arm64.zip +# Example: macOS arm64 +tar xzf rampart_*_darwin_arm64.tar.gz sudo mv rampart /usr/local/bin/ ``` @@ -77,7 +77,7 @@ sudo mv rampart /usr/local/bin/ Multi-arch container image (amd64 + arm64), built on distroless for minimal attack surface: ```bash -docker run ghcr.io/peg/rampart:latest serve --addr 0.0.0.0 --port 9090 +docker run --rm -p 9090:9090 ghcr.io/peg/rampart:latest ``` Or use with docker-compose. First, create a policy file (e.g. `mkdir policies && rampart init > policies/rampart.yaml`): @@ -97,7 +97,7 @@ volumes: rampart-audit: ``` -Available tags: `latest`, `0.9.12`, `0.9`. `latest` always points to the current stable release. Pin to a specific version tag for reproducibility. Images are published on [GitHub Container Registry](https://github.com/peg/rampart/pkgs/container/rampart). +Available tags include full versions such as `1.0.0`, minor versions such as `1.0`, and `latest` for the current stable release. Prereleases use their full tag, for example `1.0.0-rc.3`, and do not move `latest`. Pin to a specific version tag for reproducibility. Images are published on [GitHub Container Registry](https://github.com/peg/rampart/pkgs/container/rampart). ## Build from Source diff --git a/docs-site/getting-started/quickstart.md b/docs-site/getting-started/quickstart.md index e4bb9bbe..76896320 100644 --- a/docs-site/getting-started/quickstart.md +++ b/docs-site/getting-started/quickstart.md @@ -32,7 +32,7 @@ Before you dive in, skim the [integration support matrix](support-matrix.md) if === "Homebrew" ```bash - brew tap peg/rampart && brew install rampart + brew install peg/tap/rampart ``` ## One-Command Setup diff --git a/docs-site/getting-started/support-matrix.md b/docs-site/getting-started/support-matrix.md index ab6ac4b3..81cda8cd 100644 --- a/docs-site/getting-started/support-matrix.md +++ b/docs-site/getting-started/support-matrix.md @@ -46,13 +46,13 @@ Use this page as the canonical support contract for Rampart's main integration s Native plugin
rampart setup openclaw Required First-class plugin approvals / native approval UI - Recommended RC baseline + Recommended OpenClaw 2026.4.29 - 2026.5.1 Native plugin
rampart setup openclaw Required - Native plugin startup/interception; approval delivery was not the RC baseline + Native plugin startup/interception; approval delivery was not the launch baseline Supported @@ -90,7 +90,7 @@ Use this page as the canonical support contract for Rampart's main integration s - **Claude Code** → best overall native path - **Codex CLI** → best CLI path when you want strong coverage -- **OpenClaw >= 2026.5.2** → best OpenClaw path for the 1.0 RC baseline; plugin + native approval UI +- **OpenClaw >= 2026.5.2** → best OpenClaw path for 1.0; plugin + native approval UI - **Cline** → good supported path, but less polished approval UX than Claude Code ## Degraded behavior notes diff --git a/docs-site/getting-started/troubleshooting.md b/docs-site/getting-started/troubleshooting.md index fb1fc712..36419d8f 100644 --- a/docs-site/getting-started/troubleshooting.md +++ b/docs-site/getting-started/troubleshooting.md @@ -165,7 +165,7 @@ rampart test --tool read "/etc/shadow" ```bash openclaw plugins list -# Should show: rampart v0.9.12 ✓ active +# Should show: rampart 1.0.0 ✓ active rampart doctor # Should show: ✓ OpenClaw plugin: installed (before_tool_call hook active) diff --git a/docs-site/getting-started/tutorial.md b/docs-site/getting-started/tutorial.md index 11298854..10d8d5f2 100644 --- a/docs-site/getting-started/tutorial.md +++ b/docs-site/getting-started/tutorial.md @@ -38,7 +38,7 @@ Let's set it up. === "Homebrew" ```bash - brew tap peg/rampart && brew install rampart + brew install peg/tap/rampart ``` Verify: diff --git a/docs-site/guides/agent-install.md b/docs-site/guides/agent-install.md index 50b1df01..e11c6212 100644 --- a/docs-site/guides/agent-install.md +++ b/docs-site/guides/agent-install.md @@ -32,7 +32,7 @@ Follow these steps in order. Each step is independently verifiable. === "Homebrew (macOS)" ```bash - brew tap peg/rampart && brew install rampart + brew install peg/tap/rampart ``` Verify the install: diff --git a/docs-site/guides/securing-claude-code.md b/docs-site/guides/securing-claude-code.md index 2dd679c3..cf4b4522 100644 --- a/docs-site/guides/securing-claude-code.md +++ b/docs-site/guides/securing-claude-code.md @@ -22,7 +22,7 @@ The goal is not to disable Claude Code automation. The goal is to add guardrails 1. Install Rampart. ```bash -brew tap peg/rampart && brew install rampart +brew install peg/tap/rampart ``` 2. Wire Rampart into Claude Code. diff --git a/docs-site/index.html b/docs-site/index.html index 8f37f3be..af37ba06 100644 --- a/docs-site/index.html +++ b/docs-site/index.html @@ -27,7 +27,7 @@ "url": "https://rampart.sh", "applicationCategory": "SecurityApplication", "operatingSystem": "Linux, macOS, Windows", - "softwareVersion": "1.0.0-rc.2", + "softwareVersion": "1.0.0", "offers": { "@type": "Offer", "price": "0", @@ -1112,7 +1112,7 @@
-
v1.0.0-rc.2 · release candidate
+
v1.0.0 · launch-ready

Your agent has root.
That’s the problem.

Rampart sits in the execution path. Install it, run rampart quickstart, and it wires the right protection path for Claude Code, Codex, Cline, or OpenClaw before risky tool calls run.

diff --git a/docs-site/index.md b/docs-site/index.md index a4739b3c..1a511693 100644 --- a/docs-site/index.md +++ b/docs-site/index.md @@ -78,7 +78,7 @@ Rampart also scans tool **responses** — if your agent reads a file containing ```bash # Install -brew tap peg/rampart && brew install rampart +brew install peg/tap/rampart # Claude Code rampart setup claude-code @@ -131,19 +131,19 @@ intercept: { mcp: "MCP Proxy" } -engine: "YAML Policy Engine\n<10μs" { +engine: "YAML Policy Engine\\n<10μs" { style.fill: "#1d3320" style.stroke: "#2ea043" style.font-color: "#3fb950" style.border-radius: 8 } -verify: "rampart-verify\n(optional sidecar)" { +verify: "rampart-verify\\n(optional sidecar)" { style.stroke-dash: 4 style.border-radius: 8 } -audit: "Audit Trail\nhash-chained" { +audit: "Audit Trail\\nhash-chained" { style.border-radius: 8 } @@ -205,13 +205,13 @@ verify -> outcomes.approval [:octicons-arrow-right-24: See all integration guides](integrations/index.md) -## What's New in v1.0.0-rc.2 +## What's New in v1.0 -- **Prerelease update checks are sane** — `rampart doctor` no longer suggests downgrading from the 1.0 RC line to the older stable `v0.9.22` release. -- **OpenClaw 2026.5.2 is the RC baseline** — Rampart now uses OpenClaw's first-class plugin approval path as the single human-approval owner, with Rampart handling policy, audit, and durable allow-always persistence. [Details →](integrations/openclaw.md) +- **Update checks are sane** — `rampart doctor` understands the 1.0 release line and no longer suggests downgrading release candidates to the older stable `v0.9.22` release. +- **OpenClaw 2026.5.6 verified for launch** — Rampart uses OpenClaw's first-class plugin approval path as the single human-approval owner, with Rampart handling policy, audit, and durable allow-always persistence. [Details →](integrations/openclaw.md) - **Degraded mode is explicit** — sensitive OpenClaw tools block when `rampart serve` is unavailable, while only configured lower-risk `failOpenTools` may proceed. -- **Setup and doctor are release-candidate strict** — `rampart setup openclaw` installs the native plugin cleanly, repairs approval-hardening drift, and `rampart doctor` checks plugin state, serve reachability, approval timeout alignment, and version coherence. -- **Matching and bypass regressions are tighter** — shell-wrapper normalization, URL/domain handling, path matching, and OpenClaw plugin approval/degraded-mode tests now cover the hard edges found during the RC pass. +- **Setup and doctor are launch-strict** — `rampart setup openclaw` installs the native plugin cleanly, repairs approval-hardening drift, and `rampart doctor` checks plugin state, serve reachability, approval timeout alignment, and version coherence. +- **Matching and bypass regressions are tighter** — shell-wrapper normalization, URL/domain handling, path matching, and OpenClaw plugin approval/degraded-mode tests now cover the hard edges found during the 1.0 RC pass. ### v0.9.22 diff --git a/docs-site/integrations/index.md b/docs-site/integrations/index.md index 0e48c7cf..4a7db350 100644 --- a/docs-site/integrations/index.md +++ b/docs-site/integrations/index.md @@ -58,12 +58,10 @@ start: "Your agent" {shape: oval} q: "Integration method?" {shape: diamond} -hooks: "rampart setup claude-code -rampart setup cline" { +hooks: "rampart setup claude-code\\nrampart setup cline" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 6 } -shim: "rampart setup openclaw -native plugin on current builds" { +shim: "rampart setup openclaw\\nnative plugin on current builds" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 6 } mcp: "rampart mcp --" { @@ -75,25 +73,18 @@ wrap: "rampart wrap --" { preload: "rampart preload --" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 6 } -api: "HTTP API / SDK -localhost:9090" { +api: "HTTP API / SDK\\nlocalhost:9090" { style.fill: "#1d3320"; style.stroke: "#2ea043"; style.font-color: "#3fb950"; style.border-radius: 6 } start -> q -q -> hooks: "Claude Code or Cline -(native hooks, lowest overhead)" -q -> shim: "OpenClaw -(native plugin on supported builds)" -q -> mcp: "Cursor, Claude Desktop -or any MCP-compatible client" -q -> wrap: "Any CLI agent -with \$SHELL support" -q -> preload: "Any CLI agent -without \$SHELL or native hooks" -q -> api: "Custom / Python agent -or CI pipeline" +q -> hooks: "Claude Code or Cline\\n(native hooks, lowest overhead)" +q -> shim: "OpenClaw\\n(native plugin on supported builds)" +q -> mcp: "Cursor, Claude Desktop\\nor any MCP-compatible client" +q -> wrap: "Any CLI agent\\nwith \$SHELL support" +q -> preload: "Any CLI agent\\nwithout \$SHELL or native hooks" +q -> api: "Custom / Python agent\\nor CI pipeline" ``` !!! tip "Start with the simplest method" diff --git a/docs-site/integrations/openclaw.md b/docs-site/integrations/openclaw.md index e0c0695e..0a66ab26 100644 --- a/docs-site/integrations/openclaw.md +++ b/docs-site/integrations/openclaw.md @@ -12,11 +12,11 @@ When `rampart serve` is healthy, every supported tool call — exec, read, write For sensitive tools, the recommended operating assumption is simple: if Rampart policy service is unavailable, treat that as a broken state and fix it before trusting approval-path tests. By default the plugin blocks sensitive tools such as `exec` and `write` when `rampart serve` is unavailable; lower-risk tools (`read`, `web_fetch`, `web_search`, `image`) are explicitly configured fail-open and can be tightened with `plugins.entries.rampart.config.failOpenTools`. !!! info "Version requirements" - - **OpenClaw >= 2026.5.2**: Preferred RC baseline. Supports explicit plugin startup activation plus first-class plugin approvals on the shared `/approve` / native approval path. - - **OpenClaw 2026.4.29 - 2026.5.1**: Supported for native plugin startup/interception; plugin approval delivery was not the RC baseline. + - **OpenClaw >= 2026.5.2**: Recommended 1.0 path. Supports explicit plugin startup activation plus first-class plugin approvals on the shared `/approve` / native approval path. + - **OpenClaw 2026.4.29 - 2026.5.1**: Supported for native plugin startup/interception; plugin approval delivery was not the launch baseline. - **OpenClaw 2026.3.28 - 2026.4.28**: Native plugin works for tool enforcement, but Rampart's polished approval path is supported on newer OpenClaw builds. - **OpenClaw < 2026.3.28**: Legacy shim + bridge — exec-only coverage, requires re-patching after upgrades. - - **Verified RC baseline on**: OpenClaw 2026.5.2 + - **Verified 1.0 launch dogfood on**: OpenClaw 2026.5.6 `rampart setup openclaw` auto-detects your version and uses the right method. @@ -157,7 +157,7 @@ Or check plugin status directly: ```bash openclaw plugins list -# rampart v0.9.18 active +# rampart v1.0.0 active ``` ## Troubleshooting diff --git a/docs-site/reference/architecture.md b/docs-site/reference/architecture.md index 855586eb..f268235e 100644 --- a/docs-site/reference/architecture.md +++ b/docs-site/reference/architecture.md @@ -29,19 +29,19 @@ intercept: { mcp: "MCP Proxy" } -engine: "YAML Policy Engine\n<10μs" { +engine: "YAML Policy Engine\\n<10μs" { style.fill: "#1d3320" style.stroke: "#2ea043" style.font-color: "#3fb950" style.border-radius: 8 } -verify: "rampart-verify\n(optional sidecar)" { +verify: "rampart-verify\\n(optional sidecar)" { style.stroke-dash: 4 style.border-radius: 8 } -audit: "Audit Trail\nhash-chained" { +audit: "Audit Trail\\nhash-chained" { style.border-radius: 8 } diff --git a/docs-site/reference/owasp-mapping.md b/docs-site/reference/owasp-mapping.md index 27eb905b..dc1bef2b 100644 --- a/docs-site/reference/owasp-mapping.md +++ b/docs-site/reference/owasp-mapping.md @@ -13,7 +13,7 @@ This page maps Rampart's capabilities to the [OWASP Top 10 for Agentic Applicati | ASI02 | **Tool Misuse and Exploitation** | Every tool call (exec, read, write, fetch, MCP) is evaluated against YAML policies before execution. `default_action: deny` enforces least-privilege. Parameter validation, command pattern matching, and approval workflows for sensitive operations. This is Rampart's core function. | ✅ Covered | | ASI03 | **Identity and Privilege Abuse** | `agent_depth` conditions limit sub-agent privilege escalation. Self-modification protection blocks agents from running `rampart allow`/`rampart block`. User separation prevents agents from accessing policies/audit. Does not manage agent credentials, OAuth tokens, or delegated permissions — over-scoped keys are outside Rampart's scope. | ⚠️ Partial | | ASI04 | **Agentic Supply Chain Vulnerabilities** | Community policy SHA-256 verification detects tampering after registry publication. `rampart mcp scan` auto-generates policy from MCP server tool definitions. Project-local policies enforce deny-wins (a project policy can tighten but not loosen global policy). Does not verify tool provenance at source, inspect dependency trees, or provide SBOM/AIBOM. | ⚠️ Partial | -| ASI05 | **Unexpected Code Execution (RCE)** | Shell command normalization, interpreter one-liner blocking (`python3 -c`, `node -e`, `perl -e`), LD_PRELOAD cascade for subprocess interception, and pattern matching catch common injected code patterns before they run. Does not inspect code executed inside allowed interpreters (e.g., `python3 script.py`), cannot handle all obfuscation variants, and LD_PRELOAD cascade does not apply in native-hook mode (Claude Code, Cline). See [Threat Model — Known Gaps](../threat-model/). | ⚠️ Partial | +| ASI05 | **Unexpected Code Execution (RCE)** | Shell command normalization, interpreter one-liner blocking (`python3 -c`, `node -e`, `perl -e`), LD_PRELOAD cascade for subprocess interception, and pattern matching catch common injected code patterns before they run. Does not inspect code executed inside allowed interpreters (e.g., `python3 script.py`), cannot handle all obfuscation variants, and LD_PRELOAD cascade does not apply in native-hook mode (Claude Code, Cline). See [Threat Model — Known Gaps](threat-model.md). | ⚠️ Partial | | ASI06 | **Memory & Context Poisoning** | Response scanning (`response_matches`) blocks credentials and known-bad patterns in tool responses before they enter the agent's context window. Does not protect persistent memory stores, RAG databases, embeddings, or conversation history — Rampart operates at the tool call layer, not the memory layer. | ⚠️ Partial | | ASI07 | **Insecure Inter-Agent Communication** | Not addressed. Rampart operates at the agent-to-OS boundary; it has no visibility into messages passed between agents in a multi-agent system. Does not provide mutual authentication, message signing, anti-replay, or encryption for agent-to-agent channels. Note: *tool calls* from sub-agents are evaluated by the same policy engine, and `agent_depth` conditions limit sub-agent nesting depth — but these address sub-agent containment, not communication security. | ❌ Not covered | | ASI08 | **Cascading Failures** | Fail-open design prevents Rampart from becoming a single point of failure (a crashed Rampart doesn't lock out the system). `call_count` rate limiting throttles runaway agents. Webhook notifications alert on anomalies in real time. Does not prevent agent-to-agent cascade in multi-agent systems. | ⚠️ Partial | diff --git a/docs-site/reference/threat-model.md b/docs-site/reference/threat-model.md index e298ef9b..d58bebfd 100644 --- a/docs-site/reference/threat-model.md +++ b/docs-site/reference/threat-model.md @@ -1,6 +1,6 @@ # Threat Model -> Last reviewed: 2026-04-30 | Applies to: v1.0.0-rc.2+ +> Last reviewed: 2026-04-30 | Applies to: v1.0.0 Rampart is a policy engine for AI agents — not a sandbox, not a hypervisor, not a full isolation boundary. This document describes what Rampart protects against, what it doesn't, and why. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index bb4835c4..cfdaeaff 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -45,15 +45,15 @@ What's coming next for Rampart. Priorities shift based on feedback — [open an ## Current Focus -### `v1.0.0-rc.2` +### `v1.0.0` - Keep the integration support story boring and evidence-backed: hooks, plugins, preload/wrapper, MCP, and HTTP API should each say what is protected and what happens when policy evaluation is unavailable. -- Treat OpenClaw `2026.5.2+` as the recommended RC baseline for native plugin approvals. -- Keep `rampart doctor`, setup output, and docs aligned so users can answer "am I protected, how, and what breaks if serve is down?" without reading source. +- Treat OpenClaw `2026.5.2+` as the recommended 1.0 path for native plugin approvals, with launch dogfood verified on OpenClaw `2026.5.6`. +- Keep `rampart doctor`, setup output, plugin metadata, and docs aligned so users can answer "am I protected, how, and what breaks if serve is down?" without reading source. -### After the RC +### After 1.0 - Collect feedback from real OpenClaw, Claude Code, Cline, Codex, and MCP users. -- Fix any RC integration bugs without widening the public API unless the tradeoff is explicit. -- Promote to `v1.0.0` only when the support matrix and threat model still match real-world behavior. +- Fix integration bugs without widening the public API unless the tradeoff is explicit. +- Keep the support matrix and threat model honest as agent runtimes evolve. ## Future / v2.x diff --git a/docs/index.html b/docs/index.html index 8f37f3be..af37ba06 100644 --- a/docs/index.html +++ b/docs/index.html @@ -27,7 +27,7 @@ "url": "https://rampart.sh", "applicationCategory": "SecurityApplication", "operatingSystem": "Linux, macOS, Windows", - "softwareVersion": "1.0.0-rc.2", + "softwareVersion": "1.0.0", "offers": { "@type": "Offer", "price": "0", @@ -1112,7 +1112,7 @@
-
v1.0.0-rc.2 · release candidate
+
v1.0.0 · launch-ready

Your agent has root.
That’s the problem.

Rampart sits in the execution path. Install it, run rampart quickstart, and it wires the right protection path for Claude Code, Codex, Cline, or OpenClaw before risky tool calls run.

diff --git a/docs/install b/docs/install index c8dd02bc..86169596 100644 --- a/docs/install +++ b/docs/install @@ -1,151 +1,263 @@ #!/bin/sh -# Rampart installer -# Usage: curl -fsSL https://rampart.sh/install | bash -# curl -fsSL https://rampart.sh/install | RAMPART_VERSION=v0.3.0 bash +# Rampart install script. # -# Env vars: -# RAMPART_VERSION — pin a version (default: latest) -# RAMPART_INSTALL_DIR — install directory (default: ~/.local/bin) -# RAMPART_INSTALL_DRY_RUN — set to 1 to print actions without running them +# Canonical source for the website and legacy installer copies. Keep +# docs/install, docs/install.sh, and scripts/install.sh byte-for-byte synced +# with this file. +# Usage: curl -fsSL https://rampart.sh/install | sh +# curl -fsSL https://rampart.sh/install | sh -s -- --version v0.1.0 +# curl -fsSL https://rampart.sh/install | sh -s -- --auto-setup +# RAMPART_INSTALL_DRY_RUN=1 sh install.sh --version v1.0.0 +# RAMPART_VERSION=v1.0.0 RAMPART_INSTALL_DIR=$HOME/.local/bin sh install.sh set -e REPO="peg/rampart" -INSTALL_DIR="${RAMPART_INSTALL_DIR:-$HOME/.local/bin}" +INSTALL_DIR="${RAMPART_INSTALL_DIR:-}" +BINARY="rampart" VERSION="${RAMPART_VERSION:-}" +AUTO_SETUP="${RAMPART_AUTO_SETUP:-0}" DRY_RUN="${RAMPART_INSTALL_DRY_RUN:-0}" -# ── Colors ──────────────────────────────────────────────────────────────────── +# Colors (if terminal supports them). if [ -t 1 ]; then - BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m" + BOLD="\033[1m" + GREEN="\033[32m" + RED="\033[31m" + YELLOW="\033[33m" + RESET="\033[0m" else - BOLD=""; GREEN=""; YELLOW=""; RED=""; RESET="" + BOLD="" GREEN="" RED="" YELLOW="" RESET="" fi info() { printf "${GREEN}▸${RESET} %s\n" "$1"; } -warn() { printf "${YELLOW}▸${RESET} %s\n" "$1" >&2; } +warn() { printf "${YELLOW}▸${RESET} %s\n" "$1"; } error() { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; } -step() { printf "\n${BOLD}%s${RESET}\n" "$1"; } -dry() { printf "${YELLOW}[dry-run]${RESET} %s\n" "$1"; } -# ── Downloader ───────────────────────────────────────────────────────────────── -fetch() { # fetch [dest] - URL="$1"; DEST="$2" +fetch() { # fetch [dest] + URL="$1" + DEST="${2:-}" if command -v curl >/dev/null 2>&1; then - if [ -n "$DEST" ]; then curl -fsSL -o "$DEST" "$URL" - else curl -fsSL "$URL"; fi + if [ -n "$DEST" ]; then + curl -fsSL -o "$DEST" "$URL" + else + curl -fsSL "$URL" + fi elif command -v wget >/dev/null 2>&1; then - if [ -n "$DEST" ]; then wget -qO "$DEST" "$URL" - else wget -qO- "$URL"; fi + if [ -n "$DEST" ]; then + wget -qO "$DEST" "$URL" + else + wget -qO- "$URL" + fi else error "Neither curl nor wget found. Install one and retry." fi } -# ── OS / Arch ────────────────────────────────────────────────────────────────── +# Parse args. +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --version=*) VERSION="${1#--version=}"; shift ;; + --auto-setup) AUTO_SETUP=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + *) error "Unknown option: $1" ;; + esac +done + +# Detect OS. OS="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$OS" in - linux) OS="linux" ;; + linux) OS="linux" ;; darwin) OS="darwin" ;; - *) error "Unsupported OS: $(uname -s). Only linux and darwin are supported." ;; + *) error "Unsupported OS: $OS (need linux or darwin)" ;; esac +# Detect architecture. ARCH="$(uname -m)" case "$ARCH" in - x86_64|amd64) ARCH="amd64" ;; - aarch64|arm64) ARCH="arm64" ;; - *) error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported." ;; + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH (need amd64 or arm64)" ;; esac -info "Platform: ${BOLD}${OS}/${ARCH}${RESET}" +info "Detected ${BOLD}${OS}/${ARCH}${RESET}" -# ── Resolve version ──────────────────────────────────────────────────────────── +# Determine version. if [ -z "$VERSION" ]; then - info "Fetching latest release..." - VERSION="$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" - [ -n "$VERSION" ] || error "Could not determine latest version. Set RAMPART_VERSION=vX.Y.Z and retry." + info "Fetching latest version..." + VERSION=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + if [ -z "$VERSION" ]; then + error "Could not determine latest version. Try: --version v0.1.0" + fi fi -# Normalise: ensure leading 'v' +# Normalise version: release tags include a leading "v". case "$VERSION" in v*) ;; *) VERSION="v${VERSION}" ;; esac -info "Version: ${BOLD}${VERSION}${RESET}" +info "Installing ${BOLD}rampart ${VERSION}${RESET}" -# ── Build URLs ───────────────────────────────────────────────────────────────── -TARBALL="rampart_${VERSION#v}_${OS}_${ARCH}.tar.gz" +if [ -z "$INSTALL_DIR" ]; then + if [ "$(id -u)" -eq 0 ]; then + INSTALL_DIR="/usr/local/bin" + elif [ -d "$HOME/.local/bin" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif [ "$DRY_RUN" = "1" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif mkdir -p "$HOME/.local/bin" 2>/dev/null; then + INSTALL_DIR="$HOME/.local/bin" + else + INSTALL_DIR="/usr/local/bin" + fi +fi + +# Build download URLs. BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" +TARBALL="rampart_${VERSION#v}_${OS}_${ARCH}.tar.gz" TARBALL_URL="${BASE_URL}/${TARBALL}" CHECKSUM_URL="${BASE_URL}/checksums.txt" if [ "$DRY_RUN" = "1" ]; then - step "Dry-run — no changes will be made" - dry "Would download: ${TARBALL_URL}" - dry "Would install: ${INSTALL_DIR}/rampart" - dry "Would run: rampart quickstart" - if ! echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then - dry "Would hint to add ${INSTALL_DIR} to PATH" - fi + info "Dry-run — no changes will be made" + info "Would download: ${TARBALL_URL}" + info "Would verify: ${CHECKSUM_URL}" + info "Would install: ${INSTALL_DIR}/${BINARY}" exit 0 fi -# ── Download & extract ───────────────────────────────────────────────────────── -step "Downloading rampart ${VERSION}..." - +# Create temp directory. TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT -fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}" \ - || error "Download failed.\nURL: ${TARBALL_URL}\nCheck that ${VERSION} exists: https://github.com/${REPO}/releases" +# Download archive. +info "Downloading archive..." +if ! fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}"; then + error "Download failed. Check that ${VERSION} exists at:\n ${TARBALL_URL}" +fi -# Optional checksum verification +# Download and verify checksum. +info "Verifying checksum..." if fetch "$CHECKSUM_URL" "${TMP_DIR}/checksums.txt" 2>/dev/null; then + EXPECTED=$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}') if command -v sha256sum >/dev/null 2>&1; then - HASH_CMD="sha256sum" + ACTUAL=$(sha256sum "${TMP_DIR}/${TARBALL}" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then - HASH_CMD="shasum -a 256" + ACTUAL=$(shasum -a 256 "${TMP_DIR}/${TARBALL}" | awk '{print $1}') else - HASH_CMD="" + warn "No sha256sum or shasum found — skipping verification" + ACTUAL="$EXPECTED" fi - if [ -n "$HASH_CMD" ]; then - EXPECTED="$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}')" - ACTUAL="$($HASH_CMD "${TMP_DIR}/${TARBALL}" | awk '{print $1}')" - if [ -n "$EXPECTED" ] && [ "$EXPECTED" != "$ACTUAL" ]; then - error "Checksum mismatch!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}" - fi + + if [ -z "$EXPECTED" ]; then + warn "No checksum entry found for ${TARBALL} — skipping verification" + elif [ "$EXPECTED" != "$ACTUAL" ]; then + error "Checksum mismatch!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}" + else info "Checksum verified ✓" fi +else + warn "No checksums.txt found — skipping verification" fi tar -xzf "${TMP_DIR}/${TARBALL}" -C "$TMP_DIR" -# ── Install ──────────────────────────────────────────────────────────────────── -step "Installing to ${INSTALL_DIR}..." +if [ ! -f "${TMP_DIR}/${BINARY}" ]; then + error "Archive did not contain ${BINARY}" +fi -mkdir -p "$INSTALL_DIR" -mv "${TMP_DIR}/rampart" "${INSTALL_DIR}/rampart" -chmod +x "${INSTALL_DIR}/rampart" +# Install. +chmod +x "${TMP_DIR}/${BINARY}" -info "Installed: ${BOLD}${INSTALL_DIR}/rampart${RESET}" +if [ ! -d "$INSTALL_DIR" ]; then + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + info "Need sudo to create ${INSTALL_DIR}" + sudo mkdir -p "$INSTALL_DIR" + fi +fi -# PATH hint -if ! echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then - warn "${INSTALL_DIR} is not in your PATH." - warn "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):" - printf " ${BOLD}export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}\n" >&2 - export PATH="${INSTALL_DIR}:${PATH}" +if [ -w "$INSTALL_DIR" ]; then + mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" +else + info "Need sudo to install to ${INSTALL_DIR}" + sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" fi -# ── Quickstart ───────────────────────────────────────────────────────────────── -step "Running quickstart..." +info "Installed to ${BOLD}${INSTALL_DIR}/${BINARY}${RESET}" -if [ -t 0 ] && [ -t 1 ]; then - rampart quickstart +# Verify. +RAMPART_BIN="${INSTALL_DIR}/${BINARY}" +if [ -x "$RAMPART_BIN" ]; then + printf "\n" + "$RAMPART_BIN" version 2>/dev/null || true + + if echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then + printf "\n${GREEN}${BOLD}Ready!${RESET} Run ${BOLD}rampart quickstart${RESET} to get started.\n" + else + printf "\n${YELLOW}Note:${RESET} ${INSTALL_DIR} may not be in your PATH.\n" + printf "Add it: ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}\n" + printf "Then run: ${BOLD}rampart quickstart${RESET}\n" + fi else - rampart quickstart --env none 2>/dev/null || true + warn "Could not verify installed binary at ${INSTALL_DIR}/${BINARY}" fi -printf "\n${GREEN}${BOLD}Done!${RESET} rampart ${VERSION} is installed.\n" -printf "Docs: ${BOLD}https://docs.rampart.sh${RESET}\n\n" +# Detect AI agents and suggest setup commands. +detect_agents_and_suggest() { + printf "\n" + + # Detect OpenClaw + OPENCLAW_FOUND=0 + if command -v openclaw >/dev/null 2>&1; then + OPENCLAW_FOUND=1 + elif [ -f "$HOME/.local/bin/openclaw" ] || [ -f "/usr/local/bin/openclaw" ] || [ -f "/usr/bin/openclaw" ]; then + OPENCLAW_FOUND=1 + fi + + # Detect Claude Code (claude CLI) + CLAUDE_FOUND=0 + if command -v claude >/dev/null 2>&1; then + CLAUDE_FOUND=1 + elif [ -f "$HOME/.claude/settings.json" ]; then + CLAUDE_FOUND=1 + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ] || [ "$CLAUDE_FOUND" -eq 1 ]; then + printf "${GREEN}${BOLD}✓ AI agent(s) detected!${RESET}\n\n" + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting OpenClaw...\n" + "$RAMPART_BIN" setup openclaw 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup openclaw${RESET}\n" + else + printf " Run this to protect your OpenClaw agent:\n" + printf " ${BOLD}rampart setup openclaw${RESET}\n" + printf "\n" + fi + fi + + if [ "$CLAUDE_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting Claude Code...\n" + "$RAMPART_BIN" setup claude-code 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup claude-code${RESET}\n" + else + printf " Run this to protect Claude Code:\n" + printf " ${BOLD}rampart setup claude-code${RESET}\n" + printf "\n" + fi + fi + + if [ "$OPENCLAW_FOUND" -eq 0 ] && [ "$CLAUDE_FOUND" -eq 0 ]; then + printf " To protect an AI agent, run:\n" + printf " ${BOLD}rampart setup openclaw${RESET} — for OpenClaw\n" + printf " ${BOLD}rampart setup claude-code${RESET} — for Claude Code\n" + printf "\n" + fi +} + +if [ -x "$RAMPART_BIN" ]; then + detect_agents_and_suggest +fi diff --git a/docs/install.sh b/docs/install.sh new file mode 100755 index 00000000..86169596 --- /dev/null +++ b/docs/install.sh @@ -0,0 +1,263 @@ +#!/bin/sh +# Rampart install script. +# +# Canonical source for the website and legacy installer copies. Keep +# docs/install, docs/install.sh, and scripts/install.sh byte-for-byte synced +# with this file. +# Usage: curl -fsSL https://rampart.sh/install | sh +# curl -fsSL https://rampart.sh/install | sh -s -- --version v0.1.0 +# curl -fsSL https://rampart.sh/install | sh -s -- --auto-setup +# RAMPART_INSTALL_DRY_RUN=1 sh install.sh --version v1.0.0 +# RAMPART_VERSION=v1.0.0 RAMPART_INSTALL_DIR=$HOME/.local/bin sh install.sh +set -e + +REPO="peg/rampart" +INSTALL_DIR="${RAMPART_INSTALL_DIR:-}" +BINARY="rampart" +VERSION="${RAMPART_VERSION:-}" +AUTO_SETUP="${RAMPART_AUTO_SETUP:-0}" +DRY_RUN="${RAMPART_INSTALL_DRY_RUN:-0}" + +# Colors (if terminal supports them). +if [ -t 1 ]; then + BOLD="\033[1m" + GREEN="\033[32m" + RED="\033[31m" + YELLOW="\033[33m" + RESET="\033[0m" +else + BOLD="" GREEN="" RED="" YELLOW="" RESET="" +fi + +info() { printf "${GREEN}▸${RESET} %s\n" "$1"; } +warn() { printf "${YELLOW}▸${RESET} %s\n" "$1"; } +error() { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; } + +fetch() { # fetch [dest] + URL="$1" + DEST="${2:-}" + if command -v curl >/dev/null 2>&1; then + if [ -n "$DEST" ]; then + curl -fsSL -o "$DEST" "$URL" + else + curl -fsSL "$URL" + fi + elif command -v wget >/dev/null 2>&1; then + if [ -n "$DEST" ]; then + wget -qO "$DEST" "$URL" + else + wget -qO- "$URL" + fi + else + error "Neither curl nor wget found. Install one and retry." + fi +} + +# Parse args. +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --version=*) VERSION="${1#--version=}"; shift ;; + --auto-setup) AUTO_SETUP=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + *) error "Unknown option: $1" ;; + esac +done + +# Detect OS. +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +case "$OS" in + linux) OS="linux" ;; + darwin) OS="darwin" ;; + *) error "Unsupported OS: $OS (need linux or darwin)" ;; +esac + +# Detect architecture. +ARCH="$(uname -m)" +case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH (need amd64 or arm64)" ;; +esac + +info "Detected ${BOLD}${OS}/${ARCH}${RESET}" + +# Determine version. +if [ -z "$VERSION" ]; then + info "Fetching latest version..." + VERSION=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + if [ -z "$VERSION" ]; then + error "Could not determine latest version. Try: --version v0.1.0" + fi +fi + +# Normalise version: release tags include a leading "v". +case "$VERSION" in + v*) ;; + *) VERSION="v${VERSION}" ;; +esac + +info "Installing ${BOLD}rampart ${VERSION}${RESET}" + +if [ -z "$INSTALL_DIR" ]; then + if [ "$(id -u)" -eq 0 ]; then + INSTALL_DIR="/usr/local/bin" + elif [ -d "$HOME/.local/bin" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif [ "$DRY_RUN" = "1" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif mkdir -p "$HOME/.local/bin" 2>/dev/null; then + INSTALL_DIR="$HOME/.local/bin" + else + INSTALL_DIR="/usr/local/bin" + fi +fi + +# Build download URLs. +BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" +TARBALL="rampart_${VERSION#v}_${OS}_${ARCH}.tar.gz" +TARBALL_URL="${BASE_URL}/${TARBALL}" +CHECKSUM_URL="${BASE_URL}/checksums.txt" + +if [ "$DRY_RUN" = "1" ]; then + info "Dry-run — no changes will be made" + info "Would download: ${TARBALL_URL}" + info "Would verify: ${CHECKSUM_URL}" + info "Would install: ${INSTALL_DIR}/${BINARY}" + exit 0 +fi + +# Create temp directory. +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +# Download archive. +info "Downloading archive..." +if ! fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}"; then + error "Download failed. Check that ${VERSION} exists at:\n ${TARBALL_URL}" +fi + +# Download and verify checksum. +info "Verifying checksum..." +if fetch "$CHECKSUM_URL" "${TMP_DIR}/checksums.txt" 2>/dev/null; then + EXPECTED=$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}') + if command -v sha256sum >/dev/null 2>&1; then + ACTUAL=$(sha256sum "${TMP_DIR}/${TARBALL}" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + ACTUAL=$(shasum -a 256 "${TMP_DIR}/${TARBALL}" | awk '{print $1}') + else + warn "No sha256sum or shasum found — skipping verification" + ACTUAL="$EXPECTED" + fi + + if [ -z "$EXPECTED" ]; then + warn "No checksum entry found for ${TARBALL} — skipping verification" + elif [ "$EXPECTED" != "$ACTUAL" ]; then + error "Checksum mismatch!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}" + else + info "Checksum verified ✓" + fi +else + warn "No checksums.txt found — skipping verification" +fi + +tar -xzf "${TMP_DIR}/${TARBALL}" -C "$TMP_DIR" + +if [ ! -f "${TMP_DIR}/${BINARY}" ]; then + error "Archive did not contain ${BINARY}" +fi + +# Install. +chmod +x "${TMP_DIR}/${BINARY}" + +if [ ! -d "$INSTALL_DIR" ]; then + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + info "Need sudo to create ${INSTALL_DIR}" + sudo mkdir -p "$INSTALL_DIR" + fi +fi + +if [ -w "$INSTALL_DIR" ]; then + mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" +else + info "Need sudo to install to ${INSTALL_DIR}" + sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" +fi + +info "Installed to ${BOLD}${INSTALL_DIR}/${BINARY}${RESET}" + +# Verify. +RAMPART_BIN="${INSTALL_DIR}/${BINARY}" +if [ -x "$RAMPART_BIN" ]; then + printf "\n" + "$RAMPART_BIN" version 2>/dev/null || true + + if echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then + printf "\n${GREEN}${BOLD}Ready!${RESET} Run ${BOLD}rampart quickstart${RESET} to get started.\n" + else + printf "\n${YELLOW}Note:${RESET} ${INSTALL_DIR} may not be in your PATH.\n" + printf "Add it: ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}\n" + printf "Then run: ${BOLD}rampart quickstart${RESET}\n" + fi +else + warn "Could not verify installed binary at ${INSTALL_DIR}/${BINARY}" +fi + +# Detect AI agents and suggest setup commands. +detect_agents_and_suggest() { + printf "\n" + + # Detect OpenClaw + OPENCLAW_FOUND=0 + if command -v openclaw >/dev/null 2>&1; then + OPENCLAW_FOUND=1 + elif [ -f "$HOME/.local/bin/openclaw" ] || [ -f "/usr/local/bin/openclaw" ] || [ -f "/usr/bin/openclaw" ]; then + OPENCLAW_FOUND=1 + fi + + # Detect Claude Code (claude CLI) + CLAUDE_FOUND=0 + if command -v claude >/dev/null 2>&1; then + CLAUDE_FOUND=1 + elif [ -f "$HOME/.claude/settings.json" ]; then + CLAUDE_FOUND=1 + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ] || [ "$CLAUDE_FOUND" -eq 1 ]; then + printf "${GREEN}${BOLD}✓ AI agent(s) detected!${RESET}\n\n" + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting OpenClaw...\n" + "$RAMPART_BIN" setup openclaw 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup openclaw${RESET}\n" + else + printf " Run this to protect your OpenClaw agent:\n" + printf " ${BOLD}rampart setup openclaw${RESET}\n" + printf "\n" + fi + fi + + if [ "$CLAUDE_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting Claude Code...\n" + "$RAMPART_BIN" setup claude-code 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup claude-code${RESET}\n" + else + printf " Run this to protect Claude Code:\n" + printf " ${BOLD}rampart setup claude-code${RESET}\n" + printf "\n" + fi + fi + + if [ "$OPENCLAW_FOUND" -eq 0 ] && [ "$CLAUDE_FOUND" -eq 0 ]; then + printf " To protect an AI agent, run:\n" + printf " ${BOLD}rampart setup openclaw${RESET} — for OpenClaw\n" + printf " ${BOLD}rampart setup claude-code${RESET} — for Claude Code\n" + printf "\n" + fi +} + +if [ -x "$RAMPART_BIN" ]; then + detect_agents_and_suggest +fi diff --git a/install.sh b/install.sh index 1e1fd9da..86169596 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,20 @@ #!/bin/sh -# Rampart install script +# Rampart install script. +# +# Canonical source for the website and legacy installer copies. Keep +# docs/install, docs/install.sh, and scripts/install.sh byte-for-byte synced +# with this file. # Usage: curl -fsSL https://rampart.sh/install | sh # curl -fsSL https://rampart.sh/install | sh -s -- --version v0.1.0 # curl -fsSL https://rampart.sh/install | sh -s -- --auto-setup -# RAMPART_INSTALL_DRY_RUN=1 sh install.sh --version v1.0.0-rc.2 +# RAMPART_INSTALL_DRY_RUN=1 sh install.sh --version v1.0.0 +# RAMPART_VERSION=v1.0.0 RAMPART_INSTALL_DIR=$HOME/.local/bin sh install.sh set -e REPO="peg/rampart" -INSTALL_DIR="/usr/local/bin" +INSTALL_DIR="${RAMPART_INSTALL_DIR:-}" BINARY="rampart" -VERSION="" +VERSION="${RAMPART_VERSION:-}" AUTO_SETUP="${RAMPART_AUTO_SETUP:-0}" DRY_RUN="${RAMPART_INSTALL_DRY_RUN:-0}" @@ -28,6 +33,26 @@ info() { printf "${GREEN}▸${RESET} %s\n" "$1"; } warn() { printf "${YELLOW}▸${RESET} %s\n" "$1"; } error() { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; } +fetch() { # fetch [dest] + URL="$1" + DEST="${2:-}" + if command -v curl >/dev/null 2>&1; then + if [ -n "$DEST" ]; then + curl -fsSL -o "$DEST" "$URL" + else + curl -fsSL "$URL" + fi + elif command -v wget >/dev/null 2>&1; then + if [ -n "$DEST" ]; then + wget -qO "$DEST" "$URL" + else + wget -qO- "$URL" + fi + else + error "Neither curl nor wget found. Install one and retry." + fi +} + # Parse args. while [ $# -gt 0 ]; do case "$1" in @@ -60,7 +85,7 @@ info "Detected ${BOLD}${OS}/${ARCH}${RESET}" # Determine version. if [ -z "$VERSION" ]; then info "Fetching latest version..." - VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + VERSION=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') if [ -z "$VERSION" ]; then error "Could not determine latest version. Try: --version v0.1.0" @@ -75,16 +100,18 @@ esac info "Installing ${BOLD}rampart ${VERSION}${RESET}" -if [ "$(id -u)" -eq 0 ]; then - INSTALL_DIR="/usr/local/bin" -elif [ -d "$HOME/.local/bin" ]; then - INSTALL_DIR="$HOME/.local/bin" -elif [ "$DRY_RUN" = "1" ]; then - INSTALL_DIR="$HOME/.local/bin" -elif mkdir -p "$HOME/.local/bin" 2>/dev/null; then - INSTALL_DIR="$HOME/.local/bin" -else - INSTALL_DIR="/usr/local/bin" +if [ -z "$INSTALL_DIR" ]; then + if [ "$(id -u)" -eq 0 ]; then + INSTALL_DIR="/usr/local/bin" + elif [ -d "$HOME/.local/bin" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif [ "$DRY_RUN" = "1" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif mkdir -p "$HOME/.local/bin" 2>/dev/null; then + INSTALL_DIR="$HOME/.local/bin" + else + INSTALL_DIR="/usr/local/bin" + fi fi # Build download URLs. @@ -107,13 +134,13 @@ trap 'rm -rf "$TMP_DIR"' EXIT # Download archive. info "Downloading archive..." -if ! curl -fsSL -o "${TMP_DIR}/${TARBALL}" "$TARBALL_URL"; then +if ! fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}"; then error "Download failed. Check that ${VERSION} exists at:\n ${TARBALL_URL}" fi # Download and verify checksum. info "Verifying checksum..." -if curl -fsSL -o "${TMP_DIR}/checksums.txt" "$CHECKSUM_URL" 2>/dev/null; then +if fetch "$CHECKSUM_URL" "${TMP_DIR}/checksums.txt" 2>/dev/null; then EXPECTED=$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}') if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "${TMP_DIR}/${TARBALL}" | awk '{print $1}') @@ -144,6 +171,13 @@ fi # Install. chmod +x "${TMP_DIR}/${BINARY}" +if [ ! -d "$INSTALL_DIR" ]; then + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + info "Need sudo to create ${INSTALL_DIR}" + sudo mkdir -p "$INSTALL_DIR" + fi +fi + if [ -w "$INSTALL_DIR" ]; then mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" else diff --git a/internal/plugin/openclaw/embed_test.go b/internal/plugin/openclaw/embed_test.go index b681f8d6..981a20ed 100644 --- a/internal/plugin/openclaw/embed_test.go +++ b/internal/plugin/openclaw/embed_test.go @@ -40,6 +40,7 @@ func TestPackageDeclaresInstallHostFloor(t *testing.T) { } var pkg struct { Version string `json:"version"` + License string `json:"license"` OpenClaw struct { Install struct { MinHostVersion string `json:"minHostVersion"` @@ -55,6 +56,9 @@ func TestPackageDeclaresInstallHostFloor(t *testing.T) { if got, want := pkg.Version, Version(); got != want { t.Fatalf("package.json version = %q, want Version() %q", got, want) } + if got, want := pkg.License, "Apache-2.0"; got != want { + t.Fatalf("package.json license = %q, want %q", got, want) + } } func TestManifestDeclaresDegradedModeConfig(t *testing.T) { diff --git a/internal/plugin/openclaw/index.js b/internal/plugin/openclaw/index.js index 3b92d31b..1d2a53d8 100644 --- a/internal/plugin/openclaw/index.js +++ b/internal/plugin/openclaw/index.js @@ -5,7 +5,7 @@ * Replaces brittle dist-file patching with the official OpenClaw plugin API. * * @see https://github.com/peg/rampart - * @version 0.1.0 + * @version 1.0.0 */ import { readFile } from "fs/promises"; @@ -212,7 +212,7 @@ async function auditLog(toolName, params, ctx, outcome, config) { export const id = "rampart"; export const name = "Rampart"; export const description = "AI agent firewall — YAML policy-as-code for every tool call"; -export const version = "1.0.0-rc.2"; +export const version = "1.0.0"; // OpenClaw runs higher-priority before_tool_call hooks first. Rampart should // act as the final normal plugin gate so it evaluates the params that will diff --git a/internal/plugin/openclaw/openclaw.plugin.json b/internal/plugin/openclaw/openclaw.plugin.json index b56b1b7b..2e92ae30 100644 --- a/internal/plugin/openclaw/openclaw.plugin.json +++ b/internal/plugin/openclaw/openclaw.plugin.json @@ -8,7 +8,7 @@ "hook" ] }, - "version": "1.0.0-rc.2", + "version": "1.0.0", "author": "peg", "homepage": "https://rampart.sh", "repository": "https://github.com/peg/rampart", diff --git a/internal/plugin/openclaw/package.json b/internal/plugin/openclaw/package.json index d6c22f61..5676b55b 100644 --- a/internal/plugin/openclaw/package.json +++ b/internal/plugin/openclaw/package.json @@ -1,6 +1,6 @@ { "name": "rampart", - "version": "1.0.0-rc.2", + "version": "1.0.0", "description": "Rampart AI agent firewall — OpenClaw native plugin (before_tool_call hook)", "type": "module", "main": "index.js", @@ -11,7 +11,7 @@ "security", "plugin" ], - "license": "MIT", + "license": "Apache-2.0", "openclaw": { "extensions": [ "./index.js" diff --git a/mkdocs.yml b/mkdocs.yml index c1db0cf3..86e37f02 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,12 @@ edit_uri: edit/main/docs-site/ docs_dir: docs-site +# docs-site/index.html is the standalone rampart.sh landing page source, not a +# MkDocs page. Exclude it explicitly so docs builds don't warn about the +# intentional index.md/index.html collision. +exclude_docs: | + index.html + theme: name: material custom_dir: docs-site/overrides diff --git a/scripts/install.sh b/scripts/install.sh index c8dd02bc..86169596 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,151 +1,263 @@ #!/bin/sh -# Rampart installer -# Usage: curl -fsSL https://rampart.sh/install | bash -# curl -fsSL https://rampart.sh/install | RAMPART_VERSION=v0.3.0 bash +# Rampart install script. # -# Env vars: -# RAMPART_VERSION — pin a version (default: latest) -# RAMPART_INSTALL_DIR — install directory (default: ~/.local/bin) -# RAMPART_INSTALL_DRY_RUN — set to 1 to print actions without running them +# Canonical source for the website and legacy installer copies. Keep +# docs/install, docs/install.sh, and scripts/install.sh byte-for-byte synced +# with this file. +# Usage: curl -fsSL https://rampart.sh/install | sh +# curl -fsSL https://rampart.sh/install | sh -s -- --version v0.1.0 +# curl -fsSL https://rampart.sh/install | sh -s -- --auto-setup +# RAMPART_INSTALL_DRY_RUN=1 sh install.sh --version v1.0.0 +# RAMPART_VERSION=v1.0.0 RAMPART_INSTALL_DIR=$HOME/.local/bin sh install.sh set -e REPO="peg/rampart" -INSTALL_DIR="${RAMPART_INSTALL_DIR:-$HOME/.local/bin}" +INSTALL_DIR="${RAMPART_INSTALL_DIR:-}" +BINARY="rampart" VERSION="${RAMPART_VERSION:-}" +AUTO_SETUP="${RAMPART_AUTO_SETUP:-0}" DRY_RUN="${RAMPART_INSTALL_DRY_RUN:-0}" -# ── Colors ──────────────────────────────────────────────────────────────────── +# Colors (if terminal supports them). if [ -t 1 ]; then - BOLD="\033[1m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m" + BOLD="\033[1m" + GREEN="\033[32m" + RED="\033[31m" + YELLOW="\033[33m" + RESET="\033[0m" else - BOLD=""; GREEN=""; YELLOW=""; RED=""; RESET="" + BOLD="" GREEN="" RED="" YELLOW="" RESET="" fi info() { printf "${GREEN}▸${RESET} %s\n" "$1"; } -warn() { printf "${YELLOW}▸${RESET} %s\n" "$1" >&2; } +warn() { printf "${YELLOW}▸${RESET} %s\n" "$1"; } error() { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; } -step() { printf "\n${BOLD}%s${RESET}\n" "$1"; } -dry() { printf "${YELLOW}[dry-run]${RESET} %s\n" "$1"; } -# ── Downloader ───────────────────────────────────────────────────────────────── -fetch() { # fetch [dest] - URL="$1"; DEST="$2" +fetch() { # fetch [dest] + URL="$1" + DEST="${2:-}" if command -v curl >/dev/null 2>&1; then - if [ -n "$DEST" ]; then curl -fsSL -o "$DEST" "$URL" - else curl -fsSL "$URL"; fi + if [ -n "$DEST" ]; then + curl -fsSL -o "$DEST" "$URL" + else + curl -fsSL "$URL" + fi elif command -v wget >/dev/null 2>&1; then - if [ -n "$DEST" ]; then wget -qO "$DEST" "$URL" - else wget -qO- "$URL"; fi + if [ -n "$DEST" ]; then + wget -qO "$DEST" "$URL" + else + wget -qO- "$URL" + fi else error "Neither curl nor wget found. Install one and retry." fi } -# ── OS / Arch ────────────────────────────────────────────────────────────────── +# Parse args. +while [ $# -gt 0 ]; do + case "$1" in + --version) VERSION="$2"; shift 2 ;; + --version=*) VERSION="${1#--version=}"; shift ;; + --auto-setup) AUTO_SETUP=1; shift ;; + --dry-run) DRY_RUN=1; shift ;; + *) error "Unknown option: $1" ;; + esac +done + +# Detect OS. OS="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$OS" in - linux) OS="linux" ;; + linux) OS="linux" ;; darwin) OS="darwin" ;; - *) error "Unsupported OS: $(uname -s). Only linux and darwin are supported." ;; + *) error "Unsupported OS: $OS (need linux or darwin)" ;; esac +# Detect architecture. ARCH="$(uname -m)" case "$ARCH" in - x86_64|amd64) ARCH="amd64" ;; - aarch64|arm64) ARCH="arm64" ;; - *) error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported." ;; + x86_64|amd64) ARCH="amd64" ;; + aarch64|arm64) ARCH="arm64" ;; + *) error "Unsupported architecture: $ARCH (need amd64 or arm64)" ;; esac -info "Platform: ${BOLD}${OS}/${ARCH}${RESET}" +info "Detected ${BOLD}${OS}/${ARCH}${RESET}" -# ── Resolve version ──────────────────────────────────────────────────────────── +# Determine version. if [ -z "$VERSION" ]; then - info "Fetching latest release..." - VERSION="$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')" - [ -n "$VERSION" ] || error "Could not determine latest version. Set RAMPART_VERSION=vX.Y.Z and retry." + info "Fetching latest version..." + VERSION=$(fetch "https://api.github.com/repos/${REPO}/releases/latest" \ + | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + if [ -z "$VERSION" ]; then + error "Could not determine latest version. Try: --version v0.1.0" + fi fi -# Normalise: ensure leading 'v' +# Normalise version: release tags include a leading "v". case "$VERSION" in v*) ;; *) VERSION="v${VERSION}" ;; esac -info "Version: ${BOLD}${VERSION}${RESET}" +info "Installing ${BOLD}rampart ${VERSION}${RESET}" -# ── Build URLs ───────────────────────────────────────────────────────────────── -TARBALL="rampart_${VERSION#v}_${OS}_${ARCH}.tar.gz" +if [ -z "$INSTALL_DIR" ]; then + if [ "$(id -u)" -eq 0 ]; then + INSTALL_DIR="/usr/local/bin" + elif [ -d "$HOME/.local/bin" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif [ "$DRY_RUN" = "1" ]; then + INSTALL_DIR="$HOME/.local/bin" + elif mkdir -p "$HOME/.local/bin" 2>/dev/null; then + INSTALL_DIR="$HOME/.local/bin" + else + INSTALL_DIR="/usr/local/bin" + fi +fi + +# Build download URLs. BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" +TARBALL="rampart_${VERSION#v}_${OS}_${ARCH}.tar.gz" TARBALL_URL="${BASE_URL}/${TARBALL}" CHECKSUM_URL="${BASE_URL}/checksums.txt" if [ "$DRY_RUN" = "1" ]; then - step "Dry-run — no changes will be made" - dry "Would download: ${TARBALL_URL}" - dry "Would install: ${INSTALL_DIR}/rampart" - dry "Would run: rampart quickstart" - if ! echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then - dry "Would hint to add ${INSTALL_DIR} to PATH" - fi + info "Dry-run — no changes will be made" + info "Would download: ${TARBALL_URL}" + info "Would verify: ${CHECKSUM_URL}" + info "Would install: ${INSTALL_DIR}/${BINARY}" exit 0 fi -# ── Download & extract ───────────────────────────────────────────────────────── -step "Downloading rampart ${VERSION}..." - +# Create temp directory. TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT -fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}" \ - || error "Download failed.\nURL: ${TARBALL_URL}\nCheck that ${VERSION} exists: https://github.com/${REPO}/releases" +# Download archive. +info "Downloading archive..." +if ! fetch "$TARBALL_URL" "${TMP_DIR}/${TARBALL}"; then + error "Download failed. Check that ${VERSION} exists at:\n ${TARBALL_URL}" +fi -# Optional checksum verification +# Download and verify checksum. +info "Verifying checksum..." if fetch "$CHECKSUM_URL" "${TMP_DIR}/checksums.txt" 2>/dev/null; then + EXPECTED=$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}') if command -v sha256sum >/dev/null 2>&1; then - HASH_CMD="sha256sum" + ACTUAL=$(sha256sum "${TMP_DIR}/${TARBALL}" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then - HASH_CMD="shasum -a 256" + ACTUAL=$(shasum -a 256 "${TMP_DIR}/${TARBALL}" | awk '{print $1}') else - HASH_CMD="" + warn "No sha256sum or shasum found — skipping verification" + ACTUAL="$EXPECTED" fi - if [ -n "$HASH_CMD" ]; then - EXPECTED="$(grep "${TARBALL}" "${TMP_DIR}/checksums.txt" | awk '{print $1}')" - ACTUAL="$($HASH_CMD "${TMP_DIR}/${TARBALL}" | awk '{print $1}')" - if [ -n "$EXPECTED" ] && [ "$EXPECTED" != "$ACTUAL" ]; then - error "Checksum mismatch!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}" - fi + + if [ -z "$EXPECTED" ]; then + warn "No checksum entry found for ${TARBALL} — skipping verification" + elif [ "$EXPECTED" != "$ACTUAL" ]; then + error "Checksum mismatch!\n Expected: ${EXPECTED}\n Got: ${ACTUAL}" + else info "Checksum verified ✓" fi +else + warn "No checksums.txt found — skipping verification" fi tar -xzf "${TMP_DIR}/${TARBALL}" -C "$TMP_DIR" -# ── Install ──────────────────────────────────────────────────────────────────── -step "Installing to ${INSTALL_DIR}..." +if [ ! -f "${TMP_DIR}/${BINARY}" ]; then + error "Archive did not contain ${BINARY}" +fi -mkdir -p "$INSTALL_DIR" -mv "${TMP_DIR}/rampart" "${INSTALL_DIR}/rampart" -chmod +x "${INSTALL_DIR}/rampart" +# Install. +chmod +x "${TMP_DIR}/${BINARY}" -info "Installed: ${BOLD}${INSTALL_DIR}/rampart${RESET}" +if [ ! -d "$INSTALL_DIR" ]; then + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + info "Need sudo to create ${INSTALL_DIR}" + sudo mkdir -p "$INSTALL_DIR" + fi +fi -# PATH hint -if ! echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then - warn "${INSTALL_DIR} is not in your PATH." - warn "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):" - printf " ${BOLD}export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}\n" >&2 - export PATH="${INSTALL_DIR}:${PATH}" +if [ -w "$INSTALL_DIR" ]; then + mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" +else + info "Need sudo to install to ${INSTALL_DIR}" + sudo mv "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/${BINARY}" fi -# ── Quickstart ───────────────────────────────────────────────────────────────── -step "Running quickstart..." +info "Installed to ${BOLD}${INSTALL_DIR}/${BINARY}${RESET}" -if [ -t 0 ] && [ -t 1 ]; then - rampart quickstart +# Verify. +RAMPART_BIN="${INSTALL_DIR}/${BINARY}" +if [ -x "$RAMPART_BIN" ]; then + printf "\n" + "$RAMPART_BIN" version 2>/dev/null || true + + if echo ":${PATH}:" | grep -q ":${INSTALL_DIR}:"; then + printf "\n${GREEN}${BOLD}Ready!${RESET} Run ${BOLD}rampart quickstart${RESET} to get started.\n" + else + printf "\n${YELLOW}Note:${RESET} ${INSTALL_DIR} may not be in your PATH.\n" + printf "Add it: ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}\n" + printf "Then run: ${BOLD}rampart quickstart${RESET}\n" + fi else - rampart quickstart --env none 2>/dev/null || true + warn "Could not verify installed binary at ${INSTALL_DIR}/${BINARY}" fi -printf "\n${GREEN}${BOLD}Done!${RESET} rampart ${VERSION} is installed.\n" -printf "Docs: ${BOLD}https://docs.rampart.sh${RESET}\n\n" +# Detect AI agents and suggest setup commands. +detect_agents_and_suggest() { + printf "\n" + + # Detect OpenClaw + OPENCLAW_FOUND=0 + if command -v openclaw >/dev/null 2>&1; then + OPENCLAW_FOUND=1 + elif [ -f "$HOME/.local/bin/openclaw" ] || [ -f "/usr/local/bin/openclaw" ] || [ -f "/usr/bin/openclaw" ]; then + OPENCLAW_FOUND=1 + fi + + # Detect Claude Code (claude CLI) + CLAUDE_FOUND=0 + if command -v claude >/dev/null 2>&1; then + CLAUDE_FOUND=1 + elif [ -f "$HOME/.claude/settings.json" ]; then + CLAUDE_FOUND=1 + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ] || [ "$CLAUDE_FOUND" -eq 1 ]; then + printf "${GREEN}${BOLD}✓ AI agent(s) detected!${RESET}\n\n" + fi + + if [ "$OPENCLAW_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting OpenClaw...\n" + "$RAMPART_BIN" setup openclaw 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup openclaw${RESET}\n" + else + printf " Run this to protect your OpenClaw agent:\n" + printf " ${BOLD}rampart setup openclaw${RESET}\n" + printf "\n" + fi + fi + + if [ "$CLAUDE_FOUND" -eq 1 ]; then + if [ "$AUTO_SETUP" = "1" ]; then + printf "${GREEN}▸${RESET} Auto-setup: protecting Claude Code...\n" + "$RAMPART_BIN" setup claude-code 2>&1 || printf "${YELLOW} ↳ Auto-setup failed — run manually: rampart setup claude-code${RESET}\n" + else + printf " Run this to protect Claude Code:\n" + printf " ${BOLD}rampart setup claude-code${RESET}\n" + printf "\n" + fi + fi + + if [ "$OPENCLAW_FOUND" -eq 0 ] && [ "$CLAUDE_FOUND" -eq 0 ]; then + printf " To protect an AI agent, run:\n" + printf " ${BOLD}rampart setup openclaw${RESET} — for OpenClaw\n" + printf " ${BOLD}rampart setup claude-code${RESET} — for Claude Code\n" + printf "\n" + fi +} + +if [ -x "$RAMPART_BIN" ]; then + detect_agents_and_suggest +fi