Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Docker

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to build (e.g., v2.3.0)'
required: true
type: string

env:
REGISTRY: docker.io
IMAGE_NAME: capiscio/guard

jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For workflow_dispatch, the workflow accepts an input tag but actions/checkout doesn’t check out that tag/commit (no ref: set). This will build whatever is on the default branch while tagging/pushing it as the requested release tag. Set with: ref: ${{ inputs.tag }} (or resolve the tag to a SHA) when github.event_name == 'workflow_dispatch'.

Suggested change
uses: actions/checkout@v4
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}

Copilot uses AI. Check for mistakes.

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Extract version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.tag }}"
else
VERSION="${GITHUB_REF#refs/tags/}"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_short=${VERSION#v}" >> $GITHUB_OUTPUT

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}},value=${{ steps.version.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }}
type=semver,pattern={{major}},value=${{ steps.version.outputs.version }}
type=raw,value=latest,enable=${{ github.event_name == 'push' }}

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
COMMIT=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.IMAGE_NAME }}
short-description: "Security Sidecar for AI Agents - Universal Authority Layer"
readme-filepath: ./README.md

# Verify the image works
verify:
name: Verify Image
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Pull and test image
run: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.version || 'latest' }}
docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.version || 'latest' }} --version
Comment on lines +88 to +97
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verify job pulls ${{ needs.build-and-push.outputs.version || 'latest' }}, but build-and-push does not define any job outputs, so this will always fall back to latest. For workflow_dispatch (where latest tagging is disabled), this means verify may test an old image instead of the one just built. Expose the version from the build job (e.g., map it from steps.version.outputs.version) and use that tag here.

Copilot uses AI. Check for mistakes.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.4.0] - 2026-01-18

### Added
- **Docker Image**: Official `capiscio/guard` Docker image for sidecar deployment (~15MB, distroless)
- **GitHub Actions**: Automated Docker build/push workflow for multi-arch (amd64/arm64)
- **MCP Service**: Server identity operations via MCP protocol

### Changed
- Updated badge command and proto definitions

## [2.3.1] - 2025-01-14

### Fixed
Expand Down
75 changes: 75 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# syntax=docker/dockerfile:1

# ============================================================================
# CapiscIO Guard - Security Sidecar for AI Agents
# ============================================================================
# Multi-stage build for a minimal, secure container image.
#
# Usage:
# docker pull capiscio/guard
# docker run -p 8080:8080 capiscio/guard \
# gateway start --port 8080 --target http://your-agent:3000 --registry-url https://registry.capisc.io
#
# Build locally:
# docker build -t capiscio/guard .
# docker build --build-arg VERSION=v2.3.0 -t capiscio/guard:v2.3.0 .
# ============================================================================

# -----------------------------------------------------------------------------
# Stage 1: Build
# -----------------------------------------------------------------------------
FROM golang:1.24-alpine AS builder

# Build arguments
ARG VERSION=dev
ARG COMMIT=unknown

# Install build dependencies
RUN apk add --no-cache git ca-certificates

WORKDIR /src

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source
COPY . .

# Build static binary with version info
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s -X main.version=${VERSION} -X main.commit=${COMMIT}" \
Comment on lines +40 to +41
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The builder stage hard-codes GOARCH=amd64, but the workflow builds linux/amd64,linux/arm64. This will produce an amd64 binary inside the arm64 image (leading to exec format error at runtime). Use BuildKit-provided TARGETARCH/TARGETOS (or omit GOARCH and rely on --platform) so each platform builds the correct binary.

Copilot uses AI. Check for mistakes.
-o /capiscio \
Comment on lines +41 to +42
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -ldflags set -X main.commit=..., but there is no commit string variable in the main package (cmd/capiscio/main.go only defines var version = ...). This causes the Docker build to fail at link time. Either add a var commit = "..." in the main package or drop the -X main.commit flag.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +42
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-ldflags injects -X main.commit=${COMMIT}, but there is no commit variable in package main (cmd/capiscio/main.go only defines version). This will make go build fail in Docker and CI. Either add var commit = "unknown" in the main package (and optionally include it in the cobra Version output), or remove the main.commit ldflag and corresponding build-arg usage.

