AIControl Docs

FAQ — Technical Architecture

The other docs pages cover how to integrate and configure AIControl. This page goes one layer deeper: exactly what happens inside AIControl on every call, how the pieces fit together, and how the licensing and auth models actually work.


What happens, in order, when my agent calls call_tool through the gateway?

  1. Auth. The token must be agent-scoped — role agent with a non-null agent_id claim. An admin token, or an agent-role token with no agent_id, is rejected with 403 before anything else runs.
  2. Server lookup. server_id in the URL must match a registered server with status: "active" — anything else (pending_review, blocked, or no match) is rejected before the request body is even parsed.
  3. Request validation. session_id and sequence_number must be present and valid — a JSON-RPC -32602 error is returned immediately if not.
  4. Server-level approved-tools gate. If the registered server has a non-empty approved_tools list and the requested tool isn't on it, the call is denied immediately with an isError: true result — before the agent-level gate or the policy engine ever run.
  5. Agent-level approved-tools gate. If the calling agent has its own non-empty approved_tools allowlist and the tool isn't on it, the call is denied the same way. If the agent is in observe mode, this would-be denial is still logged, but the call proceeds — observe mode lets you turn governance on for an agent without blocking its calls immediately.
  6. Policy load and evaluation. Active policies are read fresh from Postgres on every call — never cached — along with current rate-limit counts and cumulative token/cost usage. One in-process Cedar evaluation runs, carrying the tool name, arguments, the scoped policy set, current time, and usage context. This typically completes in well under a millisecond.
  7. Session auto-creation and risk accumulation. The first call referencing a new session_id creates the session row on the fly. Every call adds to that session's running risk score.
  8. Decision branching. On a Business or Enterprise license, a review decision writes the audit event and a pending human-review record synchronously, then fires a Slack notification without waiting for it to send, and returns an isError: true result immediately. On Community — no Slack notification, no dashboard review queue, no path to resolve a pending review — a review decision is collapsed to deny before any review record is created, so it fails closed instead of blocking forever. Before either of those, if this exact call (same session, tool, and parameters) was already approved via HITL within the last 15 minutes, the decision is overridden to allow instead. An enforced deny writes its audit event through the write-ahead log and returns an isError: true result. An allow proceeds to forward the call.
  9. Forwarding and response scanning. The call is forwarded to the real downstream server. The response is scanned by the same deterministic, pattern-based scanner used elsewhere in the product before being returned — a flagged response is blocked with isError: true even though the underlying tool call already executed.
  10. Response. The downstream server's actual response is returned on success; an isError: true JSON-RPC result is returned on any denial, review, or scan-block; a -32000/-32602 JSON-RPC error is returned for an unreachable server or malformed request.

Every call that reaches step 4 or beyond writes an audit event, with no exception. The server-level gate, the agent-level gate, the review path, and the allow/deny path all write one — there's no code path where a governed call goes unlogged. A request rejected at step 1, 2, or 3 (auth, server lookup, or malformed params) fails before there's an agent/tool/session triple to audit against, the same way a request to a nonexistent endpoint would.


Is the policy engine ever probabilistic?

No. Policy evaluation runs on Cedar, an open-source authorization engine, in the same process as the API — there's no LLM in the decision path, and a given policy set plus a given call always produces the same decision. If the engine itself fails to evaluate a call (a malformed policy, an unexpected input), that failure resolves to deny, never to a silent allow — an engine fault should never look like permission.


What's the actual precedence when multiple policies match a call?

Any deny wins. If nothing denies but something matches with effect review, the call is routed to review. If nothing matches at all, the call is allowed. This ordering is structural to how policies compile, not a configurable priority setting — you don't need to reason about ordering between your own policies, only about which ones match.


Why does AIControl write to a local file before writing to Postgres?

This is the write-ahead log (WAL), and it exists to solve one problem: writing an audit record must never be what makes a gateway call_tool request slow, and it must never be what loses data if Postgres has a bad moment.

Every allow/deny audit event is first written to a local file on the API host with an immediate disk sync — durable in microseconds, well before any database round-trip. A background process continuously drains that file into Postgres several times a second. If the API process crashes before a batch ships, the file replays on restart, so a crash doesn't lose anything already written to the WAL.

A related detail: because there's a small lag between "written to WAL" and "visible in Postgres," anything that needs to count very recent activity — a session-scoped rate limit, for instance — reads the WAL file directly for that recent window, not just Postgres, so the shipping lag can't be exploited to slip past a limit.


How does risk scoring work?

Every session accumulates a running risk score as calls happen: allow adds 1, review adds 10, deny adds 25. It's a simple counter, not a model — a session with a few denies and reviews will visibly climb, which is what the dashboard uses to flag high-risk sessions for a human to look at.


What actually happens when a call is routed to human review?

This applies on a Business or Enterprise license — on Community the decision is collapsed to deny instead (see the decision-branching answer above), so no review record is created. On Business/Enterprise, a pending review record is created, and — if Slack is configured — a notification is posted to your review channel with approve/deny buttons. That Slack post is fire-and-forget: the response to your agent goes out immediately, without waiting to see whether the Slack message sent successfully. If Slack is down, the failure is logged and the review record still exists; your agent's call was never held up waiting on Slack either way. Approve/deny actions in Slack are signature-verified and replay-protected, and a review that's already been resolved is a no-op on a second click — this is enforced with a row lock shared by the Slack and dashboard resolution paths, so a Slack approval and a dashboard denial racing on the same review can't both go through.


