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
179 changes: 179 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: Publish Docker Image

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.0.9). Must match pyproject.toml."
required: true
type: string
branch:
description: "Branch to build from"
required: true
type: string
default: "main"
publish_dockerhub:
description: "Also publish to Docker Hub"
required: true
type: boolean
default: false
dockerhub_image:
description: "Docker Hub image, e.g. aitechnav/sentinelguard-gateway"
required: false
type: string
default: ""

permissions:
contents: read
id-token: write
packages: write

jobs:
docker:
runs-on: ubuntu-latest

steps:
- name: Resolve release inputs
id: release
env:
DISPATCH_VERSION: ${{ inputs.version }}
DISPATCH_BRANCH: ${{ inputs.branch }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${RELEASE_TAG#v}"
REF="${RELEASE_TAG}"
else
VERSION="${DISPATCH_VERSION}"
REF="${DISPATCH_BRANCH}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "ref=${REF}" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v4
with:
ref: ${{ steps.release.outputs.ref }}

- name: Verify version matches
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$VERSION" != "${{ steps.release.outputs.version }}" ]; then
echo "ERROR: pyproject.toml version ($VERSION) does not match ${{ steps.release.outputs.version }}"
exit 1
fi
echo "Version verified: $VERSION"

- name: Prepare image tags
id: images
env:
VERSION: ${{ steps.release.outputs.version }}
DOCKERHUB_IMAGE_INPUT: ${{ inputs.dockerhub_image }}
DOCKERHUB_IMAGE_VAR: ${{ vars.DOCKERHUB_IMAGE }}
PUBLISH_DOCKERHUB_INPUT: ${{ inputs.publish_dockerhub }}
DOCKERHUB_USERNAME_SET: ${{ secrets.DOCKERHUB_USERNAME != '' }}
DOCKERHUB_TOKEN_SET: ${{ secrets.DOCKERHUB_TOKEN != '' }}
run: |
OWNER=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')
GHCR_IMAGE="ghcr.io/${OWNER}/sentinelguard-gateway"
DOCKERHUB_IMAGE=$(echo "${DOCKERHUB_IMAGE_INPUT:-$DOCKERHUB_IMAGE_VAR}" | tr '[:upper:]' '[:lower:]')
PUBLISH_DOCKERHUB="${PUBLISH_DOCKERHUB_INPUT:-false}"

if [ "${{ github.event_name }}" = "release" ] && [ -n "$DOCKERHUB_IMAGE" ]; then
PUBLISH_DOCKERHUB=true
fi

if [ "$PUBLISH_DOCKERHUB" = "true" ] && [ -z "$DOCKERHUB_IMAGE" ]; then
echo "ERROR: Docker Hub publishing was requested, but no Docker Hub image was configured."
echo "Set the DOCKERHUB_IMAGE repo variable or workflow input, for example aitechnav/sentinelguard-gateway."
exit 1
fi

if [ "$PUBLISH_DOCKERHUB" = "true" ] && { [ "$DOCKERHUB_USERNAME_SET" != "true" ] || [ "$DOCKERHUB_TOKEN_SET" != "true" ]; }; then
echo "ERROR: Docker Hub publishing was requested, but DOCKERHUB_USERNAME or DOCKERHUB_TOKEN is missing."
exit 1
fi

{
echo "tags<<EOF"
echo "${GHCR_IMAGE}:${VERSION}"
echo "${GHCR_IMAGE}:latest"
if [ "$PUBLISH_DOCKERHUB" = "true" ] && [ -n "$DOCKERHUB_IMAGE" ]; then
echo "${DOCKERHUB_IMAGE}:${VERSION}"
echo "${DOCKERHUB_IMAGE}:latest"
fi
echo "EOF"
echo "ghcr_image=${GHCR_IMAGE}"
echo "dockerhub_image=${DOCKERHUB_IMAGE}"
echo "publish_dockerhub=${PUBLISH_DOCKERHUB}"
} >> "$GITHUB_OUTPUT"

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

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Login to Docker Hub
if: steps.images.outputs.publish_dockerhub == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.images.outputs.tags }}
build-args: |
SENTINELGUARD_EXTRAS=gateway,monitoring
labels: |
org.opencontainers.image.title=SentinelGuard Gateway
org.opencontainers.image.description=SentinelGuard OpenAI-compatible LLM gateway with security scanning
org.opencontainers.image.version=${{ steps.release.outputs.version }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
provenance: true
sbom: true

- name: Install cosign
uses: sigstore/cosign-installer@v3

- name: Sign published image tags
env:
TAGS: ${{ steps.images.outputs.tags }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
while IFS= read -r tag; do
if [ -n "$tag" ]; then
cosign sign --yes "${tag}@${DIGEST}"
fi
done <<EOF
${TAGS}
EOF

- name: Summary
run: |
echo "## Docker Publish Successful" >> "$GITHUB_STEP_SUMMARY"
echo "- Version: ${{ steps.release.outputs.version }}" >> "$GITHUB_STEP_SUMMARY"
echo "- Tags:" >> "$GITHUB_STEP_SUMMARY"
while IFS= read -r tag; do
if [ -n "$tag" ]; then
echo " - \`${tag}\`" >> "$GITHUB_STEP_SUMMARY"
fi
done <<EOF
${{ steps.images.outputs.tags }}
EOF
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "SBOM/provenance attestations were generated by Docker Buildx." >> "$GITHUB_STEP_SUMMARY"
echo "Image tags were signed with cosign keyless signing." >> "$GITHUB_STEP_SUMMARY"
56 changes: 56 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Publish Docs

on:
push:
branches: [main]
paths:
- "docs/**"
- "overrides/**"
- "mkdocs.yml"
- "requirements-docs.txt"
- ".github/workflows/docs.yml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install documentation dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-docs.txt

- name: Build documentation
run: mkdocs build --strict --site-dir site

- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RUN python -m pip install --upgrade pip \
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/gateway/health', timeout=3)"
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/gateway/v1/health', timeout=3)"

ENTRYPOINT ["sentinelguard"]
CMD ["gateway", "--host", "0.0.0.0", "--port", "8080"]
17 changes: 17 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pip install "sentinelguard[gateway,monitoring]"
sentinelguard init
```

For Docker Compose, run `sentinelguard init`, copy `.env.example` to `.env`,
set a provider key, then start `docker-compose.sentinelguard.yml`.

For optional model-backed detection with automatic background warmup:

```bash
Expand Down Expand Up @@ -94,12 +97,26 @@ sentinelguard scan prompt "Ignore previous instructions" --format json
# List available scanners
sentinelguard scanners list

# Create and edit scanner config
sentinelguard config init --preset standard --output sentinelguard.yaml
sentinelguard config set prompt_scanners.pii.threshold 0.3 --file sentinelguard.yaml
sentinelguard config disable toxicity --type prompt --file sentinelguard.yaml

# Edit gateway routing and security config
sentinelguard gateway-config set gateway.routing_strategy weighted --file sentinelguard-gateway.yaml
sentinelguard gateway-config set gateway.cache_enabled true --file sentinelguard-gateway.yaml
sentinelguard gateway-config get gateway.providers.0.name --file sentinelguard-gateway.yaml

# Start API server
sentinelguard serve --port 8000

# Create gateway starter files
sentinelguard init

# Start the generated Docker gateway
cp .env.example .env
docker compose -f docker-compose.sentinelguard.yml up --build

# Start OpenAI-compatible LLM gateway from generated config
export OPENAI_API_KEY="sk-..."
export SENTINELGUARD_GATEWAY_API_KEY="local-gateway-token"
Expand Down
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ print(report.summary())
pip install sentinelguard
```

## Documentation Website

The documentation site is built with MkDocs Material and published with GitHub
Pages:

```text
https://aitechnav.github.io/Sentinel_Guard/
```

Build it locally:

```bash
pip install -r requirements-docs.txt
mkdocs serve
```

For gateway mode, install the gateway extra and generate starter files:

```bash
Expand All @@ -65,6 +81,7 @@ sentinelguard init
- `sentinelguard.yaml` for scanner policy
- `sentinelguard-gateway.yaml` for routing, provider, auth, cache, and audit settings
- `.env.example` for provider and gateway keys
- `Dockerfile.sentinelguard` for a small gateway image built from PyPI
- `docker-compose.sentinelguard.yml` for container-based gateway deployment
- `README.sentinelguard.md` with local next steps

Expand Down Expand Up @@ -193,6 +210,19 @@ For a quick single-provider run without generated files:
sentinelguard gateway --provider openai --port 8080
```

Manage scanner and gateway YAML from the CLI:

```bash
# Scanner policy
sentinelguard config set prompt_scanners.pii.threshold 0.3 --file sentinelguard.yaml
sentinelguard config disable toxicity --type prompt --file sentinelguard.yaml

# Gateway routing/security settings
sentinelguard gateway-config set gateway.routing_strategy weighted --file sentinelguard-gateway.yaml
sentinelguard gateway-config set gateway.cache_enabled true --file sentinelguard-gateway.yaml
sentinelguard gateway-config get gateway.providers.0.name --file sentinelguard-gateway.yaml
```

Or run the gateway as a standalone Docker proxy:

```bash
Expand All @@ -208,15 +238,20 @@ docker run --rm -p 8080:8080 \
With Docker Compose:

```bash
docker build -t sentinelguard-gateway:local .
sentinelguard init --docker-image sentinelguard-gateway:local
sentinelguard init
cp .env.example .env
# Edit .env and set at least one upstream provider key.
export OPENAI_API_KEY="sk-..."
export SENTINELGUARD_GATEWAY_API_KEY="local-gateway-token"
docker compose -f docker-compose.sentinelguard.yml up
docker compose -f docker-compose.sentinelguard.yml up --build
```

The generated Docker setup builds `Dockerfile.sentinelguard`, which installs
the configured SentinelGuard version from PyPI. Set `SENTINELGUARD_VERSION` in
`.env` if you want the container to use a different released package version.
Official release images can be published through the GitHub Actions workflow
documented in `docs/docker-release.md`.

From this repository, the included `docker-compose.yml` can also build the
gateway directly. For local Hugging Face model-backed detection inside that
image:
Expand Down Expand Up @@ -400,10 +435,21 @@ gateway:
weight: 1
```

The gateway exposes operational discovery endpoints:
The gateway exposes stable management endpoints under `/gateway/v1`. Older
unversioned endpoints remain available as compatibility aliases.

```text
GET /gateway/v1/contract
GET /gateway/v1/health
GET /gateway/v1/routes
GET /gateway/v1/models
GET /gateway/v1/usage
GET /gateway/v1/provider-health

# OpenAI-compatible model endpoint
GET /v1/models

# Compatibility aliases
GET /models
GET /routes
GET /gateway/usage
Expand All @@ -413,6 +459,10 @@ GET /gateway/health
GET /admin
```

Use `/gateway/v1/contract` as the stable API contract for dashboards,
automation, and operational integrations. See `docs/gateway-api.md` for the
gateway API stability rule.

Gateway state can run in memory for local development or in SQLite for
persistent virtual-key usage, spend, and budget counters. Response caching can
use memory, SQLite, or Redis:
Expand Down
Loading
Loading