Copilot uses AI. Check for mistakes.
./cmd/capiscio

# -----------------------------------------------------------------------------
# Stage 2: Runtime (distroless for security)
# -----------------------------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot

# Labels for container registry
LABEL org.opencontainers.image.title="CapiscIO Guard"
LABEL org.opencontainers.image.description="Security Sidecar for AI Agents - Universal Authority Layer"
LABEL org.opencontainers.image.url="https://capisc.io"
LABEL org.opencontainers.image.source="https://github.com/capiscio/capiscio-core"
LABEL org.opencontainers.image.vendor="CapiscIO"
LABEL org.opencontainers.image.licenses="Apache-2.0"

# Copy binary from builder
COPY --from=builder /capiscio /capiscio

# Expose default gateway port
EXPOSE 8080

# Health check - verify binary is runnable
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/capiscio", "--version"]

# Run as non-root user (distroless nonroot = uid 65532)
USER nonroot:nonroot

# Default entrypoint
ENTRYPOINT ["/capiscio"]

# Default command (show help)
CMD ["--help"]
33 changes: 32 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
.PHONY: all build-cli build-python test clean proto docs
.PHONY: all build-cli build-python test clean proto docs docker docker-build docker-run
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker-push is defined below but not listed in .PHONY, so a file named docker-push could prevent this target from running. Add docker-push to the .PHONY list.

Suggested change
.PHONY: all build-cli build-python test clean proto docs docker docker-build docker-run
.PHONY: all build-cli build-python test clean proto docs docker docker-build docker-run docker-push

Copilot uses AI. Check for mistakes.

all: build-cli build-python

# =============================================================================
# Docker targets for capiscio/guard
# =============================================================================
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
DOCKER_IMAGE := capiscio/guard

docker: docker-build ## Build Docker image (alias)

docker-build: ## Build Docker image locally
@echo "Building Docker image $(DOCKER_IMAGE):$(VERSION)..."
docker build \
--build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(COMMIT) \
-t $(DOCKER_IMAGE):$(VERSION) \
-t $(DOCKER_IMAGE):latest \
.