If a reviewer approves a call, does the agent's original call get unblocked automatically?

Not the original call — that request already returned its isError: true result and is gone. But if your agent retries the identical call (same session_id, same tool, same parameters) within 15 minutes of the approval, that retry is automatically allowed: evaluate_tool_call checks for a matching approved hitl_reviews record for that session/tool/parameters combination before it ever re-evaluates Cedar, and overrides the decision to allow with reason hitl_approved_retry. Cedar itself is still stateless and evaluates every call independently — this check runs before Cedar, not inside it. A retry outside that 15-minute window, or with different parameters, is evaluated fresh and routed to review again. Denied reviews are never retried automatically — only an approved status unblocks a retry.


How are machine tokens and human accounts verified differently?

Machine (agent) tokens are non-expiring JWTs. Only a hash of the token is ever stored, and it's checked against the database on every request — so revoking a token takes effect immediately, on the very next call.

Human accounts authenticate with email + password. A human session JWT has an 8-hour expiry and is verified by signature alone, with no database lookup on each request — this keeps dashboard requests fast. One consequence: deactivating a human account in the admin panel doesn't force an immediate logout — an already-issued session token keeps working until it naturally expires.


How does licensing actually work — is it just a database flag?

Yes, by design. Your plan (community / business / enterprise / trial) and status (active / past_due / canceled / expired / unset) live in a single row in your own org_settings table — no signed key, no RS256, no public key baked into the app. No row, or a row with no license_plan set, defaults to community.

That row is kept honest by a daily background sync against billing.aictl.io, keyed on the activation code you entered at Dashboard → Billing (PUT /settings/license-activation, which validates the code against billing.aictl.io before ever storing it — an invalid code is never saved). A business/enterprise plan only counts as usable when its synced license_status is active; past_due/canceled/never-synced all gate the same as community. If billing.aictl.io is unreachable, the last-known-good status is kept for up to 7 days before the license is forced to an unreachable state and gates closed — so an outage on AIControl's side can't be used to keep a lapsed subscription's features unlocked indefinitely, and a brief outage doesn't lock out a paying customer either.

GET /license-info is intentionally public and unauthenticated — the frontend needs to know your plan before you're even logged in, to decide what to show as locked versus available.


What's actually gated by plan, and what isn't?

CapabilityAvailability
Core interception, policy CRUD, agent CRUD, token issuanceCommunity
Deterministic Cedar policy evaluationCommunity (every tier — never gated)
Per-agent approved-tools enforcementCommunity
Rate and token-budget policiesCommunity
Append-only audit log (7-day query window)Community
Slack HITL notifications actually firingBusiness or Enterprise
In-dashboard review queue (view and resolve pending reviews)Business or Enterprise
Audit log — 30-day query windowBusiness
Audit log — 90-day query windowEnterprise or Trial
Session drill-down, audit log CSV exportEnterprise
Compliance report generation (PDF/Markdown)Enterprise
Natural-language policy authoring, drafting, and simulationEnterprise

A review decision only creates a review record on Business or Enterprise. On Community it's collapsed to deny instead — there's no Slack notification and no dashboard review queue to resolve it, so letting it sit as review would block the call with no path forward.


How does natural-language policy authoring actually get from "an English sentence" to "an enforced policy"?

Four steps, and nothing skips ahead of a human: you describe what you want in plain English; AIControl drafts a policy scoped the same way any hand-written policy is (which agent, which tool, which system, what condition, what effect); the draft is checked server-side to confirm it actually compiles before it's ever shown to you, so you never review something that looks valid but isn't; and you can simulate it — a read-only replay against your own real historical traffic, showing what it would have done — before you decide whether to activate it. Nothing is written to your active policy set, and no audit event is created, until you explicitly activate.


Is my data ever sent anywhere outside my own environment?

AIControl runs entirely in your infrastructure via Docker Compose, and your audit data stays in your own PostgreSQL instance. The one exception is the LLM call behind compliance-report narrative generation and natural-language policy drafting — those two features send a summary of your own aggregated audit statistics (or your policy description) to the LLM provider configured in your .env, using your own API key. Every other governance decision — every gateway call_tool request — is evaluated entirely in-process, with nothing sent to any external service.


Does AIControl support SSO (SAML / OAuth / OIDC)?

Not yet. Human authentication today is email + password (POST /auth/login) plus a magic-link invite/reset flow (POST /auth/magic-link/validate, POST /auth/set-password) — see API Reference: Auth. There's no SAML, OAuth, or OIDC integration, and no identity-provider-driven user provisioning. It's on the roadmap; if it's a requirement for your evaluation, email hello@aictl.io to discuss timeline.


Is AIControl multi-tenant?

Not enforced today — treat each deployment as single-tenant. Every table carries a nullable tenant_id column, but no request path (auth, gateway, or the admin API) filters or isolates by it — an admin token can see and manage every agent, policy, and audit event in the database, regardless of tenant_id. If you need isolation between business units or customers, run separate AIControl instances (separate docker compose stacks, separate Postgres databases) rather than relying on tenant_id to isolate rows within one instance.