Approval Service
The Approval Service implements the Human-in-the-Loop (HITL) approval system for AEGIS. It manages approval requests created by the orchestration engine at mandatory checkpoints, records reviewer decisions, handles escalation for timed-out requests, and writes HMAC-signed entries to the append-only audit trail.
Overview
All regulatory filings produced by AEGIS agents must be reviewed and approved by a human before submission. The approval service provides the backend for this workflow:
- The orchestration engine detects a HITL checkpoint (e.g.,
pre_filing,good_cause_review) - It creates an approval request via this service, including a snapshot of the current execution state
- The approval sits in
pendingstatus until a reviewer decides (approve, reject, or modify) - Once decided, the orchestration engine can resume execution
The service also implements time-based escalation: if a pending approval exceeds the configured timeout (default 24 hours), it can be reassigned to a fallback reviewer.
Every action (creation, decision, escalation) is recorded in the HMAC-signed, append-only audit trail.
Port & Language
| Property | Value |
|---|---|
| Port | 8004 |
| Language | Python 3.12 |
| Framework | FastAPI |
| Entry point | src/approval/main.py |
Key Endpoints
| Method | Path | Description |
|---|---|---|
POST | /approvals | Create a new approval request for a HITL checkpoint. |
GET | /approvals | List approval requests with optional filters (status, reviewer_id, agent_id). Supports pagination. |
GET | /approvals/{approval_id} | Get a single approval request with full state snapshot. |
POST | /approvals/{approval_id}/decide | Record a decision: approved, rejected, or modified. |
POST | /approvals/escalate | Check for timed-out approvals and optionally reassign to a fallback reviewer. |
GET | /health | Health check. |
Architecture
Module Breakdown
src/approval/
├── main.py # FastAPI app, all endpoint definitions
├── config.py # Settings from environment variables
├── schemas.py # Pydantic request/response models
├── repository.py # PostgreSQL CRUD for approval_requests table
├── audit.py # HMAC-signed audit trail writer
└── seed_filings.py # Demo filing data seederApproval Lifecycle
Created (pending)
│
├── Reviewer approves → status: "approved"
├── Reviewer rejects → status: "rejected"
├── Reviewer modifies → status: "modified" (modified_output stored in snapshot)
│
└── Timeout exceeded → Escalated (reassigned to fallback reviewer, stays "pending")Approval Request Fields
Each approval request stores:
| Field | Description |
|---|---|
id | UUID of the approval request |
execution_id | The orchestration execution that triggered this checkpoint |
agent_id | Which agent created this request |
checkpoint_type | The type of checkpoint (e.g., pre_filing, good_cause_review, technical_review) |
state_snapshot | Full JSON snapshot of the execution state at the time of the checkpoint |
reviewer_strategy | Either named_individual or role_based |
reviewer_id | The specific reviewer assigned (for named_individual strategy) |
status | pending, approved, rejected, or modified |
decision | The recorded decision |
reviewer_comments | Free-text comments from the reviewer |
decided_at | Timestamp of the decision |
Decision Types
| Decision | Behavior |
|---|---|
approved | The filing package is approved as-is. The orchestration engine can proceed. |
rejected | The filing is rejected. The reviewer should provide comments explaining why. |
modified | The reviewer has edited the output. The modified_output dict is stored in the state snapshot for the orchestration engine to use on resume. |
Reviewer Assignment Strategies
| Strategy | Description |
|---|---|
named_individual | A specific reviewer_id is assigned. This reviewer must decide. |
role_based | Any user with the appropriate role can decide. The approval is visible to all eligible reviewers. |
Escalation Logic
The /approvals/escalate endpoint finds all pending approvals whose created_at timestamp exceeds the configured timeout:
SELECT * FROM approval_requests
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '1 second' * $1If a fallback_reviewer_id is provided, timed-out approvals are reassigned to that reviewer. Each escalation is recorded in the audit trail.
In production, the escalation endpoint would be called by a cron job or scheduler. In development, you can call it manually to test the escalation flow.
Audit Trail (audit.py)
Every approval action writes a signed entry to the audit_logs table:
- Payload construction: The event data dict is JSON-serialized with sorted keys
- HMAC signing: A SHA-256 HMAC signature is computed using the
HMAC_SIGNING_KEY - Insert: The entry is inserted into
audit_logswith the signature
signature = hmac.new(
settings.HMAC_SIGNING_KEY.encode(),
json.dumps(event_data, sort_keys=True).encode(),
hashlib.sha256,
).hexdigest()The audit_logs table has database triggers that prevent UPDATE and DELETE operations, ensuring the trail is truly append-only.
Audit event types:
| Event Type | Trigger |
|---|---|
approval.created | New approval request created |
approval.approved | Reviewer approved the request |
approval.rejected | Reviewer rejected the request |
approval.modified | Reviewer modified and approved |
approval.escalated | Approval timed out and was escalated |
Dependencies
Python Packages
| Package | Version | Purpose |
|---|---|---|
fastapi | ^0.115 | Web framework |
uvicorn | ^0.34 | ASGI server |
asyncpg | ^0.29 | PostgreSQL async driver |
aegis-shared | local | Shared DB helpers (PostgresPool) |
Infrastructure Dependencies
| Dependency | Purpose |
|---|---|
| PostgreSQL 15 | Approval requests table, audit trail |
Configuration
| Environment Variable | Default | Description |
|---|---|---|
APPROVAL_HOST | 0.0.0.0 | Bind address |
APPROVAL_PORT | 8004 | Bind port |
DATABASE_URL | postgresql://aegis:aegis_local@localhost:5432/aegis | PostgreSQL connection |
HMAC_SIGNING_KEY | aegis-local-hmac-key-change-in-production | Key for HMAC-SHA256 audit signatures |
ESCALATION_TIMEOUT_SECONDS | 86400 | Timeout before escalation (24 hours) |
DB_SCHEMA | public | Database schema prefix for tables |
The HMAC_SIGNING_KEY must be kept secret and consistent across deployments. Changing the key will make it impossible to verify the signatures of existing audit log entries. In production, this should be sourced from HashiCorp Vault.
Running Locally
cd services/approval-service
poetry install
poetry run uvicorn approval.main:app --reload --port 8004Requires PostgreSQL to be running with the approval_requests and audit_logs tables created (handled by infrastructure/docker/postgres/init.sql).