Agent Config Service
The Agent Config Service manages prompt templates, versioning, lifecycle, and runtime resolution for all AEGIS AI agents. It provides a centralized, auditable system for creating, reviewing, and deploying prompt templates with Jinja2 templating, role-based access control, and Redis-cached runtime resolution.
Overview
Instead of hardcoding system prompts in the orchestration engine, prompts are managed as versioned templates organized into namespaces. Each template goes through a controlled lifecycle:
- Draft — Authors create and edit drafts privately. One draft per user per template.
- Pre-production — Promoted drafts are immutable and can be targeted to specific users for testing.
- Active — The production version served to all users at runtime.
- Archived — Superseded versions preserved for audit and rollback.
Every lifecycle transition is recorded in an HMAC-signed, append-only audit trail.
The orchestration engine calls this service at conversation start to render the agent’s persona (when it uses a prompt_template_ref) and each loaded skill’s context_templates (R35 P3b), which system_prompt_node assembles into the base system message. These runtime calls send X-Tenant-Id (always) and X-User-Id (when a real end user is present) as headers; /resolve + /render accept identity optionally (get_optional_user). A best-effort soft-fail degrades prompt context (but never the skill’s executable surface) if the service is unreachable.
Port & Language
| Property | Value |
|---|---|
| Port | 8010 |
| Language | Python 3.12 |
| Framework | FastAPI |
| Entry point | src/agent_config/main.py |
Key Endpoints
| Method | Path | Description |
|---|---|---|
GET | /prompts/namespaces | List namespaces (filtered by user access) |
POST | /prompts/namespaces | Create a namespace (tenant admin only) |
GET/PUT | /prompts/namespaces/{id} | Get or update namespace config |
GET/POST | /prompts/namespaces/{id}/tiers | List or create budget tiers |
GET/POST | /prompts/templates | List or create prompt templates |
GET/PUT/DELETE | /prompts/templates/{id} | Template CRUD |
GET/POST | /prompts/templates/{id}/versions | List versions or create a draft |
GET | /prompts/templates/{id}/my-draft | Get current user’s draft |
PUT | /prompts/versions/{id}/save-draft | Update draft in place, re-validate |
DELETE | /prompts/versions/{id}/discard | Hard-delete a draft |
POST | /prompts/versions/{id}/promote-pre-prod | Promote draft to pre-production |
POST | /prompts/versions/{id}/promote-active | Promote pre-prod to active |
POST | /prompts/versions/{id}/approve | Approve a pending promotion |
POST | /prompts/versions/{id}/reject | Reject with reason |
POST | /prompts/versions/{id}/restore | Restore an archived version |
GET | /prompts/resolve/{namespace}:{slug} | Resolve best version (hot path) |
POST | /prompts/render/{namespace}:{slug} | Resolve + render with variables |
GET | /prompts/pending-approvals | List pending approvals for current user |
POST | /prompts/audit/verify | Verify HMAC signatures on audit entries |
GET/POST/PUT/DELETE | /prompts/namespaces/{id}/access | Namespace access control CRUD |
GET/POST | /skills | List or create skill definitions |
GET/PUT/DELETE | /skills/{id} | Skill detail / update / soft-delete. Create/update accept context_mode (reason_alongside|compute_and_return, R35 §11.4). |
GET/POST | /skills/{id}/code-blocks | List or create code blocks. Create/update accept llm_visible (R35 §11.3, default true). |
GET/PUT/DELETE | /skills/{id}/code-blocks/{block_key} | Code block CRUD |
GET/POST | /skills/{id}/personas | R35 P2 — list or create skill personas (voice/identity; exactly one of prompt_text / prompt_template_ref; at most one is_default) |
GET/PUT/DELETE | /skills/{id}/personas/{persona_key} | R35 P2 — skill persona CRUD |
GET | /health | Health check |
Architecture
Module Breakdown
src/agent_config/
├── main.py # FastAPI app, lifespan, route registration
├── config.py # Settings from environment variables
├── database.py # AsyncPG connection pool (shared PostgresPool)
├── dependencies.py # get_current_user (X-User-Id required), get_optional_user (user optional, tenant required — used by /resolve + /render), get_redis
├── seed.py # Namespace/tier/prompt seeder + migration runner
├── models/
│ ├── namespaces.py # Pydantic models for namespace CRUD
│ ├── tiers.py # Budget tier models
│ ├── templates.py # Template CRUD models
│ ├── versions.py # Version CRUD + lifecycle models
│ └── audit.py # Audit log models
├── routes/
│ ├── namespaces.py # Namespace + tier endpoints
│ ├── templates.py # Template CRUD endpoints
│ ├── versions.py # Version CRUD + lifecycle + pending approvals
│ ├── resolution.py # Runtime resolve + render (hot path)
│ ├── audit.py # Audit log queries + HMAC verification
│ └── access.py # Namespace access control
├── services/
│ ├── validation.py # 5-step save-time validation pipeline
│ ├── injection_scan.py # Prompt injection pattern detector
│ ├── lifecycle.py # Promote, approve, reject, restore, conflict
│ ├── renderer.py # Jinja2 SandboxedEnvironment with timeout
│ ├── resolution.py # Redis-cached runtime resolution
│ └── audit_service.py # HMAC signing + audit log writes
└── migrations/
└── 001_initial_schema.sqlTemplate Organization
Templates are organized in a two-level hierarchy: Namespace > Slug. Each namespace has its own budget tiers, access control, and approval settings.
| Namespace | Purpose | Auto-Approve |
|---|---|---|
agents | Agent system prompts (Rule 37, Rule 32, etc.) | No |
ontology | Knowledge graph ontology generation | Yes |
detection | Event detection rule formulas | Yes |
notifications | Notification text generation | Yes |
Version Lifecycle
Draft (private, mutable)
│
├── Promote to Pre-prod ──┬── No conflict → Pre-production
│ └── Conflict (409) → Override or Merge
│
Pre-production (immutable, targeted)
│
├── auto_approve = true → Active (immediate)
├── auto_approve = false → Pending approval
│ ├── Approver approves → Active
│ └── Approver rejects → Stays Pre-prod (author revises)
│
Active (one per template)
│
└── Superseded → Archived (restorable)Validation Pipeline
Every draft creation and save runs a 5-step validation pipeline:
- Jinja2 Syntax Check — Catches template syntax errors with line numbers
- Restricted Subset Enforcement — Blocks
macro,import,set,callconstructs and non-whitelisted filters - Variable Extraction — Detects undeclared variables, warns on unknowns
- Injection Scan — Regex-based detection of instruction override, role confusion, sandbox escape, and data exfiltration patterns
- Token Budget Check — Estimates rendered tokens using tiktoken, rejects if over budget tier limit
Conflict Resolution
When promoting to pre-production and another user’s pre-prod already exists, the API returns 409 with two options:
| Option | Behavior |
|---|---|
| Override | Archives the existing pre-prod, promotes your draft |
| Merge | Archives both, creates a new version from a merged body |
Runtime Resolution (Hot Path)
The resolve endpoint checks in order:
- Redis cache for pre-prod version targeting this user (60s TTL)
- Redis cache for active version (5-min TTL)
- Database query on cache miss
Access Control
Namespace access uses a four-level role hierarchy:
| Role | Permissions |
|---|---|
viewer | Read templates and versions |
author | Create templates, drafts, promote |
approver | Approve/reject promotions (cannot approve own versions) |
admin | Manage tiers, access control, namespace settings |
Unauthorized access returns 404 (not 403) to avoid leaking namespace existence.
Database Tables
| Table | Purpose |
|---|---|
prompt_namespaces | Namespace definitions with approval config |
prompt_budget_tiers | Token budget limits per namespace |
prompt_templates | Template metadata with active version pointer |
prompt_versions | Version bodies with status, validation, targeting |
prompt_audit_log | Append-only, HMAC-signed lifecycle events |
namespace_access_control | Role-based access per namespace per user |
skill_definitions | Skill metadata (domain tags, context_templates, required_capabilities, context_mode) |
skill_code_blocks | Sandboxed code blocks per skill (input_binding, llm_visible, mode) |
skill_personas | R35 P2 — a skill’s voice/identity personas (one default per skill) |
agent_definitions | R35 P1 — agent → root skill + persona + model config (read by the loader from R35 P3a) |
Key constraints:
- One draft per user per template (partial unique index on
status = 'draft') - One pre-prod per template (partial unique index)
- One active per template (partial unique index)
- Audit log is append-only (triggers reject UPDATE/DELETE)
- One default persona per skill (
idx_one_default_persona_per_skill, partial unique onis_default); each persona sets exactly one ofprompt_text/prompt_template_ref(app-enforced)
Dependencies
Python Packages
| Package | Version | Purpose |
|---|---|---|
fastapi | ^0.115 | Web framework |
uvicorn | ^0.34 | ASGI server |
asyncpg | ^0.29 | PostgreSQL async driver |
redis | ^5.0 | Redis client for cache |
jinja2 | ^3.1 | Template rendering (SandboxedEnvironment) |
tiktoken | ^0.7 | Token estimation (cl100k_base encoding) |
aegis-shared | local | Shared DB helpers (PostgresPool) |
Infrastructure Dependencies
| Dependency | Purpose |
|---|---|
| PostgreSQL 15 | Template/version storage, audit trail |
| Redis 7 | Runtime resolution cache |
Configuration
| Environment Variable | Default | Description |
|---|---|---|
AGENT_CONFIG_HOST | 0.0.0.0 | Bind address |
AGENT_CONFIG_PORT | 8010 | Bind port |
DATABASE_URL | postgresql://aegis:aegis_local@localhost:5432/aegis | PostgreSQL connection |
REDIS_URL | redis://localhost:6379 | Redis connection |
HMAC_SIGNING_KEY | aegis-local-hmac-key-change-in-production | Key for audit trail HMAC signatures |
DEFAULT_TENANT_ID | 00000000-0000-0000-0000-000000000001 | Default tenant for local dev |
The HMAC_SIGNING_KEY must be kept secret and consistent. Changing the key invalidates all existing audit log signatures. Use HashiCorp Vault in production.
Running Locally
cd services/agent-config-service
poetry install
poetry run uvicorn agent_config.main:app --reload --port 8010Seed Data
Run the seeder to create default namespaces, budget tiers, and migrate agent prompts from the orchestration engine:
cd services/agent-config-service
poetry run python -m agent_config.seedThis creates:
- 4 namespaces (agents, ontology, detection, notifications)
- 5 budget tiers
- 4 agent system prompt templates with active version 1
The service lifespan additionally self-applies its numbered SQL migrations
(001–009) and runs idempotent seeders on every boot, including the R35 P2
skill seeders: seed_rrc_prompts (the skills namespace + two regulatory-context
templates), seed_rrc_skills (rrc_rule37 + rrc_rule32 with 8 sandboxed code
blocks + a default persona each), and seed_rrc_skill_rules (3 HITL require_hitl
skill rules, register-only until R35 P5). These are additive — nothing on the live
agent path reads them until R35 P3a.
Gateway Access
All endpoints are proxied through the API gateway at http://localhost:8000/api/v1/prompts/*.
Integration with Orchestration Engine
The orchestration engine’s system_prompt_node calls this service at conversation start:
result = await render_prompt(namespace="agents", slug=slug, variables={}, user_id=user_id)If the service is unreachable, it falls back to hardcoded prompts in fallback_prompts.py. The resolved prompt_version_id is stored in the conversation state for audit trail purposes.