Default access policy
The default access policy gives every unmatched MCP, LLM, and outbound HTTP request one deterministic governance outcome.
Policy identities
AIControls generates one read-only policy for each request plane:
| Plane | Generated policy | Match field |
|---|---|---|
| MCP tool calls | accessdefault:mcp | object.mcp.tool != "" |
| LLM calls | accessdefault:llm | object.llm.model != "" |
| Outbound HTTP calls | accessdefault:http | object.http.host != "" |
When the workload unmatched action is Deny, AIControls also generates accessdefault:mcp:workload and accessdefault:llm:workload. They match the same unmatched MCP and LLM calls, and only when the call comes from a registered workload with pinned access.
Each generated policy runs only when no enforcing access policy matched the request. A guardrail match does not suppress the default. This distinction keeps content and risk checks from being mistaken for proof that the caller has access.
The generated policies do not run for responses, token issuance, discovery operations, or a request that the evaluator allowed because policy evaluation failed open. Their definitions and metadata are source owned. An ordinary CEL policy cannot turn itself into a final default by copying a name, label, or annotation.
Actions
| Action | Result |
|---|---|
| Allow | Forward the request and record unmatched as the evaluation status. |
| Hold for approval | Pause the request until a reviewer approves or denies it. |
| Deny | Block the request. |
| Inline Judge | Apply an explicit Allow, Hold, or Deny fallback. For an eligible unmatched request, a bounded intent-alignment assessment can tighten that fallback. |
New and upgraded installations start with Allow for all three planes. AIControls does not silently tighten an existing installation. You must deliberately save a restrictive action. Registered workloads have their own workload unmatched action, which is Deny on a new installation.
AIControls enables Hold for approval and Deny only when the access-classification owner for that plane reports complete coverage. If coverage becomes incomplete after you save a restrictive default, the runtime filters that default instead of treating partial classification as complete. The Settings page shows the readiness state and reason for each plane.
Inline Judge
Inline Judge is an opt-in companion to a plane's deterministic fallback. It runs only for an unmatched request. It does not run for responses, token issuance, discovery operations, an explicit access match, a CEL evaluation error, or a request a guardrail already denied.
Configure these fields independently for each plane:
| Field | Meaning |
|---|---|
| Fallback decision | The unchanged Allow, Hold, or Deny result used whenever Judge cannot supply a usable verdict. |
| Latency budget | The maximum synchronous delegate time, from 100 to 5,000 milliseconds. |
| Confidence threshold | The minimum confidence, from 0 to 1, required for a misalignment verdict to tighten the fallback. |
| Auto-apply | When off, a qualifying misalignment adds a Hold companion. When on, it adds a Deny companion. |
| Cache TTL | How long a complete verdict can be reused, from 1 to 300 seconds. |
Only a fresh, complete misaligned verdict with confidence at or above the threshold changes the result. An aligned, unknown, failed, malformed, stale, expired, timed-out, or low-confidence assessment leaves the fallback unchanged. Judge can tighten a decision and never loosen it. AIControls does not derive or invent confidence when the delegate omits it.
The companion policy uses the stable name accessdefault:<plane>:judge-deny. Immediate policy decisions record the assessment status, value, confidence, capability revision, configuration revision, timestamps, latency, and cache status. MCP and outbound HTTP audit events preserve this provenance, as do terminal LLM Hold and Deny events. A successful LLM call does not yet copy request-time Judge provenance to its later terminal audit row. Before sending request facts to the delegate, AIControls bounds them and redacts credentials and personal data regardless of the audit-log redaction setting. Cached entries use an opaque keyed digest and retain no raw prompt or tool arguments.
The process-level workloadAccess.judge flag defaults to false and is read at startup. When it is off, AIControls preserves a stored Judge configuration in Settings but compiles and enforces only its fallback. Self-hosted installations must also configure a confidence-bearing llmExt.endpoint.
Evaluation and audit behavior
The three deterministic fallbacks are stored as one revisioned setting. An optional Judge configuration is stored as a revision-linked companion, and AIControls updates both atomically. An older binary ignores the companion and continues to enforce the explicit fallback. A conflicting edit is rejected so you can reload before retrying. The update, companion, and artifact revision roll back together if configuration-audit persistence fails.
When a generated default handles a request, the audit event reports unmatched as the evaluation status. If the default is the most restrictive result, the event also uses its stable policy name. If a guardrail produces a more restrictive result, the policy field continues to identify that guardrail while the unmatched status records that no access policy matched. Guardrail-only traffic therefore remains visibly unmatched without losing the rule that determined its effective decision. See Decision types for the status contract.
The generated policy definitions are currently visible on Settings → General → Default access policy, not in the editable policy catalog or policy replay. Audit events identify a generated policy by its stable name when it determines the effective decision. Catalog and replay support for source-owned virtual policies is planned separately.
Registered workloads
This section applies only to self-managed installations. Workload Trust isn't available on Nirmata-hosted workspaces. See Feature availability.
A workload with a pod_uid identity binding and a Workload Registration changes which requests count as unmatched, and it adds Audit and containment behavior around the default. A registration with only a virtual_key binding stays on the legacy path, and none of this applies to it.
- Referenced grants match. The Access Binding that the workload's Blueprint selects, and the bindings that the workload's override references, are access policies for that workload. A call that one of them covers is matched, and the default doesn't run. A reference to a missing or disabled binding grants nothing, so the call is unmatched.
- Audit doesn't count its own deny entries as matches. In Audit mode, the workload's own deny entries are recorded as shadowed decisions and don't set the match. If nothing else matches, the default access policy still decides the call, even when it is Deny.
- The workload unmatched action adds a Deny. When the workload unmatched action is Deny, an unmatched MCP or LLM call is denied in Enforce mode. In Audit mode, the deny is recorded as a shadowed decision and the call is allowed, with or without the audit unmatched override.
- The audit unmatched override. In Audit mode, with
auditUnmatchedOverrideon in the workload's override, a Hold or Deny from the MCP or LLM default becomes Allow. The event keepsunmatchedas its status, records the would-be decision as shadowed, and the unmatched metrics count the call as unmatched. The HTTP default is never overridden, and Enforce mode ignores the override. See Audit unmatched override. - Fail-closed and containment decide first. A workload whose configured access is incoherent, or whose ServiceAccount is contained, is denied before the default access policy runs. The default doesn't apply to these calls. See Fail-closed rules.
- The Judge reads the same inputs. The Inline Judge assesses a workload's unmatched call against the same Blueprint, bindings, and override that authorization used. When an input is missing, the Judge returns unknown, and the fallback decision applies.
The aicontrols_workload_access_decided_total metric labels each decision with the registration's mode (audit, enforce, or unregistered). See Metrics.
Workload callers
This setting applies only to self-managed installations. Workload Trust isn't available on Nirmata-hosted workspaces. See Feature availability.
The workload unmatched action sets what happens to an MCP or LLM call from a registered workload that none of its grants match. Without it, an unmatched call from a workload in Enforce gets the plane's action, which is Allow on a new installation.
| Action | Result |
|---|---|
| Deny | In Enforce mode, deny the call with accessdefault:mcp:workload or accessdefault:llm:workload. In Audit mode, allow the call and record the deny as a shadowed decision, so the call still becomes a recommendation. |
Use the MCP and LLM actions above (inherit) | Apply the plane's action, as for every other caller. |
The action can only add a restriction. With Deny, a call gets the most restrictive of the plane's action and the workload deny, and a plane action of Hold for approval or Deny keeps applying in Audit mode unless the audit unmatched override is on. With inherit, only the plane's action applies.
It applies only to calls from a workload with a pod_uid identity binding and a registration, which AIControls resolves from a verified, Pod-bound ServiceAccount token. Developer sessions, personal access tokens, human OIDC users, virtual keys, machine-to-machine OIDC clients, and Cross-App Access tokens keep the plane's action. An unregistered workload gets the unregistered workload mode instead. Outbound HTTP calls aren't affected. Like a restrictive plane action, Deny takes effect on a plane only while its access classification is complete. When a stored Deny isn't applied on a plane for that reason, Workload callers: unmatched calls shows a Deny isn't applied yet warning with each plane's classification status and reason, and workloadCallers.notApplied carries the same text.
Defaults and upgrades
AIControls stores the action as the access_default_workload_unmatched setting and seeds it the first time a release with this feature starts:
- A new installation, which has recorded no calling identity yet, starts with Deny. After you learn a workload's access in Audit, promote it, and switch to Enforce, a call outside the promoted access is blocked without any change to the plane actions.
- An existing installation, which has already served callers, starts with
inherit, so a registered workload keeps the behavior it has today. Select Deny when you're ready. - If the seed can't be written, AIControls retries for a few seconds before it loads policies. If it still fails, AIControls applies
inherit. A later start seeds the setting as of the start that failed: identities recorded after that failure don't make the installation count as existing. To skip the wait, select an action in Settings.
Change it on Settings → General → Default access policy under Workload callers: unmatched calls, or with the API:
curl -X PUT "$YOUR_WORKSPACE_URL/api/v1/settings/default-access-policy/workload-callers" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"action":"deny","expectedAction":"inherit"}'
expectedAction is optional. When it doesn't match the stored action, the request returns 409 Conflict. Selecting deny also returns 409 Conflict while the MCP or LLM access classification is incomplete. The change is recorded in the configuration audit log. The replica that saves it applies it immediately, and other replicas apply it at their next policy refresh. GET /api/v1/settings/default-access-policy returns it as workloadCallers, with action, effectiveAction, and source (seed or admin).
The action is stored apart from the per-plane actions so that an older binary, during a rolling upgrade or a rollback, still reads the per-plane document. An older binary doesn't apply the workload unmatched action.
Unregistered workloads
This setting applies only to self-managed installations. Workload Trust isn't available on Nirmata-hosted workspaces. See Feature availability.
The default access policy also sets what the gateway does with an MCP or LLM call from a Kubernetes workload that has no registration. This mode runs before any access policy, so an organization-wide grant, such as an Access Binding that every caller matches, can't give an unregistered workload AI access.
| Mode | Result |
|---|---|
| Enforce | Deny the call with the reason workload_not_registered. The caller receives a message that names the workload and tells you to register it. |
| Audit | Allow the call, and record the denial it would have received as a shadowed decision on its audit event. |
In both modes, AIControls records the call as a security event and lists the workload in Inventory → Workloads as discovered.
Which calls it applies to
The mode applies only to a request-phase MCP or LLM call that the gateway attributes to a Kubernetes workload:
- The caller presents a verified, Pod-bound Kubernetes ServiceAccount token.
- The token's Pod still resolves, through its owner references, to a workload such as a Deployment.
- Neither that ServiceAccount's identity binding nor the workload tuple has a registration.
Every other caller keeps its existing behavior:
- Developer sessions, personal access tokens, human OIDC users, virtual keys, machine-to-machine OIDC clients, and Cross-App Access tokens aren't Kubernetes workloads, so the mode never applies to them.
- A call that can't be attributed to a workload keeps the behavior it has today. That includes an unverified or unsigned ServiceAccount token (governed by
identity.requireVerified), a verified token that isn't bound to a Pod, and any call in a deployment where AIControls has no live Pod resolver. A Pod-bound call whose Pod lookup fails, runs over its budget, or is refused by RBAC doesn't keep that behavior: a registered workload is denied (see the fail-closed rule below), and an unregistered one is treated as unregistered. All other calls go through the access policies and the per-plane default above, unchanged. - Outbound HTTP calls, responses, pushed notification events on an existing subscription, and token issuance aren't affected. A subscription request itself is gated.
A registration takes effect within one second of being saved, on every replica. Registering through the wizard also binds the workload's ServiceAccount and attaches a Workload Blueprint in the same transaction, so the workload's next call is governed by its Blueprint's access as well. See Register a workload. If AIControls can't read whether the workload has a registration, it fails closed and treats the call as unregistered, and the aicontrols_unregistered_workload_lookup_failures_total metric counts it. When a caller's Pod resolves to a different workload than the registered workload that shares its ServiceAccount, AIControls discards the registration and increments the aicontrols_workload_identity_mismatch_total metric. When a registered workload's Pod can't be resolved, including when the live Pod lookup is refused by RBAC or runs over its budget, AIControls denies the call as fail-closed in every mode and increments the aicontrols_workload_registration_binding_unavailable_total metric. Containment and fail-closed rules decide first: a contained workload or one with incoherent configured access is denied for that reason instead. Like those decisions, an Enforce denial can't be lifted by a policy exception, and the proxy-wide observe mode doesn't downgrade it. Use Audit to observe.
Defaults and upgrades
AIControls stores the mode as the unregistered_workload_mode setting and seeds it the first time a release with this feature starts:
- A new installation, which has recorded no calling identity yet, starts in Enforce.
- An existing installation, which has already served callers, starts in Audit, so no call that works today is blocked by the upgrade. Review the recorded events and register the workloads you expect, then switch to Enforce.
- If the seed can't be written, AIControls retries for a few seconds. If it still fails, AIControls runs in Audit. A later start seeds the setting as of the start that failed: identities recorded after that failure don't make the installation count as existing. To skip the wait, select a mode in Settings.
To start an automated installation in a specific mode, set workloadTrust.unregisteredWorkloadMode to audit or enforce in your Helm values:
workloadTrust:
unregisteredWorkloadMode: audit
The value replaces the default seed on both new and existing installations, and the chart rejects any other value. Leave it unset to keep the defaults above. Like the other workloadTrust values, it applies on first boot only. The first-run setup wizard doesn't ask for this mode; change it later in Settings → General → Default access policy → Unregistered workloads. GET /api/v1/settings/default-access-policy reports where the mode came from as unregisteredWorkloads.source: preset, seed, or admin.
After the first start, the stored value always wins. Every replica reads the same setting and applies a change within a minute; the replica that saves the change applies it immediately. Change it on Settings → General → Default access policy under Unregistered workloads, or with the API:
curl -X PUT "$YOUR_WORKSPACE_URL/api/v1/settings/default-access-policy/unregistered-workloads" \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"mode":"enforce","expectedMode":"audit"}'
expectedMode is optional. When it doesn't match the stored mode, the request returns 409 Conflict. The change is recorded in the configuration audit log. GET /api/v1/settings/default-access-policy returns the mode as unregisteredWorkloads.mode.
The mode is stored apart from the per-plane actions so that an older binary, during a rolling upgrade or a rollback, still reads the per-plane document. An older binary doesn't apply the unregistered workload mode.
Network egress compatibility
networkFilter.warnFallback remains an earlier compatibility gate for outbound HTTP traffic. If it is set to deny, an HTTP request without a definitive access or guardrail decision is denied as egress-warn-fallback before the HTTP default applies. AIControls does not translate that existing setting into an accessdefault:http value, so upgrades preserve their configured egress behavior.