docker-run: ## Run Docker image locally (example)
@echo "Running $(DOCKER_IMAGE):latest..."
@echo "Usage: make docker-run TARGET=http://localhost:3000"
docker run --rm -p 8080:8080 $(DOCKER_IMAGE):latest \
gateway start --port 8080 --target $(TARGET) --registry-url https://registry.capisc.io
Comment on lines +23 to +27
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker-run passes --target $(TARGET) but TARGET has no default, so running make docker-run without setting TARGET will pass an empty value and likely fail. Consider setting a sensible default (e.g., TARGET ?= http://host.docker.internal:3000) or erroring out when TARGET is unset.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make docker-run prints an example using TARGET=http://localhost:3000, but from inside the container localhost refers to the container itself, not the host. This will typically fail unless the user runs with host networking. Consider defaulting TARGET to http://host.docker.internal:3000 (with a Linux note or --add-host=host.docker.internal:host-gateway) or erroring out when TARGET is empty/localhost to avoid a confusing failure.

Copilot uses AI. Check for mistakes.

docker-push: ## Push Docker image to registry (requires docker login)
docker push $(DOCKER_IMAGE):$(VERSION)
docker push $(DOCKER_IMAGE):latest

# =============================================================================
# Build targets
# =============================================================================
proto:
@echo "Generating protobuf files..."
cd proto && buf generate
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,69 @@ curl -H "X-Capiscio-Badge: $(cat badge.jwt)" http://localhost:8080/api/v1/agent
curl http://localhost:8080/api/v1/agent
```

## 🐳 Docker

The **CapiscIO Guard** is available as a Docker image for easy deployment:

```bash
docker pull capiscio/guard
```

### Quick Start with Docker

```bash
# Run the gateway in front of your agent
docker run -p 8080:8080 capiscio/guard \
gateway start \
--port 8080 \
--target http://host.docker.internal:3000 \
--registry-url https://registry.capisc.io
```

Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quick-start example uses --target http://host.docker.internal:3000, which isn’t available by default on many Linux Docker setups. Consider adding a short Linux note (e.g., using --add-host=host.docker.internal:host-gateway or using a user-defined network / container name) to prevent copy/paste failures.

Suggested change
> **Note for Linux users:** `host.docker.internal` is not available by default on many Linux Docker setups.
> You can either:
> - add `--add-host=host.docker.internal:host-gateway` to the `docker run` command so the container can reach your host at `http://host.docker.internal:3000`, or
> - run your agent in another container on a user-defined Docker network and use the container name as the `--target` (as shown in the Docker Compose example below).

Copilot uses AI. Check for mistakes.
### Docker Compose

```yaml
version: '3.8'
services:
guard:
image: capiscio/guard:latest
ports:
- "8080:8080"
command:
- gateway
- start
- --port=8080
- --target=http://agent:3000
- --registry-url=https://registry.capisc.io
depends_on:
- agent

agent:
image: your-agent:latest
# Your agent runs on internal port 3000
```

### Available Tags

| Tag | Description |
|-----|-------------|
| `latest` | Latest stable release |
| `vX.Y.Z` | Specific version (e.g., `v2.3.0`) |
| `vX.Y` | Latest patch for minor version |
| `vX` | Latest for major version |

### Building Locally

```bash
# Clone and build
git clone https://github.com/capiscio/capiscio-core.git
cd capiscio-core
make docker-build

# Test locally
make docker-run TARGET=http://localhost:3000
Comment on lines +183 to +184
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local test command suggests make docker-run TARGET=http://localhost:3000, but localhost inside the Guard container points back to the container, not your host agent, so this example will usually fail. Consider changing the example to TARGET=http://host.docker.internal:3000 (and add a note for Linux users, e.g. --add-host=host.docker.internal:host-gateway), or recommend using the Docker Compose example for local testing.

Suggested change
# Test locally
make docker-run TARGET=http://localhost:3000
# Test locally (container -> host agent)
# macOS/Windows: host.docker.internal resolves to the host
# Linux: ensure your docker run includes --add-host=host.docker.internal:host-gateway
make docker-run TARGET=http://host.docker.internal:3000

Copilot uses AI. Check for mistakes.
```

### 7. gRPC Server (SDK Integration)

The gRPC server provides programmatic access to all CapiscIO functionality for SDKs in Python, Node.js, and other languages. The SDKs automatically manage the gRPC server process.
Expand Down
16 changes: 8 additions & 8 deletions cmd/capiscio/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ var issueCmd = &cobra.Command{
Short: "Issue a new Trust Badge",
Long: `Issue a new Trust Badge.

Trust Levels:
0 - Self-signed (did:key, iss == sub) - implied by --self-sign
1 - Domain Validated (DV) - requires registry CA
2 - Organization Validated (OV) - requires registry CA
3 - Extended Validated (EV) - requires registry CA
4 - Community Vouched (CV) - requires registry CA
Trust Levels (RFC-002 §5):
0 - Self-Signed (SS) - did:key, iss == sub - implied by --self-sign
1 - Registered (REG) - account registration with CA
2 - Domain Validated (DV) - DNS/HTTP domain ownership proof
3 - Organization Validated (OV) - legal entity verification
4 - Extended Validated (EV) - manual review + security audit

Examples:
# Self-signed badge (level 0 implied)
Expand Down Expand Up @@ -1283,7 +1283,7 @@ func init() {
issueCmd.Flags().StringVar(&issueIssuer, "iss", "did:web:registry.capisc.io", "Issuer DID (auto-set to did:key for level 0)")
issueCmd.Flags().StringVar(&issueDomain, "domain", "example.com", "Agent Domain")
issueCmd.Flags().DurationVar(&issueExpiry, "exp", 5*time.Minute, "Expiration duration (default 5m per RFC-002)")
issueCmd.Flags().StringVar(&issueLevel, "level", "1", "Trust level (0=self-signed, 1=DV, 2=OV, 3=EV, 4=CV)")
issueCmd.Flags().StringVar(&issueLevel, "level", "1", "Trust level (0=SS, 1=REG, 2=DV, 3=OV, 4=EV per RFC-002)")
issueCmd.Flags().StringVar(&issueAudience, "aud", "", "Audience (comma-separated URLs)")
issueCmd.Flags().BoolVar(&issueSelfSign, "self-sign", false, "Issue self-signed badge (implies level 0)")
issueCmd.Flags().StringVar(&keyFile, "key", "", "Path to private key file (optional, auto-generates if not provided)")
Expand All @@ -1292,7 +1292,7 @@ func init() {
keepCmd.Flags().StringVar(&keepAgentID, "agent-id", "", "Agent ID (UUID) to request badges for")
keepCmd.Flags().StringVar(&issueDomain, "domain", "", "Agent domain (optional, uses agent's registered domain)")
keepCmd.Flags().DurationVar(&issueExpiry, "exp", 5*time.Minute, "Badge expiration duration")
keepCmd.Flags().StringVar(&issueLevel, "level", "1", "Trust level (1=DV, 2=OV, 3=EV, 4=CV)")
keepCmd.Flags().StringVar(&issueLevel, "level", "1", "Trust level (1=REG, 2=DV, 3=OV, 4=EV per RFC-002)")
keepCmd.Flags().StringVar(&keyFile, "key", "", "Path to private key file (required for --self-sign)")
keepCmd.Flags().StringVar(&keepOutFile, "out", "badge.jwt", "Output file path for badge")
keepCmd.Flags().DurationVar(&keepRenewBefore, "renew-before", 1*time.Minute, "Time before expiry to renew")
Expand Down
2 changes: 1 addition & 1 deletion cmd/capiscio/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
)

var version = "2.3.1"
var version = "2.4.0"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description focuses on adding Docker support, but this change also bumps the CLI version string to 2.4.0. If 2.4.0 is not being released as part of this PR, consider keeping the existing version and letting the release/tag drive the version, or update the PR description/release notes to reflect the version bump.

Copilot uses AI. Check for mistakes.

var rootCmd = &cobra.Command{
Use: "capiscio",
Expand Down
14 changes: 8 additions & 6 deletions proto/capiscio/v1/badge.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ service BadgeService {
rpc StartKeeper(StartKeeperRequest) returns (stream KeeperEvent);
}

// Trust level for badges (RFC-002 v1.1)
// Trust level for badges (RFC-002 §5)
// NOTE: Proto enum ordinals (1-5) map to RFC-002 level strings ("0"-"4")
// The badge JWT `vc.credentialSubject.level` uses the RFC string values
enum TrustLevel {
TRUST_LEVEL_UNSPECIFIED = 0;
TRUST_LEVEL_SELF_SIGNED = 1; // Self-signed (Level 0, did:key)
TRUST_LEVEL_DV = 2; // Domain Validated (Level 1)
TRUST_LEVEL_OV = 3; // Organization Validated (Level 2)
TRUST_LEVEL_EV = 4; // Extended Validated (Level 3)
TRUST_LEVEL_CV = 5; // Community Vouched (Level 4)
TRUST_LEVEL_SELF_SIGNED = 1; // RFC-002 Level "0": Self-Signed (SS) - did:key, iss == sub
TRUST_LEVEL_DV = 2; // RFC-002 Level "1": Registered (REG) - account registration
TRUST_LEVEL_OV = 3; // RFC-002 Level "2": Domain Validated (DV) - DNS/HTTP proof
TRUST_LEVEL_EV = 4; // RFC-002 Level "3": Organization Validated (OV) - legal entity
TRUST_LEVEL_CV = 5; // RFC-002 Level "4": Extended Validated (EV) - security audit
Comment on lines +44 to +53
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TrustLevel enum value names no longer match their documented meanings (e.g., TRUST_LEVEL_DV is commented as Registered/REG, while TRUST_LEVEL_OV is commented as Domain Validated/DV). This is confusing for API consumers and easy to misuse. Consider adding correctly named aliases (e.g., TRUST_LEVEL_REG = 2, TRUST_LEVEL_DV = 3, etc.) with option allow_alias = true, and/or adjust names/comments so they consistently describe the same level.

Copilot uses AI. Check for mistakes.
}

// Badge claims structure
Expand Down