AIControl Docs

API Reference

Base URL: http://your-aicontrol-host:8001

Interactive docs (Swagger UI): http://your-aicontrol-host:8001/docs


Authentication

AIControl uses two token types:

Machine API tokens — Issued by POST /tokens or scripts/issue_token.py. Used by AI agents and automation scripts. Stored as SHA-256 hashes; never recoverable after issuance.

Human JWTs — Issued by POST /auth/login after password authentication. Used by dashboard users and the React frontend. 8-hour expiry. Not stored in the database — validated by signature only.

Pass either type as a Bearer header:

Authorization: Bearer <token>

Endpoint access by token type:

RoleToken typeCan call
agent (agent-scoped)MachinePOST /mcp/{server_id}/tools/list, POST /mcp/{server_id}/call_tool, GET /reviews/{id}
admin (machine)MachineAll management endpoints
admin (human)Human JWTDashboard endpoints (require_human or require_admin)

Setup

GET /setup/status

Returns whether first-run setup is required. Use this to decide whether to redirect to /setup on the frontend.

Auth: none required

Response 200:

json
{ "setup_required": true }

setup_required: false once any user exists in the database.


POST /setup/complete

Create the root administrator account and organisation on first run. Returns 409 if any user already exists.

Auth: none required

Request:

json
{
  "org_name": "Acme Corp",
  "timezone": "America/New_York",
  "full_name": "Alice Smith",
  "email": "alice@acme.com",
  "password": "securepassword"
}

timezone must be a valid IANA timezone string (e.g. "UTC", "Europe/London"). password minimum 8 characters.

Response 200:

json
{
  "token": "eyJhbGc...",
  "first_login": true
}

Auth

POST /auth/login

Authenticate with email and password. Returns a human JWT valid for 8 hours.

Auth: none required

Request:

json
{
  "email": "alice@acme.com",
  "password": "securepassword"
}

Response 200:

json
{
  "token": "eyJhbGc...",
  "user": {
    "id": "uuid",
    "email": "alice@acme.com",
    "name": "Alice Smith",
    "role": "admin"
  }
}

Returns 401 for invalid credentials. Returns 403 for deactivated accounts.


POST /auth/magic-link/validate

Validate a magic link token (used in the invite and password reset flows). Does not issue a JWT — call POST /auth/set-password after this to complete the flow.

Auth: none required

Request:

json
{ "token": "plaintext_invite_token_from_url" }

Response 200:

json
{
  "name": "Bob Jones",
  "email": "bob@acme.com"
}

Returns 401 for expired or invalid tokens.


POST /auth/set-password

Set password using a valid magic link token. Issues a human JWT on success.

Auth: none required

Request:

json
{
  "token": "plaintext_invite_token_from_url",
  "password": "newpassword"
}

Response 200:

json
{
  "token": "eyJhbGc...",
  "user": { "id": "uuid", "email": "bob@acme.com", "name": "Bob Jones", "role": "viewer" }
}

MCP Servers

Admin-only CRUD for registering downstream MCP servers with the gateway. No credential field exists anywhere in this API — registration is metadata-only (Decision 9 of the gateway pivot: pure passthrough, no stored secret).

POST /mcp-servers

Register a downstream MCP server.

Auth: admin

Request:

json
{
  "name": "invoicing-mcp",
  "base_url": "https://invoicing.internal/mcp",
  "approved_tools": ["get_invoice", "create_invoice"]
}

