Development Guide¶
Prerequisites¶
| Tool | Version | Notes |
|---|---|---|
| Docker + Docker Compose | latest | Easiest way to run the full stack |
| Python | 3.12+ | Containers ship 3.12-slim; Pipfile declares 3.14 for local dev |
| Node.js | 20+ | Frontend builds against node:20-alpine |
| Go | 1.22+ | Required for the LLM gateway |
| PostgreSQL | 16+ | Bundled in docker-compose.yml, otherwise install locally |
| Redis | 7+ | Required for Celery, rate limits, session quotas |
Repository Layout¶
zentinelle.git/
├── backend/ Django 5 backend
│ └── zentinelle/
│ ├── api/views/ REST views (assistant.py, evaluate.py, events.py, ...)
│ ├── auth/ Pluggable auth (open / local / sso)
│ ├── clickhouse/ Optional analytics store
│ ├── management/commands/ CLI entry points (bootstrap_token, generate_secrets, ...)
│ ├── models/ Tenant-scoped models (policy, risk, compliance, ...)
│ ├── proxy/ Provider proxies for LLM calls routed through Django
│ ├── schema/ Strawberry GraphQL schema
│ ├── services/ Policy engine, evaluators, content scanner, alerts
│ └── tasks/ Celery tasks (events, billing, compliance_monitoring)
├── frontend/ Next.js 16 GRC portal (TypeScript, Apollo)
│ └── app/
│ ├── (app)/ Authenticated app routes (agents, policies, compliance, ...)
│ ├── (login)/ Auth flows
│ ├── api/ Next.js route handlers (proxy + assistant)
│ └── actions/ Server actions
├── gateway/ Go LLM sidecar (:8742) — see gateway/README.md
├── scripts/ Development scripts (demo_agent.py, codemap.py, kf.py)
├── docker/ init.sql, nginx.conf
├── docs/wiki/ MkDocs Material source for zentinelle.dev
├── docker-compose.yml 8-service stack (postgres, redis, backend, celery × 2, nginx, frontend, gateway)
└── Makefile Common dev commands
Quick Start (Docker Compose)¶
git clone https://github.com/calliopeai/zentinelle.git
cd zentinelle
cp .env.example .env
docker compose up -d
Defaults to AUTH_MODE=open so no login is required. Open http://localhost:8080 — you're in the portal as admin.
Services and ports:
| Service | URL | Notes |
|---|---|---|
| Portal | http://localhost:8080 | Nginx → Next.js + Django |
| GraphQL | http://localhost:8080/gql/zentinelle/ | Management API |
| REST API | http://localhost:8080/api/zentinelle/v1/ | Agent-facing |
| Admin | http://localhost:8080/admin/ | Django admin |
| LLM Gateway | http://localhost:8742 | Go sidecar |
To register an agent you need a bootstrap token:
docker compose exec backend python manage.py bootstrap_token generate \
00000000-0000-0000-0000-000000000001 --label "default tenant"
Local Dev (No Docker)¶
Backend¶
cd backend
pipenv install
cp ../.env.example .env
pipenv run python manage.py migrate
pipenv run python manage.py createsuperuser
pipenv run python manage.py runserver 0.0.0.0:8000
Background workers (separate terminals):
cd backend
pipenv run celery -A config worker -l info # event processing
pipenv run celery -A config beat -l info # periodic tasks (baselines, retention, monitoring)
Frontend¶
Gateway¶
cd gateway
go build -o zentinelle-gateway .
ZENTINELLE_URL=http://localhost:8000 ./zentinelle-gateway
Run Real Agents¶
scripts/demo_agent.py registers a real agent against a running Zentinelle, sends events, evaluates policies, and heartbeats — perfect for validating end-to-end before integrating real workloads.
# 1. Generate a bootstrap token (default tenant)
docker compose exec backend python manage.py bootstrap_token generate \
00000000-0000-0000-0000-000000000001 --label "demo"
# 2. Run the demo agent
ZENTINELLE_URL=http://localhost:8080 \
ZENTINELLE_BOOTSTRAP_TOKEN=bt_00000000-0000-0000-0000-000000000001_... \
python scripts/demo_agent.py
While it runs, watch the agent appear at http://localhost:8080/agents, events stream to /events, and policy evaluations land in /audit-logs.
The script imports the SDK from a sibling checkout of zentinelle-sdk.git/python/. If you don't have one, install the published package:
Management Commands¶
All commands run via python manage.py <command> (or docker compose exec backend python manage.py ...). Source: backend/zentinelle/management/commands/.
| Command | Purpose |
|---|---|
bootstrap_token generate <tenant_id> |
Mint an HMAC-signed agent registration token |
bootstrap_token list [tenant_id] |
List active tokens |
bootstrap_token revoke <token_prefix> |
Revoke a token |
generate_secrets |
Print fresh SECRET_KEY, ZENTINELLE_SECRET_KEY, ZENTINELLE_BOOTSTRAP_SECRET |
setup_sentinel |
Load AI provider fixtures and configure periodic tasks |
seed_demo |
Create demo tenant, agents, policies, risks, incidents, and events |
seed_models |
Seed AIModel registry from MODEL_FIXTURES |
sync_models [provider] |
Pull live model lists from provider APIs |
seed_prompt_library |
Seed the system prompt library |
activate_compliance_pack <pack> --tenant <id> |
Provision a baseline policy bundle (hipaa, soc2, ...) |
policy_apply / policy_diff / policy_validate / policy_export |
Policy-as-code workflow |
create_junohub_endpoint |
Provision a Calliope-managed JunoHub endpoint |
generate_offline_license |
Issue an offline license file |
Run any command with --help for full options.
Building the Go Gateway¶
The gateway is a single static binary. It detects provider from the request path (/v1/chat/completions → OpenAI, /v1/messages → Anthropic, /v1beta/models/* → Google), calls Zentinelle /api/zentinelle/v1/evaluate for the policy decision, injects the real provider key on success, and streams the response back. SSE-aware. See gateway/README.md for full env-var reference.
Docker build:
Database Schemas¶
Zentinelle ships with two PostgreSQL schemas managed by backend/zentinelle/db_router.py:
| Schema | Contents | Purpose |
|---|---|---|
public |
Django auth + sessions | Framework tables |
zentinelle |
Agents, policies, events, risks, incidents, compliance | Core GRC data |
zentinelle_analytics |
UsageMetric, UsageAggregate, AuditLog |
High-volume analytics |
All schemas live in the same database by default. The analytics schema can be split off (or replaced with ClickHouse via CLICKHOUSE_URL) without touching the core schema.
Auth Modes¶
AUTH_MODE is set in .env and picked up by backend/config/settings/base.py.
| Mode | Behaviour |
|---|---|
open |
No login required. Every request is admin. Internal/dev only — prod.py rejects startup if used. |
local |
Built-in username/password with session cookies. Django auth + sessions. |
sso |
OIDC/SAML through any provider (Google, Okta, Cognito, Entra ID, Keycloak). Requires OIDC_* env vars. |
Backwards compatibility: standalone is an alias for local.
In production:
- SECRET_KEY, ZENTINELLE_SECRET_KEY, ZENTINELLE_BOOTSTRAP_SECRET must be set or startup fails.
- AUTH_MODE=open is rejected.
- HSTS, secure cookies, CORS allowlist, and X-Frame-Options: DENY are enforced automatically.
Generate fresh secrets with python manage.py generate_secrets.
Testing¶
cd backend
pipenv run pytest # all tests
pipenv run pytest zentinelle/tests/ # zentinelle only
pipenv run pytest zentinelle/tests/test_policy_engine.py
pipenv run pytest -x # stop on first failure
pipenv run pytest --cov=zentinelle # with coverage
Pre-Commit Validation¶
Run before every backend commit. Stop and fix on any failure — don't commit past a halt.
cd backend
# 1. Django startup — catches schema ordering and import errors
pipenv run python -c \
"import django, os; os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings'); django.setup()"
# 2. Migration check — catches unapplied model changes
pipenv run python manage.py makemigrations --check --dry-run
# 3. Tests
pipenv run pytest
# 4. Lint
pipenv run flake8
pipenv run isort --check-only .
GraphQL Workflow¶
Zentinelle exposes a single GraphQL endpoint at POST /gql/zentinelle/ (see api.md).
# 1. Modify schema in backend/zentinelle/schema/
# 2. Export schema file
cd backend && pipenv run python manage.py dev_utils --generate_schema
# 3. Regenerate TypeScript types (frontend must have backend running)
cd frontend && npm run compile
Schema Ordering Rule¶
Critical: define types in this exact order in every schema file. Forward references crash Django startup silently.
- Imports
- Enums and InputTypes
- ObjectTypes
- Result types (
*Result,*Connection) - Query class
- Mutation class
Migrations¶
cd backend
pipenv run python manage.py makemigrations zentinelle # after model changes
pipenv run python manage.py migrate # apply
pipenv run python manage.py showmigrations # check
All Zentinelle models live in the zentinelle schema (via DB router). Migrations live in zentinelle/migrations/. Analytics models route to zentinelle_analytics automatically.
Adding a New Policy Type¶
- Add the type to
Policy.PolicyTypeinbackend/zentinelle/models/policy.py. - Implement the evaluator in
backend/zentinelle/services/evaluators/<your_type>.py(subclassBasePolicyEvaluator). - Wire it into
backend/zentinelle/services/policy_engine.py(the_evaluatorsdict). - Export from
backend/zentinelle/services/evaluators/__init__.py. - Add GraphQL types in
backend/zentinelle/schema/types/policy.pyand mutations inschema/mutations/policy.py. - Add a config snippet to the simulator UI defaults in
frontend/app/(app)/policies/simulator/page.tsx. - Add it to
ALL_POLICY_TYPESinfrontend/app/(app)/policies/analyzer/page.tsx. - Update
docs/wiki/policies.mdwith the new policy block. - Write tests in
backend/zentinelle/tests/test_policy_engine.py.
Adding a New REST Endpoint¶
- Create a view in
backend/zentinelle/api/views/. - Register the URL in
backend/zentinelle/api/urls.py. - Use
TenantResolvermiddleware for tenant scoping. Always filter querysets bytenant_id. - Write tests under
backend/zentinelle/tests/. - Update
docs/wiki/api.md.
Adding an SDK Framework Plugin¶
Plugins live in the sibling repo zentinelle-sdk.git/. Each plugin:
- Wraps the core SDK client.
- Intercepts the framework's LLM calls.
- Calls
evaluate()before andemit()after. - Handles errors gracefully — never break the agent.
See the LangChain plugin in zentinelle-sdk.git/python/zentinelle/plugins/langchain/ for the reference implementation.
Naming Conventions¶
| Pattern | Convention | Example |
|---|---|---|
| GraphQL ObjectType | <Entity>Type |
PolicyType, AgentEndpointType |
| GraphQL Mutation | <Verb><Entity> |
CreatePolicy, UpdateAgentEndpoint |
| GraphQL Query class | <Domain>Query |
PolicyQuery, AgentQuery |
| REST view | <Entity>View |
EvaluateView, RegisterView |
| Service | <Domain>Service or <Domain>Engine |
PolicyEngine, ContentScannerService |
| Celery task | <verb>_<entity> |
process_events, generate_compliance_report |
Common Field Names¶
| Model | Correct | Wrong |
|---|---|---|
AgentEndpoint |
agent_id (slug) |
endpoint_id |
Policy |
scope_type |
target_type |
AgentEndpoint |
api_key_hash + api_key_prefix |
api_key (never stored) |
| All models | tenant_id (string) |
organization_id, org_fk |
Git Workflow¶
- No co-authorship messages in commits.
- No rebases or force-pushes.
- Commit directly to
main. - Don't push to remote unless explicitly asked.