Agent Config Service
The Agent Config Service is the runtime source of truth for agent behavior in AEGIS: skill definitions, skill routers, rule definitions, prompt templates, platform settings, agent definitions, and entity-type definitions all live here as tenant-configurable data, not code. This is the service that makes horizontalization possible — an agent’s skills, routers, rules, prompts, and LLM settings are edited and versioned through this API (and the /configuration admin UI), never by shipping orchestration-engine code. It provides a centralized, auditable system with Jinja2 templating, role-based access control, an HMAC-signed append-only audit trail, 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 (
target_users) and/or platform roles (target_roles, R42e) 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. Accepts target_users: string[] and (R42e) target_roles: string[] — the version is served when the caller is pinned in target_users OR holds a role overlapping target_roles; the /override and /merge conflict resolvers accept both fields too |
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. R38 — create accepts optional as_draft (default false): when true, the skill is created with review_status='draft' regardless of caller role (admin included) — the multi-step wizard path. Drafts are stomp-proof (subsequent writes don’t change status); POST /skills/{id}/submit is the sole exit |
GET/PUT/DELETE | /skills/{id} | Skill detail / update / soft-delete. Create/update accept context_mode (reason_alongside|compute_and_return, R35 §11.4), response_mode (llm_synthesis|verbatim, migration 019 — verbatim = the platform renders the skill’s tool results to the user verbatim as the turn’s primary response; the LLM is instructed not to restate them) and version_check_on_turn (R36 — compliance-grade skills check their loaded version every turn). Create/update responses include a warnings list (e.g. an active non-dependency skill with no approved router mapping is unreachable via selection — warn, never block). |
POST | /skills/{id}/submit | R38a — the ONLY exit from draft: draft → pending_review. Author-or-admin (ownership-derived); any other current status → 409 with current_status in the body. Pairs with the reopen_review draft-guard: a draft survives any number of writes until explicitly submitted |
POST | /skills/{id}/review | Approve/reject a skill under review (admin, R37a). R38 — approve accepts optional router_id (UUID): if the skill has no approved mapping under an active router, the approval transaction also creates an approved skill_router_map entry under that router and (if none exists) an approved skill_description_versions row from the skill’s description column (falls back to display_name) — “publish-on-approve”, making the skill reachable via R36 selection. Idempotent (approving an already-mapped skill is a no-op on the mapping); unknown router_id → 404 and the approval rolls back; ignored on reject and on the revision-promote path. Approve responses include unmapped: boolean — true when the skill ends the review unreachable via selection |
POST | /skills/{id}/revisions | R38a — clone an approved, active skill into a private draft revision (the GitHub-clone model). Deep-copies code blocks, personas, outgoing dependencies, and skill-sourced advisory rules (rule keys copy byte-identically — rule uniqueness is per-skill since migration 015); owner_id = the cloner, parent_skill_id = the source row, same skill_key (legal under the partial approved+active unique index). Admin approve on a revision = atomic PROMOTE: old row → is_active=false + superseded_by_skill_id, revision → approved, router mappings + description history remap, surviving sibling revisions reparent. A superseded row can never be re-activated (409 skill_superseded) |
GET | /skills/{id}/revisions | R38a — list revisions cloned from this row (newest first) |
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 | /skill-routers | R36 — list skill routers |
POST | /skill-routers | R36 — create a router (admin) |
PUT | /skill-routers/{id} | R36 — update a router (admin). Retirement = is_active=false; there is no DELETE route — routers are soft-deactivated only, so the manifest-delta watermark cannot miss a change |
GET | /skill-routers/manifest | R36 — derived manifest read (router_key + recruitment_blurb per active router) — what system_prompt_node renders into general-mode prompts |
GET | /skill-routers/disambiguation?router_keys=... | R36 — derived disambiguation payload: the pulled routers’ approved skills with latest-approved descriptions (backs pull_routers) |
GET | /skill-routers/unrouted-skills | R36 — active non-dependency skills with no approved router mapping (unreachable via selection) |
GET | /skill-routers/{id}/contents | R36 — router-contents admin view |
GET/POST | /skill-routers/{id}/mappings | R36 — list / create router↔skill mappings (POST admin). Mappings carry a promotion status (draft/pending_review/approved/rejected); admin submissions auto-approve; only approved mappings render |
DELETE | /skill-routers/{id}/mappings/{skill_id} | R36 — remove a mapping (admin) |
GET | /skills/{skill_id}/router-mappings | R38 — skill-side mapping view (any authenticated user, read-only): the skill’s skill_router_map rows enriched with router_key and router_is_active. 404 if the skill doesn’t exist; empty list if unmapped. Backs the ReviewDialog’s decision on whether to offer a router at approval |
GET/POST | /skills/{skill_id}/description-versions | R36 — list / submit disambiguation description versions (POST admin; append-only, latest approved renders) |
GET | /rules | List rules (filters: trigger, source_level, source_id, is_active, and R37d enforcement ∈ advisory|mandatory) |
GET | /rules/effective?skill_id=... | R37d — a skill’s effective rule set: {own, mandatory}. own = its source_level=skill rules; mandatory = every mandatory floor rule whose applies_to selector matches, each with a provenance (matched_via ∈ category|domain_tag|skill + detail). Non-admin readable (feeds the skill editor RulesTab on both surfaces). R37e adds lint[] — review-time warnings for flow-changing/state-mutating own rules on a skill carrying a mandatory gate ({rule_key, code, message, mandatory_rule_keys}); rendered in the ReviewDialog and RulesTab |
POST/PUT/DELETE | /skills/{skill_id}/rules, /skills/{skill_id}/rules/{rule_id} | R37e — the author (advisory) rule write path, skill-nested. Open to the skill’s owner or an admin; the server derives source_level='skill' + source_id from the path and forces enforcement='advisory' + empty applies_to for every caller (explicit 400 on mandatory/selector/foreign-source attempts; 403 for a non-owner; mandatory rules are untouchable here even when homed on the skill). A non-admin write calls reopen_review(skill) — the R37a load gate then keeps the skill (and its rules) dormant until an admin re-approves. Write responses carry the lint warnings and (R38) the same namespace-lint warnings[] as /rules/validate |
POST | /rules/validate | R37e — non-admin condition validation (stateless parse-only) → {valid, error, ast}. Backs the author surface’s live validation. R38 — accepts optional trigger (one of the 8 rule triggers) and the response gains warnings[] (each {code, field, message}): a namespace lint flagging field references whose root namespace the evaluator can never resolve (warn-never-block — such conditions silently never match). With trigger, checks against that trigger’s exact namespace table; without it, only namespaces no trigger populates are flagged. Codes: namespace_trigger_mismatch, unknown_namespace |
POST/PUT/DELETE | /admin/rules, /admin/rules/{id} | Rule CRUD (admin). Create/update accept R37d enforcement + applies_to ({category_ids[], domain_tags[], skill_ids[]}); a mandatory rule with an empty selector is rejected |
POST | /admin/rules/validate | Validate a condition_text expression without persisting → {valid, error, ast} (admin twin of /rules/validate; R38 — same optional trigger + warnings[] namespace lint) |
POST | /admin/rules/match-preview | R37d — blast radius of an applies_to selector → {count, skills[]} (each matched_via). Resolved by the same selector helper the runtime fan-out uses, so preview and enforcement agree |
POST | /rules/evaluate | Internal (service-to-service, not gateway-routed) — the orchestration engine’s per-trigger rule evaluation. Mandatory rules scope by applies_to against the loaded skills and sort before advisory rules (un-bypassable precedence) |
GET | /settings | Tenant platform settings (LLM runtime config): stored tenant values + effective values + a sources map (tenant|default) + the calling user’s user_override if any. Any authenticated identity — the orchestration engine calls it at every turn start with X-Tenant-Id/X-User-Id headers |
PUT | /settings | Admin-only upsert of tenant settings. Nullable fields clear back to inherit; models are validated against the curated list; writes an HMAC-signed audit row (platform_settings.update) and DELs the settings:{tenant_id} Redis cache |
GET | /settings/model-options | Curated model picker for the settings UI (LiteLLM model ids) |
GET | /settings/user-overrides | Admin — list per-user daily budget overrides |
PUT | /settings/user-overrides/{user_id} | Admin — upsert a per-user override (at least one of max_tokens_per_day / max_cost_usd_per_day); audit action platform_settings.user_override.upsert |
DELETE | /settings/user-overrides/{user_id} | Admin — remove a per-user override (204); audit action platform_settings.user_override.delete |
GET | /health | Health check |
Authoring Authorization (R42d)
Authoring writes are permission-gated via aegis_shared.auth (a deliberate
change from R37’s open-authoring posture — authoring is now a granted
capability per the R42 Appendix C sign-off):
| Write path | Required permission |
|---|---|
POST /skills, code-block writes, POST /skills/{id}/submit, POST /skills/{id}/revisions, skill-rule writes | skills.propose (held by operator, power_user, admin — the operator escape hatch: propose a draft, an author/admin takes it from there) |
PUT /skills/{id} | skills.author (power_user, admin) |
| Prompt template and version writes | prompts.author (power_user, admin) |
Ownership and review_status governance (draft → pending_review → approved)
are unchanged and sit on top of these gates. Checks are mode-gated by
AEGIS_AUTH_MODE (warn-and-pass in log, 403 in enforce) — see
Auth Flow — Service-Side Enforcement.
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 # Binds the STRICT aegis_shared.auth variant (R42d): get_current_user = get_strict_user, so a header-less request 401s in BOTH auth modes (stamped-empty-perms = revocation); plus get_optional_user, require_permission, 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
│ └── routers.py # R36: skill-router CRUD, manifest/disambiguation reads, description versions
├── 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.sql
├── ...
├── 010_skill_routers.sql # R36: skill_routers, skill_router_map,
│ # skill_description_versions, version_check_on_turn
├── 016_platform_settings.sql # platform_settings, user_budget_overrides,
│ # prompt_audit_log.action widened to VARCHAR(64)
├── 020_identity_normalization.sql # R42e: uuid retypes on identity columns,
│ # sentinel → real-admin skill ownership remap
└── 021_prompt_target_roles.sql # R42e: prompt_versions.target_roles TEXT[]Template 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 a pre-prod version targeting this caller (60s TTL). Targeting matches
target_users(pinned user) ORtarget_rolesrole overlap (R42e); the cache key carries a sorted-roles component so a role-based match caches per role set (the…:preprod:*invalidation SCAN pattern still covers it) - 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.
Platform Settings (Tenant LLM Runtime Config)
The /settings* endpoints back the admin Platform Settings page
(/configuration/platform-settings in the frontend) — tenant-level LLM runtime
configuration that replaces editing env vars on the box. Nine nullable fields:
| Field | Governs |
|---|---|
default_model | Primary LLM model for all agent turns |
fallback_model | One retry on primary-model errors (all LLM call sites) |
max_tokens_per_execution / max_cost_usd_per_execution | Per-execution budget |
max_output_tokens_per_call | max_tokens ceiling for a single LLM call (thinking + visible output; validated 8k–128k — below ~8k, Sonnet 5’s adaptive thinking can consume the whole ceiling and return an empty message) |
thinking_mode | adaptive | disabled — sent explicitly to Anthropic models (omitted for models that don’t accept it) |
max_tokens_per_day / max_cost_usd_per_day | Tenant-aggregate daily caps |
tenant_branding | E0.3 — free-text label shown in the sidebar footer for every user (max 200 chars; blank/whitespace normalizes to NULL = no footer line). Unlike the LLM knobs it has no env fallback tier: resolution is tenant setting → NULL (neutral chrome). The sidebar reads it via the non-admin GET /settings path |
NULL means inherit: the effective value falls back to the orchestration
engine’s env defaults (DEFAULT_LLM_MODEL, DEFAULT_MAX_TOKENS_PER_EXECUTION,
DEFAULT_MAX_COST_PER_EXECUTION, DEFAULT_MAX_OUTPUT_TOKENS_PER_CALL,
DEFAULT_THINKING_MODE), which remain the fallback floor. GET /settings
returns the stored values alongside the resolved effective values and a sources
map (tenant | default) so the UI can show where each value comes from. Model
choices are validated against the curated GET /settings/model-options list.
Reads are Redis-cached at settings:{tenant_id} (TTL 300s, DEL on write). Every
write lands an HMAC-signed row in prompt_audit_log (platform_settings.update,
platform_settings.user_override.upsert, platform_settings.user_override.delete;
migration 016 widens action to accommodate these).
Per-user daily budget overrides (/settings/user-overrides*) cap individual users;
the tenant daily caps remain the aggregate ceiling. The settings page’s user picker
is fed by the auth service’s admin-only GET /auth/users directory.
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 (target_users + target_roles since R42e; author_id/draft_owner_id/approved_by are uuid since migration 020) |
prompt_audit_log | Append-only, HMAC-signed lifecycle events. actor_id stays TEXT permanently (R42e — rows are never rewritten); the audit-log list endpoints resolve actor_resolved (the actor’s email, via identity_map) at read time |
namespace_access_control | Role-based access per namespace per user |
skill_definitions | Skill metadata (domain tags, context_templates, required_capabilities, context_mode, response_mode — verbatim output contract, R36 version_check_on_turn) |
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) |
skill_routers | R36 — tenant-scoped, admin-curated recruitment taxonomy (router_key, recruitment_blurb); soft-deactivate only, no hard delete |
skill_router_map | R36 — router↔skill mappings with promotion status (draft/pending_review/approved/rejected; one row per pair, multi-homing = multiple pairs) |
skill_description_versions | R36 — append-only disambiguation descriptions per skill; the latest approved version renders in selection payloads |
rule_definitions | Condition→action rules (trigger, condition/condition_text, action_capability, source_level, priority). R37d adds enforcement (advisory|mandatory, default advisory) + applies_to JSONB selector ({category_ids[], domain_tags[], skill_ids[]}) — the admin-owned compliance floor. Migration 013 is the sole owner of the enforcement CHECK (the 012 single-owner lesson) |
platform_settings | Tenant-level runtime config (migrations 016/017/018): tenant_id PK + the nullable value columns (NULL = inherit the env default; tenant_branding from 018 has no env tier — NULL = no branding) + updated_by and timestamps |
user_budget_overrides | Per-user daily budget overrides (migration 016): (tenant_id, user_id) PK, max_tokens_per_day, max_cost_usd_per_day, updated_by, timestamps |
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–021) and runs idempotent seeders on every boot, including the R35 P2
skill seeders: seed_rrc_prompts (the skills namespace + three regulatory-context
templates), seed_rrc_skills (rrc_rule37 + rrc_rule32 + the R35-followup
flaring_watch monitoring skill — 10 sandboxed code blocks + a default persona
each; the flaring_watch blocks share the rrc_rule32 code constants under
distinct block keys), and seed_rrc_skill_rules (3 HITL require_hitl
skill rules, live since R35 P5 — flaring_watch is read-only and carries none).
R36 adds seed_skill_routers: two approved routers, spacing → rrc_rule37
and flaring → rrc_rule32 + flaring_watch (the watch/file pair), with
version_check_on_turn = true set on the two filing skills. Description versions
are append-only; the seed may supersede its own (submitted_by IS NULL) text but
never an admin edit. The legacy rule37-spacing registry skill is intentionally
left unmapped. R37d adds seed_compliance_floor
(runs last): a platform “AEGIS Compliance” catalog + “Compliance” category, files
rrc_rule37 + rrc_rule32 under it (only when uncategorized — an admin re-filing
survives reboot), and seeds one mandatory require_hitl rule scoped to the
Compliance category that fires on either skill’s {rule}_filing_assembly step — the
compliance floor an author cannot remove. Its reviewer is a soft reference
(reviewer_role="compliance_reviewer") that R40’s org hierarchy will resolve.
R38 adds seed_creator (after the RRC seed block): one function seeds the
platform Skill Creator agent — skill → persona → agent row, in that order (an
active agent_definitions row with an unresolvable root_skill_key fails
orchestration boot). The skill_creator skill has zero code blocks (its tools
are native orchestration handlers, not sandboxed blocks), required_capabilities=[],
domain_tags=['authoring'], no catalog category (the compliance floor’s category
axis must not fire on the Creator’s own writes), review_status='approved', and
is_system=true. Its single default persona (creator_default) embeds the
rule-grammar and seed-code contracts, the two-pattern code library, the five-ask
wizard dialogue, the fix-loop budgets, and the authoring scope guard in
prompt_text — and the persona seed UPSERTS prompt_text on every boot, so
prompt revisions propagate to deployed boxes (unlike the get-or-create RRC
personas). The agent row is agent_key='creator', root_skill_key='skill_creator'.
Seeded agent_definitions rows no longer pin model_config.model_preference
(now null), so the tenant’s Platform Settings default_model governs; an
explicitly set per-agent model_preference still overrides it.
R42e — seeds write real identities: prompt seeds are authored by
system@aegis.local (the nil UUID), and seeded skills are owned by the
earliest active admin, looked up at runtime (agent_config/identity.py) —
if no active admin exists yet, the ownership stamp is skipped fail-closed rather
than inventing an id. The pre-R42 admin-author sentinel UUID is retired
(migration 020 remaps rows it owned to the real admin).
Gateway Access
All endpoints are proxied through the API gateway — the prompt routes at http://localhost:8000/api/v1/prompts/* and the platform-settings routes at http://localhost:8000/api/v1/settings*.
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, the render soft-fails and prompt context degrades (the hardcoded fallback_prompts.py registry was deleted in R35 P6 — the platform default persona lives in orchestration/persona.py). The resolved prompt_version_id is stored in the conversation state for audit trail purposes.
R36: the orchestration engine additionally reads this service’s router layer — GET /skill-routers/manifest at conversation birth (general mode, rendered into the sealed index-0 prompt block) and GET /skill-routers/disambiguation when the LLM calls pull_routers. Both are derived reads; no compiled router block is materialized anywhere.
Platform Settings: at every turn start (both the synchronous /execute path and the SSE stream) the orchestration engine calls GET /settings with X-Tenant-Id/X-User-Id headers and resolves each value as request override → tenant setting → env default. A fetch failure degrades to the env defaults. Workspace assessment skills and the scoped agent go through the same resolution. See Orchestration Engine for the daily-cap counters.