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
1 change: 1 addition & 0 deletions docs/ADMIN_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A lightweight web-based admin interface for managing WorkflowEntity objects (Job
- **API Key Required**: Admin interface requires a valid API key
- **Authentication**: Add `X-API-Key` header to requests
- **Secure Access**: All admin endpoints are protected
- **IP allowlist (deployment)**: Set environment variable `ADMIN_ALLOWED_IPS` to restrict which client IPs may access the `/admin` web UI (including static assets and WebSocket upgrades). Comma-separated IPv4/IPv6 addresses and optional CIDR ranges (e.g. `10.0.0.0/8, 203.0.113.4`). If unset or empty, all IPs are allowed. The effective client IP is taken from the first value in `X-Forwarded-For` when present (typical behind a reverse proxy); otherwise the direct peer address is used. Ensure your proxy sets or overwrites `X-Forwarded-For` correctly so the allowlist matches real clients.

### 📊 **Dashboard**

Expand Down
15 changes: 15 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ All notable changes to this project will be documented in this file.

## Unreleased

### Added

- **`ADMIN_ALLOWED_IPS` for admin UI** — When set, only matching client IPs may access `/admin` (HTML, APIs, static files, WebSocket). Comma-separated IPs and optional CIDR notation; empty or unset allows all. Uses the first address in `X-Forwarded-For` when present.

### Unit Tests Results

`just test`

| Status | Count |
| ---------- | ----- |
| ✅ Passed | 446 |
| 🤔 Skipped | 0 |
| 🔴 Failed | 0 |
| ⏱️ in | 48s |

## v0.12.0

### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "Supervaize API",
"description": "API version: v1 Controller version: 0.11.0\n\nAPI for controlling and managing Supervaize agents. \n\nMore information at [https://doc.supervaize.com](https://doc.supervaize.com)\n\n## Authentication\n\nSome endpoints require API key authentication. Protected endpoints expect the API key in the X-API-Key header.\n\n[Swagger](/docs)\n[Redoc](/redoc)\n[OpenAPI](/openapi.json)\n",
"description": "API version: v1 Controller version: 0.12.0\n\nAPI for controlling and managing Supervaize agents. \n\nMore information at [https://doc.supervaize.com](https://doc.supervaize.com)\n\n## Authentication\n\nSome endpoints require API key authentication. Protected endpoints expect the API key in the X-API-Key header.\n\n[Swagger](/docs)\n[Redoc](/redoc)\n[OpenAPI](/openapi.json)\n",
"termsOfService": "https://supervaize.com/terms/",
"contact": {
"name": "Support Team",
Expand Down Expand Up @@ -4023,7 +4023,7 @@
"type": "string",
"format": "date-time",
"title": "Timestamp",
"default": "2026-03-23T18:38:30.447473"
"default": "2026-03-23T22:47:03.534997"
},
"status_code": {
"type": "integer",
Expand Down
4 changes: 2 additions & 2 deletions docs/model_reference/model_core.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Model Reference Core

**Version:** 0.11.0
**Version:** 0.12.0

### `account.Account`

Expand Down Expand Up @@ -438,4 +438,4 @@ public_url: full url (including scheme and port) to use for outbound connections
```


*Uploaded on 2026-03-23 18:38:30*
*Uploaded on 2026-03-23 22:47:03*
6 changes: 3 additions & 3 deletions docs/model_reference/model_extra.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Model Reference extra

**Version:** 0.11.0
**Version:** 0.12.0

### `common.SvBaseModel`

Expand Down Expand Up @@ -458,7 +458,7 @@ Standard error response model
| `error` | `str` | **required** | |
| `error_type` | `<enum 'ErrorType'>` | **required** | |
| `detail` | `str` | `None` | |
| `timestamp` | `datetime` | datetime.datetime(2026, 3, 23, 18, 38, 29, 522267) | |
| `timestamp` | `datetime` | datetime.datetime(2026, 3, 23, 22, 47, 2, 632663) | |
| `status_code` | `int` | **required** | |

### `server.ServerInfo`
Expand Down Expand Up @@ -490,4 +490,4 @@ A base class for creating Pydantic models.
| `details` | `Dict[str, Any]` | **required** | |


*Uploaded on 2026-03-23 18:38:30*
*Uploaded on 2026-03-23 22:47:03*
26 changes: 1 addition & 25 deletions docs/rfc/001-cloud-deployment-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Set for all platforms:
- `SUPERVAIZER_PORT` = `--port`
- `SV_LOG_LEVEL=INFO` (default)
- `SUPERVAIZER_API_KEY` – stored in provider secret store (generated if `--generate-api-key`)
- `ADMIN_ALLOWED_IPS` (optional) – comma-separated IPv4/IPv6 addresses and/or CIDR ranges allowed to access the `/admin` web UI; if unset or empty, all IPs are allowed (see `docs/ADMIN_README.md`). Uses the first hop in `X-Forwarded-For` when present.
- **RSA** (choose one):
- `SV_RSA_PRIVATE_KEY` (secret value), or
- `SV_RSA_PRIVATE_KEY_PATH` (if platform supports volumes)
Expand Down Expand Up @@ -270,21 +271,18 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **CLI Structure Setup** ✅

- ✅ Add `deploy` subcommand to existing CLI (`src/supervaizer/cli.py`)
- ✅ Create `src/supervaizer/deploy/` module structure
- ✅ Add Docker-related dependencies to `pyproject.toml`

2. **Docker Support** ✅

- ✅ Create `Dockerfile` generator (`src/supervaizer/deploy/docker.py`)
- ✅ Create `.dockerignore` generator
- ✅ Add `docker-compose.yml` generator for local testing
- ✅ Implement image building and tagging logic
- ✅ Generate all files under `.deployment/` directory

3. **State Management** ✅

- ✅ Create deployment state manager (`src/supervaizer/deploy/state.py`)
- ✅ Implement `.deployment/state.json` persistence
- ✅ Add state validation and migration logic
Expand All @@ -307,27 +305,23 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **GCP Cloud Run Driver** (`src/supervaizer/deploy/drivers/cloud_run.py`) ✅

- ✅ Artifact Registry integration
- ✅ Secret Manager integration
- ✅ Cloud Run service management
- ✅ Health check verification

2. **AWS App Runner Driver** (`src/supervaizer/deploy/drivers/aws_app_runner.py`) ✅

- ✅ ECR integration
- ✅ Secrets Manager integration
- ✅ App Runner service management
- ✅ Health check verification

3. **DigitalOcean App Platform Driver** (`src/supervaizer/deploy/drivers/do_app_platform.py`) ✅

- ✅ DOCR integration
- ✅ App Platform service management
- ✅ Health check verification

4. **Driver Testing** ✅

- ✅ Unit tests for each driver with mocked APIs
- ✅ Test authentication and permission handling
- ✅ Test resource creation, update, and deletion
Expand All @@ -350,31 +344,26 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **Plan Command** (`src/supervaizer/deploy/commands/plan.py`) ✅

- ✅ Resource detection and diff generation
- ✅ Cost estimation (optional)
- ✅ Dry-run validation

2. **Up Command** (`src/supervaizer/deploy/commands/up.py`) ✅

- ✅ Orchestrate build → push → deploy → verify workflow
- ✅ Handle rollback on failure
- ✅ Progress reporting with rich console

3. **Down Command** (`src/supervaizer/deploy/commands/down.py`) ✅

- ✅ Safe resource cleanup
- ✅ Confirmation prompts
- ✅ Resource dependency handling

4. **Status Command** (`src/supervaizer/deploy/commands/status.py`) ✅

- ✅ Service health reporting
- ✅ Resource utilization metrics
- ✅ Configuration validation

5. **Command Testing** ✅

- ✅ Unit tests for each command with mocked dependencies
- ✅ Test command-line argument parsing and validation
- ✅ Test workflow orchestration and error handling
Expand All @@ -397,37 +386,32 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **Local Command Implementation** (`src/supervaizer/deploy/commands/local.py`) ✅

- ✅ Docker availability checking
- ✅ Test secret generation (API keys and RSA keys)
- ✅ Docker Compose service management
- ✅ Health check verification and reporting
- ✅ Service information display

2. **Local Testing Features** ✅

- ✅ Automatic Docker image building with local-test tag
- ✅ Environment variable configuration for local testing
- ✅ Comprehensive health endpoint testing
- ✅ Service logs display for debugging
- ✅ Cleanup instructions and resource management

3. **Local Testing Integration** ✅

- ✅ Integration with existing Docker management system
- ✅ Consistent secret generation across local and cloud deployments
- ✅ Health check endpoint validation
- ✅ Error handling and cleanup on failure

4. **Local Testing Documentation** ✅

- ✅ Created comprehensive LOCAL_TESTING.md guide
- ✅ Added usage examples and troubleshooting
- ✅ Documented environment variables and configuration
- ✅ Provided cleanup and debugging instructions

5. **Local Testing Validation** ✅

- ✅ Unit tests for all local testing functions
- ✅ Integration tests with Docker Compose
- ✅ Health check endpoint testing
Expand All @@ -445,13 +429,11 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **Secret Management** ✅

- ✅ Environment variables setup in docker
- ✅ Centralized environment variable management with `get_docker_env_vars()` function
- ✅ Factorized environment variable handling in Dockerfile generation

2. **Health Verification** ✅

- ✅ Enhanced health check endpoints with retry logic and exponential backoff
- ✅ Created comprehensive `health.py` utility module with `HealthVerifier` class
- ✅ Implemented `HealthCheckConfig` for configurable health check parameters
Expand All @@ -478,9 +460,7 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **Testing**

- **Unit Testing**

- Test all driver classes (`src/supervaizer/deploy/drivers/`)
- Test all command implementations (`src/supervaizer/deploy/commands/`)
- Test state management (`src/supervaizer/deploy/state.py`)
Expand All @@ -490,7 +470,6 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
- Achieve 90%+ code coverage

- **Integration Testing**

- Mock cloud provider APIs for all three platforms
- Test complete deployment workflows (plan → up → status → down)
- Test error handling and rollback scenarios
Expand Down Expand Up @@ -519,19 +498,16 @@ All deployment artifacts are stored under `.deployment/` directory (added to `.g
**Tasks**:

1. **Security Audit**

- Secret handling validation
- Permission scope review
- Security best practices implementation

2. **Performance Optimization**

- Parallel deployment operations
- Caching improvements
- Resource cleanup optimization

3. **Release Preparation**

- Version bumping and changelog
- Release notes and migration guides
- Community feedback integration
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ ready-to-go:
just generate_documentation
bash -euc 'branch_id="$(but status --json | uv run python tools/get_applied_but_branch_id.py)"; but commit "$branch_id" -m "chore: update documentation" --json --status-after'

# Merge develop to main - after just tag_version
# Merge develop to main
merge-to-main:
@echo "Switching to main branch..."
git checkout main
Expand Down
100 changes: 100 additions & 0 deletions src/supervaizer/admin/ip_allowlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, you can obtain one at
# https://mozilla.org/MPL/2.0/.

"""IP allowlist for the /admin web UI (ADMIN_ALLOWED_IPS)."""

from __future__ import annotations

import ipaddress
import os

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from supervaizer.common import log

_ENV_KEY = "ADMIN_ALLOWED_IPS"


def is_admin_url_path(path: str) -> bool:
"""True for /admin and /admin/... but not /administrator."""
return path == "/admin" or path.startswith("/admin/")


def get_effective_client_ip(request: Request) -> str:
"""Client IP for allowlist checks (reverse-proxy aware).

Uses the first address in ``X-Forwarded-For`` when present (typical behind
nginx/Traefik); otherwise ``request.client.host``.
"""
xff = request.headers.get("x-forwarded-for")
if xff:
return xff.split(",")[0].strip()
if request.client and request.client.host:
return request.client.host
return ""


def _parse_allowed_entries(raw: str) -> list[str]:
return [p.strip() for p in raw.split(",") if p.strip()]


def client_ip_is_allowed(client_ip: str, allowed_raw: str) -> bool:
"""Return True if client_ip matches any entry (exact IP or CIDR).

``allowed_raw`` is the value of ``ADMIN_ALLOWED_IPS`` (comma-separated).
"""
if not allowed_raw.strip():
return True
if not client_ip:
return False
try:
addr = ipaddress.ip_address(client_ip)
except ValueError:
log.warning(f"[Admin IP allowlist] Unparseable client IP: {client_ip!r}")
return False
for entry in _parse_allowed_entries(allowed_raw):
if "/" in entry:
try:
net = ipaddress.ip_network(entry, strict=False)
if addr in net:
return True
except ValueError:
log.warning(f"[Admin IP allowlist] Ignoring invalid CIDR: {entry!r}")
continue
else:
try:
if addr == ipaddress.ip_address(entry):
return True
except ValueError:
log.warning(f"[Admin IP allowlist] Ignoring invalid IP: {entry!r}")
continue
return False


def admin_request_ip_allowed(request: Request) -> bool:
"""True when ``ADMIN_ALLOWED_IPS`` is unset/empty or the client IP is allowed."""
allowed = os.getenv(_ENV_KEY, "").strip()
if not allowed:
return True
return client_ip_is_allowed(get_effective_client_ip(request), allowed)


class AdminIPAllowlistMiddleware(BaseHTTPMiddleware):
"""Reject requests to ``/admin`` when ``ADMIN_ALLOWED_IPS`` is set and IP is not listed."""

async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
if not is_admin_url_path(request.url.path):
return await call_next(request)
if admin_request_ip_allowed(request):
return await call_next(request)
return JSONResponse(
status_code=403,
content={"detail": "Admin interface is not allowed for this client IP"},
)
2 changes: 2 additions & 0 deletions src/supervaizer/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from supervaizer.__version__ import API_VERSION, VERSION
from supervaizer.account import Account
from supervaizer.admin.ip_allowlist import AdminIPAllowlistMiddleware
from supervaizer.admin.routes import create_admin_routes
from supervaizer.agent import Agent
from supervaizer.common import (
Expand Down Expand Up @@ -543,6 +544,7 @@ async def validation_exception_handler(

# Deploy admin routes if API key is available
if self.api_key and admin_interface:
self.app.add_middleware(AdminIPAllowlistMiddleware)
log.info(
f"[Server launch] 💼 Deploy Admin interface @ {self.public_url}/admin"
)
Expand Down
Loading
Loading