Skip to main content

Compiling a workload registration

Reference

A workload registration is never applied as written. AIControls expands its intent tiers into a neutral behavior declaration, compiles that into policy on two planes under guardrails that can only narrow it, and records everything the compilation could not carry across. This page is the reference for what compiles where, what a compile can refuse to do, and the two places a registration is deliberately not fully enforceable.

Self-managed installations only

Workload Trust is available only in private, self-managed AIControls installations, on premises or in a cloud cluster that you run. It needs a dedicated cluster that you run, with Kyverno, Kyverno Runtime, and cluster OIDC and RBAC access in place. Nirmata-hosted (SaaS) workspaces don't include it. See Feature availability.

You see the compile result in two places. The registration wizard's Review step shows what a registration compiles to before you save it. The workload's detail, GET /api/v1/workload-trust/{namespace}/{kind}/{name}, carries the compile result for the registration that is in force. Workload Protection separately reports whether the generated controls are Observing, pending enablement, Enforcing, or Degraded.

Registration intent tiers, such as network: cluster or files: own-volumes, expand to the primitives in the table below using your Runtime Binding. An unbound tier expands to nothing and the workload reports intent-tier-unbound naming the field to configure.

The compile table​

Intent has no model or MCP section, so the compile gets no gateway model or tool constraints from intent. Those grants come from Access Bindings.

Expanded primitiveCompiles toPlane
network.dnsegress allowlistGateway
network.dns (same values)RuntimePolicy.network allow, paired with deny: ["*"]Kernel
network.dns (negative form)RuntimePolicy.dns allow — observe only; reports every other name the pod resolvesKernel
network.cidrsRuntimePolicy.network allow, paired with deny: ["*"]Kernel
network.portspart of the gateway allowlistGateway only — see below
execRuntimePolicy.exec allow, paired with deny: ["*"]Kernel (needs the open/exec hooks)
open.read, open.writeRuntimePolicy.open allow, paired with deny: ["*"]Kernel (needs the open/exec hooks)
kubernetesnothing — it is a posture declaration, checked against the ServiceAccount's real power—

One declared name reaches both planes. network.dns is not split between them. The kernel matches a domain from the pod's own DNS answers and a cluster Service name from Service and EndpointSlice informers, so the same name is expressible on both sides. The split is by what each plane can do with a name:

GatewayKernel
Granularityper request — host, path, header, model, toolper flow — destination
Sees the name fromthe request itselfthe pod's own DNS answer, or a Service informer
Needs the workload to cooperateyes — it must honor the proxy settingsno
Enforceswhich model, which tool, which promptwhether a flow may leave at all
Blind toa workload that routes around itencrypted DNS, a cached answer, an address the pod never resolved

Neither is a superset of the other, which is why a declared name compiles to both.

Two things a declaration cannot carry into the kernel​

Both are recorded as compile findings rather than quietly dropped, because a silent omission is a policy that reads as fully enforced and is not.

No policy value has a port​

network.ports reaches the gateway and stops there. There is no kernel policy value that carries a port, so a local model on 11434 or a vLLM server on 8000 is constrained by address or by Service name, or not at all.

You will hit this. A workload that talks to an in-cluster inference server on a non-standard port gets a kernel rule about the destination, and the port half of the declaration is enforced only for traffic that goes through the gateway. Compilation emits compile-ports-dropped naming the exact ports, so the gap appears in the compile findings rather than being discovered from the generated policy.

Ports are still worth declaring. They constrain the gateway plane, and they document intent for review.

The kernel open rule has no read/write mode​

open.read and open.write are separate lists in the expanded declaration. The kernel open rule takes one value list, so they are merged: a path you declared write-only compiles as readable too.

This is a genuine widening of the compiled policy relative to what the declaration says, so it is reported as compile-open-mode-collapsed naming the affected paths. The two lists stay separate in the declaration: they are accurate, they are what conformance is checked against, and they are what a future kernel rule with a mode would use.

The four guardrails​

1. Clamp, never widen​

Every compiled value is intersected with the namespace's platform ceiling before it becomes policy. A declaration can describe a subset of what the platform already allows; it can never open something the platform closed.

A clamp is not a failure. The compile succeeds with the narrower set and records compile-clamped naming what was dropped and what was kept — so "my declared destination is not in the policy" has an answer rather than being a mystery.

2. AIControls renders the policy, not you​

A registration is expressed as intent tiers and AIControls renders the enforcement policy from them. There is no field through which a registration can carry Custom Resource text. A registration that could would let the team shipping a workload author the kernel enforcement policy applied to it, which is the wrong direction for a security control.

3. Breadth and sensor representation caps​

A registration matching more than a configured share of the filesystem or the address space is rejected at compile, with a message naming the offending patterns and the measured share. It is not narrowed, and it is not compiled — the alternative is a policy that permits everything while reporting as enforced.