approved_tools is optional, defaults to [] (meaning every tool the downstream server exposes is reachable through this registration, subject to policy and the agent's own approved-tools list).

Response 201:

json
{
  "id": "3f2a1c8b-...",
  "name": "invoicing-mcp",
  "base_url": "https://invoicing.internal/mcp",
  "status": "pending_review",
  "approved_tools": ["get_invoice", "create_invoice"],
  "created_at": "2026-09-09T10:00:00Z"
}

New registrations always start in status: "pending_review".


GET /mcp-servers

List all registered MCP servers, newest first.

Auth: admin

Response 200: array of server objects (same shape as the POST response).


GET /mcp-servers/

Get one registered server.

Auth: admin

Response: 200, or 404.


PATCH /mcp-servers/

Update a server's status and/or approved_tools. All fields optional.

Auth: admin

Request:

json
{ "status": "active" }

status must be one of pending_review, active, or blocked — any other value is rejected. Only active servers accept gateway traffic; a non-active server's gateway calls return 403.

Response: 200 with updated server, or 404.


DELETE /mcp-servers/

Delete a server registration.

Auth: admin

Response: 204, or 404.


MCP Gateway

The core integration point. Agents point their MCP client at these endpoints instead of the real downstream server. Both require an agent-scoped token — an admin token, or an agent-role token issued without an agent_id, is rejected with 403.

POST /mcp//tools/list

Proxies tools/list to the registered downstream server, filtered to the server registration's approved_tools if non-empty.

Auth: agent-scoped token

Request:

json
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }

Response 200: the downstream server's own tools/list response (optionally filtered), or a JSON-RPC error if the server is unreachable:

json
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": "Downstream MCP server unreachable"}}

Returns 403 if the server's status isn't active, or 404 if server_id doesn't match a registered server.


POST /mcp//call_tool

The core interception point. Evaluates Cedar policy on the requested tool call, audits the result, and — if allowed — forwards to the real downstream server and scans the response before returning it.

Auth: agent-scoped token

Request:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "params": {
    "name": "query_credit_bureau",
    "arguments": {"applicant_id": "A-10042", "report_type": "full"},
    "session_id": "b3e1f9a2-...",
    "sequence_number": 3,
    "workflow": "loan_application_review",
    "input_tokens": 812,
    "output_tokens": 143,
    "cost_usd": 0.0091
  }
}

session_id and sequence_number are required — a missing or invalid value returns a -32602 JSON-RPC error, not an HTTP 422. workflow defaults to "unassigned". input_tokens/output_tokens/cost_usd are optional and only matter to token_budget policy conditions scoped to this tool.

Response 200 (allowed, forwarded): the downstream server's own tools/call response, unchanged.

Response 200 (denied by approved-tools gate, Cedar policy, review, or response scan): a JSON-RPC result with isError: true — never an HTTP error code:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {"content": [{"type": "text", "text": "Denied by policy: policy_matched:block_dangerous_tools"}], "isError": true}
}

Response (malformed request): -32602 JSON-RPC error for missing/invalid session_id or sequence_number.

Response (downstream unreachable): -32000 JSON-RPC error, same shape as tools/list.

Returns 403 if the server's status isn't active or if the token isn't agent-scoped, or 404 if server_id doesn't match a registered server.


Agents

GET /agents

List all registered agents with enriched metadata.

Auth: admin

Response 200: array of agent objects including last_active, deny_rate, approved_at, created_at, system_prompt_hash, plus coverage-tracking fields: framework, hook, sdk_version, workflow, coverage_last_seen_at, coverage_state (unknown | governed | installed_not_firing), silent_noop_warnings (array), unresolved_systems (array). These fields are populated by the adapter's own install-time handshake to POST /agents/{agent_id}/coverage (see below) — the handshake distinguishes "agent never ran" from "agent ran but the hook never fired," which traffic alone can't tell apart. The call goes only to the customer's own self-hosted API; nothing leaves their infrastructure.


GET /agents/

Get one agent.

Auth: admin

Response: 200 with agent object (same coverage fields as GET /agents), or 404.


POST /agents//coverage

Install-time handshake — called by the SDK adapter itself, not by a human. Records which framework/hook/SDK version is instrumenting this agent and which workflow it's reporting.

Auth: agent token (must match agent_id, or be unscoped)

Request:

