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

on:
pull_request:
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: scopilot
POSTGRES_PASSWORD: scopilot
POSTGRES_DB: scopilot
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U scopilot"
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis:7-alpine
ports: ["6379:6379"]
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,api,worker,webex,mcp,cli,sources,postgres]"
- name: Lint
run: ruff check .
- name: Run Alembic migrations against Postgres
env:
SCOPILOT_DB__URL: postgresql+asyncpg://scopilot:scopilot@localhost:5432/scopilot
SCOPILOT_DB__SYNC_URL: postgresql+psycopg2://scopilot:scopilot@localhost:5432/scopilot
run: alembic upgrade head
- name: pytest
env:
SCOPILOT_DB__URL: sqlite+aiosqlite:///:memory:
run: pytest -v --tb=short

docker:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trivy scan
uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: table
severity: CRITICAL,HIGH
exit-code: "0" # warn, don't fail (Phase-6 follow-up: fail on CRITICAL)
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,31 @@ project into six phases. **Phase 1 (this PR)** lands the foundation:
- Schema includes `proposals`, `proposal_audit`, `matrix_versions`,
`threat_lookups`, `audit_events` so Phases 3 and 5 can land additively.

**Phase 5 (this PR)** adds:
**Phase 6 (this PR)** adds:

- `services/mcp_server/` — MCP server exposing 14 tools (runs, SGT,
proposals, threat intel). Two transports on one shared registry:
stdio (`python -m services.mcp_server.stdio`) for Claude Code /
Desktop, and streamable HTTP for LibreChat / remote clients.
`set_sgt_name` is gated by `--allow-dictionary-edit`.
- `deploy/Dockerfile` — multi-stage; one image serves every role
(api / worker / scheduler / mcp / threat-daemon / webex-bot / ui).
Non-root, read-only-rootfs friendly.
- `deploy/docker-compose.yml` — full stack (postgres + redis + every
service) behind Compose profiles for the optional ones (webex,
threat, ui).
- `deploy/k8s/base/` — kustomize base with Deployments + Services +
HPAs + PodDisruptionBudget for the scheduler + Ingress + an example
NetworkPolicy stack. Migration runs as a pre-install Job /
argocd-sync-wave -10.
- `core/observability/` — JSON structured logs + Prometheus metrics
(counters for flow_unknown / classifications / proposals /
threat_lookups). API exposes `/metrics`.
- `.github/workflows/ci.yml` — ruff lint, Alembic migration on a real
Postgres service container, `pytest` against the full matrix,
Docker build + push to GHCR on `main`, Trivy scan.

**Phase 5** added:

- `core/threat/` — pluggable threat-intelligence layer with a
`ThreatIntelClient` Protocol and four implementations:
Expand Down Expand Up @@ -213,9 +237,20 @@ project into six phases. **Phase 1 (this PR)** lands the foundation:
`BackgroundTasks`; `POST /v1/proposals/{id}/decision` goes through the
state machine.

Subsequent phases (separate PRs):
## Deploy

- **Phase 6** — MCP server + K8s manifests + observability + CI/CD.
```bash
# Local: full stack on docker-compose
docker compose -f deploy/docker-compose.yml up -d
docker compose -f deploy/docker-compose.yml --profile ui --profile webex up -d

# Kubernetes
kubectl apply -k deploy/k8s/base
```

Both targets need at least `SCOPILOT_ANTHROPIC__API_KEY`; threat-intel
and WebEx-bot keys are optional. In production, source secrets via
External Secrets Operator rather than the example `Secret`.

## Apply migrations

Expand Down
3 changes: 1 addition & 2 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@

from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool

from alembic import context
from segmentation_copilot.config import get_settings
from segmentation_copilot.core.models.orm import Base


config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
Expand Down
10 changes: 5 additions & 5 deletions alembic/versions/50d5b46784ac_initial_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
Revises:
Create Date: 2026-05-22 11:29:07.382587
"""
from typing import Sequence, Union
from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa

from alembic import op

revision: str = '50d5b46784ac'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
Expand Down
1 change: 0 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import httpx
import streamlit as st


DEFAULT_API_BASE = os.environ.get("SCOPILOT_API_BASE", "http://localhost:8000")


Expand Down
17 changes: 17 additions & 0 deletions deploy/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.git/
.github/
.venv/
__pycache__/
*.pyc
data/
.pytest_cache/
.ruff_cache/
.mypy_cache/
.coverage
htmlcov/
*.egg-info/
build/
dist/
node_modules/
deploy/k8s/
*.md
49 changes: 49 additions & 0 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Multi-stage build — one image, one role per replica.
#
# The image installs ALL optional extras so the same artifact can serve
# every role (api / worker / scheduler / threat-daemon / webex-bot /
# mcp-http / streamlit-ui). Pick the role with the container CMD or
# pod spec. Keeps the registry footprint to one tag per release.

ARG PYTHON_VERSION=3.11

# --- builder ---------------------------------------------------------------
FROM python:${PYTHON_VERSION}-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONDONTWRITEBYTECODE=1

WORKDIR /build
COPY pyproject.toml README.md ./
COPY src/ src/
COPY services/ services/
COPY alembic.ini ./
COPY alembic/ alembic/
COPY app.py ./

RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install ".[api,worker,webex,mcp,cli,ui,sources,otel,postgres]"

# --- runtime ---------------------------------------------------------------
FROM python:${PYTHON_VERSION}-slim AS runtime

ENV PATH="/opt/venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1

# Run as non-root. UID 10001 matches the K8s PSP-style baseline.
RUN groupadd --system --gid 10001 scopilot && \
useradd --system --uid 10001 --gid scopilot --home /app --shell /usr/sbin/nologin scopilot

WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /build /app

USER 10001:10001

# Default to the API; override the CMD per replica.
EXPOSE 8000
CMD ["uvicorn", "services.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Loading