CapDefaultRejectsAccepts
Filesystem, per open.read / open.write / exec list50%/**, /*/app/**, /etc/ssl/certs/**, /usr/local/bin/python3.11
IPv4 address space, across network.cidrs1/256 — one /810.0.0.0/4, 0.0.0.0/010.42.0.0/24, 10.42.1.7/32
Runtime sensor CIDR representation, per value/24 minimum prefix length10.0.0.0/23, 10.42.0.0/16, 10.96.0.0/1210.42.0.0/24, 10.42.1.7/32

The filesystem share is an estimate, and deliberately a coarse one: a pattern is scored by how many leading path segments it fixes before its first wildcard, so /** scores 100%, /app/** scores about 6%, and an exact path scores nothing. It is not measuring your filesystem — it is separating "permits a directory" from "permits everything". The address-space share is exact.

The sensor limit is separate from breadth. Its current egress maps hold individual IPv4 addresses, so it expands a CIDR into at most the 256 addresses in one /24. Splitting a real cluster /16 into /24s does not make the intent enforceable: it would still require 65,536 address entries. AIControls therefore fails the compile with a diagnostic naming the broad target. It does not render a partial allow list; Enforcement remains Not ready and Verification remains Not verified.

After apply, verification also reads the sensor's own target verdict. Applied=True says the daemon loaded the policy; only TargetsValid=True says every network target was programmed. Both must be true on every compiled runtime policy. A missing, unknown, unresolved, or unsupported TargetsValid condition keeps Enforcement Enable pending and Verification Not verified, with the condition reason shown.

A binding that includes /**, expecting it to be silently reduced to something sensible, is the case this refuses. Narrow the value to the directories the workload actually uses.

4. The 256-name cap on kernel domains​

The sensor holds a bounded number of domain names per pod. A compiled allow list past that limit fails loudly rather than being truncated: a truncated allow list denies traffic you declared, which in enforce mode is an outage with no visible cause.

The cap is per pod, not per container, so a multi-container pod is where a realistic declaration trips it.

Cluster Service names cost nothing against this cap. A fully-qualified <service>.<namespace>.svc.cluster.local name resolves from Service and EndpointSlice informers rather than from DNS answers, so prefer it for every in-cluster destination — it is both more precise and free.

The pod is the unit, not the container​

The kernel groups by pod. A pod with three containers has three expanded declarations, and the compiled RuntimePolicy is their union — so a sidecar's declared permissions widen the application container's. A mesh proxy alone would blow open any meaningful exec or open allow list.

Three consequences:

  • Known infrastructure sidecars are excluded from the union rather than merged, and the compiled policy is stamped with which ones — pod-level (sidecars excluded: istio-proxy).
  • Union widening is reported. When merging genuinely grants something a container that constrained that list never declared, compile-union-widened names the container, the list, and the extra values. The effective policy being looser than the application asked for is something the operator should be told, not something to discover later.
  • A pod containing an undeclared container does not compile at all. Not partially — at all. Enforcement becomes Not ready rather than partially protecting the pod. Excluded infrastructure sidecars do not count against this.

A single-container pod is never reported as widened. The warning fires only where a merge actually loosened something.

Observe first, always​

Every compiled kernel policy starts in observing mode. Compilation prepares controls; it is not a step in the primary Register → Observe → Enforce workflow.

Enabling enforcement requires an explicit operator action that records the authenticated actor and server time. No configuration flag, source, or score enables a policy on its own.

This is not caution for its own sake. A registration drafted from too short an observation window, or one written for a workload with periodic behavior that did not occur during it, can deny legitimate traffic. Observing shows what would be denied against real traffic before you enable enforcement.

Two further limits on promotion:

  • Enablement is refused when there is no registered workload or the readiness review fails. The registration's other preconditions — it names a purpose and an autonomy, and it compiles — are re-evaluated on every reconciliation.
  • The negative-form dns rule is always monitor, whatever the rest of the policy is doing. It only ever observes.

Compile findings​

Every outcome is reported as structured data with a stable code, so it can be displayed, filtered, and acted on rather than read out of a log.

CodeBlockingMeaning
compile-undeclared-containeryesA container in the pod has no declaration, so Enforcement is Not ready rather than partially active
compile-breadth-capyesA declaration matches more than the configured share of the filesystem or address space
compile-intern-capyesThe compiled domain allow list is past the sensor's per-pod limit
compile-promotion-refusedyesEnforcement was requested without a registered workload, a complete server-authored enablement record, or a passing readiness review
compile-render-failedyesThe rendered policy failed validation before anything was applied
compile-clampednoDeclared values were dropped because the platform ceiling does not permit them
compile-union-widenednoThe merged pod policy grants something a container never declared
compile-ports-droppednoDeclared ports have no kernel rendering
compile-open-mode-collapsednoA write-only path compiled as readable
compile-sidecar-excludednoA container was excluded from the union as infrastructure

A blocking finding means no policy is produced at all — not a partial one. A partially compiled policy would be a policy nobody wrote. For a running workload, AIControls keeps the policy it applied from the last declaration that compiled until the declaration compiles again or is withdrawn. See A registration that doesn't compile keeps the last working policy.

A registration with nothing kernel-renderable — gateway-only primitives, or tiers that expand to nothing — is not a failure. It compiles the gateway half and reports nothing-to-compile.

Read the compile result​

"compile": {
"status": "failed",
"mode": "monitor",
"ceiling": "platform",
"diagnostics": [
{
"code": "compile-breadth-cap",
"severity": "error",
"path": "behavior.open.read",
"detail": "behavior.open.read matches an estimated 100% of the filesystem, over the 50% cap. Broadest first: /** (~100%) …",
"attempted": ["/**"]
}
],
"l7": { "egressDomains": ["api.anthropic.com"], "ports": [443] }
}

status is compiled, nothing-to-compile, or failed. severity is error (blocking), warning, or info; only error sets status to failed. attempted and kept carry the values, so a client can act on them without parsing detail.

See also​