json
{
  "framework": "openai_agents",
  "hook": "on_tool_start",
  "sdk_version": "0.4.0",
  "workflow": "loan_application_review",
  "agent_name": "loan-underwriting-agent",
  "silent_noop_warnings": []
}

An unrecognized agent_id is auto-registered in observe mode rather than rejected — a live, ungoverned agent nobody registered is exactly what this handshake exists to surface, and 404ing would leave it invisible instead.

Response 200: { "agent_id": "...", "coverage_state": "governed" }


POST /agents

Register a new agent.

Auth: admin

Request:

json
{
  "name": "loan-underwriting-agent",
  "owner": "platform-team@company.com",
  "framework": "langchain",
  "model_version": "gpt-4o",
  "approved_tools": ["query_credit_bureau", "read_loan_application"],
  "system_prompt_hash": "sha256hex",
  "governance_mode": "observe",
  "metadata": {}
}

All fields except name and owner are optional. governance_mode is "govern" (default — policy decisions are enforced) or "observe" (decisions are recorded exactly as if enforced, but the gateway never actually blocks a call for this agent — useful for rolling out a new policy set risk-free).

Response 201: agent object with generated id.


PUT /agents/

Update an agent. governance_mode can be changed here too ("govern" or "observe").

Auth: admin

Response: 200 with updated agent, or 404.


DELETE /agents/

Delete an agent record.

Auth: admin

Response: 204, or 404.


DELETE /agents//token

Revoke all active tokens for this agent.

Auth: admin

Response 200: { "revoked": 2 }


PATCH /agents//approved-tools

Replace the agent's approved tools list. The gateway enforces this before policy evaluation runs at all — any tool absent from this list is denied immediately with reason: tool_not_approved_for_agent.

Auth: admin

Request: { "approved_tools": ["tool_a", "tool_b"] }

Pass [] to remove all per-tool restrictions for that agent.

Response 200: { "agent_id": "...", "approved_tools": ["tool_a", "tool_b"] }


Tokens

GET /tokens

List all issued API tokens. The raw token value is never returned.

Auth: admin (human JWT)

Query parameters: active_only (boolean, default true)

Response 200: array of { id, role, description, agent_id, agent_name, revoked, created_at }


POST /tokens

Issue a new API token. If agent_id is provided, any prior active token for that agent is revoked automatically.

Auth: admin

Request:

json
{
  "role": "agent",
  "description": "loan-underwriting-agent prod token",
  "agent_id": "7c4d2e8f-..."
}

Response 200:

json
{
  "token_id": "a3b1c9d2-...",
  "role": "agent",
  "description": "loan-underwriting-agent prod token",
  "agent_id": "7c4d2e8f-...",
  "token": "eyJhbGc..."
}

The token value is shown once. Save it immediately.


Policies

GET /policies

List all policies ordered by priority.

Auth: admin

Response 200: array of policy objects. Scope fields carry a camelCase alias: principalType, principalId, actionTool, resourceSystem, alongside effect, cedar_text, condition, library, priority, category.


GET /policies/library

List all library template policies (library=true). These are inactive templates, not active rules.

Auth: admin

Response 200: array of policy objects grouped by category.


GET /policies/

Get one policy.

Auth: admin

Response: 200, or 404.


POST /policies

Create a policy. Compiles to Cedar and takes effect on the very next call_tool request through the gateway — no restart, no bundle push.

Auth: admin

Request:

json
{
  "name": "block_large_disbursements",
  "description": "Block loan disbursements above $10,000",
  "principal_type": null,
  "principal_id": null,
  "action_tool": null,
  "resource_system": null,
  "effect": "deny",
  "condition": {
    "tool_name_in": ["initiate_transfer", "disburse_loan_funds"],
    "numeric_conditions": { "amount": { "gt": 10000 } }
  },
  "severity": "critical",
  "compliance_frameworks": ["SOC2", "OCC"]
}

principal_type/principal_id scope the policy to one agent or agent group (null = every agent); action_tool scopes to one tool (null = any tool); resource_system scopes to one business system (null = any system). effect is deny or review — there is no stored allow. See Policies for the full condition-key vocabulary.

A policy with an empty condition must still constrain at least one of principal_id, action_tool, or resource_system, or the request is rejected with 422 — an unconstrained, unconditional policy would deny every call from every agent.

priority is accepted but not read by the engine — see Policies → Precedence.

Response 201: policy object with generated id, including the compiled cedar_text.


PUT /policies/

Update a policy. All fields optional (same shape as POST /policies). Recompiles cedar_text and takes effect on the next call_tool request through the gateway.

Auth: admin

Response: 200 with updated policy, or 404.


GET /policies//activity

How often this policy has fired recently.

Auth: admin

Query parameters: window (default "7d")

Response 200:

json
{ "window": "7d", "fired": 12, "calls_evaluated": 340 }

calls_evaluated matches on the policy's agent + tool scope only — a business system isn't persisted on the audit event itself (it's resolved at gateway-call time), so for a system-scoped policy this count is an upper bound, not exact.


DELETE /policies/

Delete a policy. Takes effect immediately.

Auth: admin

Response: 204, or 404.


POST /policies/activate-baseline

Activate a set of pre-built safety policies from the library. Idempotent — calling again when policies are already active is safe.

Auth: admin

Request:

json
{ "mode": "standard" }

mode is "standard" or "strict".

  • standard — activates 4 Dangerous Operations policies
  • strict — activates those 4 plus 4 Data Protection policies

Response 200:

json
{
  "mode": "standard",
  "activated": ["block_shell_execution", "block_file_deletion", "block_cloud_metadata_access", "block_sensitive_file_reads"]
}

Returns 422 for invalid mode.


POST /policies/nl-draft

Enterprise feature. Describe a policy in plain English and get back a scoped, compile-checked draft. Never writes to the policies table.

Auth: admin + enterprise license

Request:

json
{ "description": "Flag any transfer over $10,000 for review", "agent_id": null }

Response 200:

json
{
  "draft": {
    "principal_type": null,
    "principal_id": null,
    "action_tool": null,
    "resource_system": null,
    "effect": "review",
    "condition": { "numeric_conditions": { "amount": { "gt": 10000 } } }
  },
  "sentence": "Flag any call where amount > 10000 for review.",
  "status": "ok",
  "warnings": []
}

If the draft doesn't compile, draft is null, status is "requires_manual_authoring", and warnings explains why — a draft is never handed back looking ready when it isn't.


POST /policies/simulate

Enterprise feature. Replay a draft (from nl-draft, or hand-written) against real historical audit_events. Read-only — writes neither a policy nor an audit event.

Auth: admin + enterprise license

Request:

json
{
  "draft": {
    "principal_type": null, "principal_id": null,
    "action_tool": null, "resource_system": null,
    "effect": "review",
    "condition": { "numeric_conditions": { "amount": { "gt": 10000 } } }
  },
  "window_days": 7
}

Response 200:

json
{
  "eligible_events": 340,
  "would_deny": 0,
  "would_review": 12,
  "corpus_start": "2026-05-01T00:00:00Z",
  "corpus_note": null,
  "matches": [
    { "audit_event_id": "uuid", "tool_name": "initiate_transfer", "decision": "review", "occurred_at": "2026-05-14T09:12:00Z" }
  ]
}

If no events fall in the simulation's eligible corpus, would_deny/would_review are null and corpus_note explains why — a false "0 of 0" is never returned as if it meant "this policy changes nothing." The draft is compile-checked before replay; a draft that doesn't compile returns 422.

Rate-limit and token-budget aggregates are zeroed during simulation — those reflect point-in-time state and can't be honestly replayed against a historical event.


Reviews (HITL)

GET /reviews/

Get the status of a human review.

Auth: agent or admin

Response 200:

json
{
  "id": "f1e2d3c4-...",
  "audit_event_id": "3f2a1c8b-...",
  "session_id": "b3e1f9a2-...",
  "tool_name": "export_customer_data",
  "tool_parameters": "{\"limit\": 500}",
  "response_deadline": "2026-06-03T14:00:00Z",
  "assigned_to": null,
  "status": "pending",
  "reviewer": null,
  "review_note": null,
  "reviewed_at": null,
  "created_at": "2026-06-03T10:00:00Z"
}

status is pending, approved, or denied.


GET /reviews

List reviews.

Auth: human JWT (any role) + business license

Query parameters: status (pending/approved/denied), limit (default 50, max 200), offset

Response 200: array of review objects

Returns 402 for community installations.


PATCH /reviews/

Approve or deny a pending review from the dashboard.

Auth: human JWT (any role) + business license

Request: { "action": "approve" } or { "action": "deny", "note": "reason text" }

Response 200: updated review object. Returns 409 if the review was already resolved (approved or denied) by a prior call — resolution is row-locked, shared with the Slack approve/deny path, so a concurrent Slack and dashboard resolution on the same review can't both succeed. Returns 402 for community installations.

If your agent retries the identical call (same session_id, tool name, and parameters) within HITL_RETRY_WINDOW_MINUTES (15 minutes by default) of an approve here, that retry is automatically allowed instead of being routed to review again.


Audit Events

GET /audit-events

List audit events with optional filters. Community plan: limited to events from the last 7 days.

Auth: admin (human JWT)

Query parameters:

ParameterDescription
decisionFilter by allow, deny, or review
agent_idFilter by agent UUID
tool_nameExact tool name
date_fromISO 8601 start
date_toISO 8601 end
limitDefault 50
offsetPagination offset

Response 200: array including agent_name, policy_name, policy_id, workflow, sequence_number, tool_response (truncated 200 chars; always null via the MCP gateway path — not currently recorded). Each event where a policy fired includes a nested policy object (name, scope fields, effect) so the caller doesn't need a second lookup to render what happened.


GET /audit-events/export

Export filtered audit events as CSV. Accepts the same filters as GET /audit-events. Maximum 10,000 rows.

Auth: admin (human JWT) + enterprise license

Response: text/csv with Content-Disposition: attachment; filename=audit_events.csv

Returns 402 for community installations.


POST /audit-export-configs

Register an outbound SIEM export target. Every audit event written after creation is streamed to it — see Audit Log: SIEM Export.

Auth: admin (human JWT) + business license

Body:

FieldDescription
export_typeotel or webhook
target_urlDestination URL — an OTLP/HTTP collector endpoint for otel, a receiver URL for webhook
enabledDefault true
tenant_idOptional

Response 201: the created config (id, export_type, target_url, enabled, tenant_id).

Returns 402 for community installations.


GET /audit-export-configs

List all configured SIEM export targets.

Auth: admin (human JWT) + business license

Response 200: array of configs, same shape as the create response.


DELETE /audit-export-configs/

Remove a SIEM export target. Stops future exports; does not affect events already delivered.

Auth: admin (human JWT) + business license

Response 204: empty. Returns 404 if the config doesn't exist.


Sessions

GET /sessions

List agent sessions with enriched metadata.

Auth: human JWT (any role) + enterprise license

Response 200: array including agent_name, event_count, has_pending_review, completed_at.

Returns 402 for community/business installations.


GET /sessions//events

Get all gateway call events for a session, ordered by sequence_number.

Auth: human JWT (any role) + enterprise license

Response 200: array of audit events including trigger_context.

Returns 402 for community/business installations.


Dashboard

GET /dashboard/summary

Returns current operational summary. Polled by the dashboard Overview page.

Auth: admin (human JWT)

Response 200:

json
{
  "intercepts_today": 47,
  "allowed_today": 40,
  "denied_today": 5,
  "reviewed_today": 2,
  "deny_rate_today": 0.12,
  "pending_reviews": 3,
  "active_warnings": 1,
  "overdue_reviews": 0,
  "top_denied_tool": { "tool": "query_credit_bureau", "count": 3 },
  "high_risk_sessions": 0,
  "active_sessions": 12,
  "active_agents": 7,
  "active_policies": 18,
  "top_tools": [{"tool": "read_loan_application", "count": 22}],
  "decisions_by_hour": [{"hour": "2026-06-03T10:00:00Z", "allow": 10, "deny": 2, "review": 1}]
}

active_warnings is always 0 for community edition. top_denied_tool is null when no deny events today.


GET /dashboard/metrics

Returns analytics for the Decision metrics page.

Auth: admin (human JWT)

Response 200:

json
{
  "policy_hit_rate": 0.34,
  "deny_trend": [{"date": "2026-06-01", "allow": 120, "deny": 15, "review": 5}],
  "top_agents_by_deny_rate": [{"agent_name": "loan-agent", "deny_count": 10, "deny_rate": 0.22}],
  "avg_review_seconds": 342
}

GET /dashboard/activity-log

List user activity log entries (admin actions on policies, agents, users).

Auth: admin (human JWT)

Query parameters: action, date_from, date_to, limit, offset

Response 200: array of { id, user_email, action, resource_type, resource_id, before_state, after_state, ip_address, created_at }


Users

GET /users

List all dashboard users.

Auth: admin (human JWT)

Response 200:

json
[
  {
    "id": "uuid",
    "email": "alice@acme.com",
    "name": "Alice Smith",
    "role": "admin",
    "is_active": true,
    "is_root": true,
    "password_set": true,
    "last_login": "2026-06-03T09:00:00Z",
    "created_at": "2026-06-02T14:00:00Z"
  }
]

POST /users

Invite a new dashboard user. Returns a one-time magic link for the user to set their password.

Auth: admin (human JWT)

Request: { "email": "bob@acme.com", "name": "Bob Jones", "role": "viewer" }

role is admin, analyst, or viewer.

Response 201:

json
{
  "id": "uuid",
  "email": "bob@acme.com",
  "magic_link": "http://localhost:3000/invite?token=<plaintext>"
}

The magic link is shown once — share it with the user securely.


PUT /users/

Update a user's name, role, or active status.

Auth: admin (human JWT)

Request: { "is_active": false } or { "role": "admin" }

Response 200: updated user object.


DELETE /users/

Delete a user. Root user cannot be deleted.

Auth: admin (human JWT)

Response: 204, or 404.


POST /users//regenerate-invite

Generate a new magic link for a user. Invalidates any previous link. Works for both pending users (never set password) and active users (password reset).

Auth: admin (human JWT)

Response 200: { "magic_link": "http://localhost:3000/invite?token=<plaintext>" }


Org Settings

GET /org-settings

Get the organisation singleton settings.

Auth: human JWT (any role)

Response 200:

json
{
  "org_name": "Acme Corp",
  "timezone": "America/New_York"
}

PUT /org-settings

Update organisation settings. All fields optional (patch semantics).

Auth: admin (human JWT)

Request: { "org_name": "Acme Corp", "timezone": "Europe/London" }

timezone must be a valid IANA timezone string.

Response 200: updated org settings object.


License

GET /license-info

Returns the current license plan. Used by the frontend at runtime to unlock enterprise features. Safe to call unauthenticated — never returns the raw license key or email.

Auth: none required

Response 200:

json
{
  "plan": "enterprise",
  "company": "Acme Corp",
  "is_enterprise": true,
  "is_business": true,
  "expires_at": "2027-06-03T00:00:00Z"
}

plan is community, business, enterprise, or trial. Returns community when no org_settings row exists, or when one exists but license_plan isn't set.


GET /license/features

Returns which paid destinations exist for this install and their feature flags, for the dashboard to decide what to render in its navigation. A paid destination is absent entirely on a lower tier, never shown locked or greyed out.

Auth: human JWT (any role)

Response 200:

json
{
  "tier": "enterprise",
  "features": {
    "nl_authoring": true,
    "simulation": true,
    "hitl": true,
    "compliance_reports": true
  },
  "license_status": "active"
}

tier is "free", "business", or "enterprise" — reflects the plan the org is licensed for, regardless of license_status, so a past_due/canceled Business or Enterprise org still sees its nav items (routed to a reactivate banner) instead of losing them outright. license_status is "active", "past_due", "canceled", or null (community, or a paid plan that hasn't synced yet).


PUT /settings/license-activation

The only way an activation code enters the system — there's no install.sh prompt for it. Validates the code against billing.aictl.io before saving it; a code that doesn't validate is never stored.

Auth: admin (human JWT)

Request: { "activation_code": "..." }

Response 200:

json
{
  "license_status": "active",
  "license_plan": "business",
  "license_synced_at": "2026-09-21T10:00:00Z"
}

Response 400: invalid or unrecognized activation code — nothing is saved.


Billing

GET /billing/usage

Returns plan details and gateway call usage for the current and previous calendar month.

Auth: admin (human JWT)

Response 200:

json
{
  "plan": "business",
  "company": "Acme Corp",
  "annual_price_usd": 999.0,
  "retention_days": 30,
  "features": ["Cedar policy enforcement", "..."],
  "this_month": { "period": "2026-09", "intercepts": 124500 },
  "last_month": { "period": "2026-08", "intercepts": 98000 },
  "manage_subscription_url": "https://billing.aictl.io/portal/...",
  "upgrade_url": null,
  "license_status": "active",
  "license_synced_at": "2026-09-21T10:00:00Z"
}

features is the cascaded list for the plan (community features, plus business features if applicable, plus enterprise features if applicable) — not a fixed enum of internal flag names. manage_subscription_url is null until an activation code has been entered; upgrade_url is always null today (see Operations → Billing and usage).


Enterprise Compliance

POST /enterprise/compliance/report

Generate a compliance report narrative using the configured LLM.

Auth: admin + enterprise license

Request:

json
{
  "frameworks": ["soc2", "eu_ai_act"],
  "date_from": "2026-01-01",
  "date_to": "2026-05-31",
  "format": "pdf"
}

frameworks values: eu_ai_act, nist_ai_rmf, soc2, iso_42001. format: pdf or md.

Set LLM_MOCK_ENABLED=true to generate a placeholder without an LLM API key.

Response: Binary file download (application/pdf or text/markdown).


GET /enterprise/compliance/reports

List previously generated compliance reports.

Auth: admin + enterprise license

Response 200: array of { id, frameworks, date_from, date_to, format, generated_at, generated_by_email, llm_model, token_input, token_output }


GET /enterprise/compliance/reports//download

Download a previously generated report.

Auth: admin + enterprise license

Response: Binary file download.


Health

GET /health

API liveness check. No authentication required. Use for load balancer health checks.

Response 200 (community, no license):

json
{
  "status": "ok",
  "service": "aicontrol",
  "policy_engine_status": "in_process",
  "drift_detector_status": "enterprise_only"
}

policy_engine_status is always "in_process" on every tier — Cedar evaluates in the same process as the API, so there's no separate service to be unreachable.

drift_detector_status reflects an internal background consistency check (agent-approved tools vs. policy coverage) and is only meaningful on an enterprise-licensed install, where it returns "healthy" or "degraded" instead of "enterprise_only".


Error responses

CodeMeaning
400Bad request
401Missing or invalid token
402Enterprise license required
403Token lacks permission
404Resource not found
409Conflict (e.g. setup already complete)
422Validation error — field-level detail in body
500Internal server error