Runtime Guard Reference
Lookup material for Runtime Guard — endpoints, the capability-state contract, reconciliation row and destination fields, suppression reasons, and destination identity rungs. For what these mean and how to read them, see Runtime Guard.
Endpoints
Three read-only endpoints. All require an admin or viewer role; a developer (builder) token cannot read cluster-wide runtime findings.
| Endpoint | Returns |
|---|---|
GET /api/v1/runtime/status | Capability snapshot: CRD presence, per-node sensor and BPF-LSM state, RBAC gaps, ingest lag |
GET /api/v1/runtime/reconciliation | One row per workload for the window, sorted by bypassedDestinations descending |
GET /api/v1/runtime/workloads/{key}/observations | Raw observations for one workload, grouped by signal family |
curl "$YOUR_WORKSPACE_URL/api/v1/runtime/reconciliation?window=6h" \
-H "Authorization: Bearer $ADMIN_TOKEN"
The window parameter
/reconciliation and /observations accept an optional window duration (for example 15m, 1h, 6h, 24h).
- Default:
1h - Clamped to the range
5m–24h - An unparseable value falls back to the default rather than erroring
The key path parameter
The workload key is the canonical <namespace>/<kind>/<name> string, percent-encoded so it arrives as a single path segment:
curl "$YOUR_WORKSPACE_URL/api/v1/runtime/workloads/search%2FDeployment%2Frag-indexer/observations" \
-H "Authorization: Bearer $ADMIN_TOKEN"
The disabled shape is a contract, not an error
When the feature is off, the tenant is Nirmata-hosted, or the controller could not start, all three endpoints return HTTP 200 with enabled: false and a human-readable reason. They never return a 4xx or 5xx for this case.
The enabled and reason fields are common to all three. The rest of each payload is that endpoint's own data fields, present but empty:
| Endpoint | Empty-data fields in the disabled shape |
|---|---|
/status | nodes: [], rbacGaps: [] |
/reconciliation | rows: [] |
/workloads/{key}/observations | groups: [], plus workloadKey echoing the key you requested |
For example, /reconciliation:
{
"enabled": false,
"reason": "unavailable on Nirmata-hosted (SaaS) tenants",
"rows": []
}
This is deliberate: a client — and the admin console page — must be able to state why the surface is dark rather than render an empty view that looks like a clean result. The reasons are:
reason | Meaning |
|---|---|
not running in Kubernetes | The environment predicate failed; the config flag is irrelevant |
unavailable on Nirmata-hosted (SaaS) tenants | Environment predicate; fails closed even with runtimeGuard.enabled: true |
disabled by config | Environment is eligible, but runtimeGuard.enabled is false |
runtime guard is enabled but inactive on this replica (no Kubernetes API access or no shared store) | Configured and eligible, but this replica did not start the controller |
runtime guard is not configured | No configuration is present at all |
Capability states
Every capability fact is three-valued, and unknown is never conflated with unavailable.
| State | Meaning | Read it as |
|---|---|---|
available | The capability was positively established | Evidence of this class can be produced |
unavailable | Absence was positively established | Evidence of this class cannot be produced here |
unknown | The evidence to decide is not present | Cannot tell — not clean, and not absent |
unknown arises from, among others: no capability probe has completed yet; a node's status shard is older than the freshness window (two minutes — four missed 30-second flushes); or no positive BPF-LSM evidence source is wired.
The distinction is load-bearing rather than pedantic. A node that has simply been idle since the sensor started looks identical to a node whose hook never loaded, so idleness must yield unknown. Reporting it as unavailable would be a false negative; reporting it as available would be a fail-open — and a fail-open of exactly this kind shipped once and had to be fixed, when a hosted runner with no BPF-LSM token was reported as capable.
status fields
| Field | Type | Notes |
|---|---|---|
enabled | bool | False means read reason; all other fields are empty |
crd / crdReason | state / string | Whether the RuntimePolicy CRD is served. Re-probed every 5 minutes, so a later sensor install is picked up without restarting AIControls |
bpfLsm / bpfLsmReason | state / string | The policy-level BPF-LSM answer across the fleet |
nodes[] | array | Per-node node, sensorAlive, bpfLsm, reason |
rbacGaps[] | array | Permissions the ServiceAccount lacks, from the startup SelfSubjectAccessReview: group, resource, verb, namespace |
ingestLag | object | See below |
probedAtUnix | int | When the capability probe last ran. 0 means never — state is unknown, not clean |
ingestLag
Reconciles the sensor's own counters against what AIControls stored.
| Field | Meaning |
|---|---|
lag | Kernel observations the sensor recorded that are not reflected in stored findings. A figure > 0 indicates possible loss |
perNode | The same shortfall, per node |
scrapedAtUnix | When the sensor counters were last read. 0 means never scraped — which is unknown, not clean |
scrapeError | Set when either side of the comparison could not be read. When present, any lag figure is untrustworthy and completeness is unknown |
A small transient lag is usually just the sensor's roughly-10-second report flush. A figure that persists across refreshes means findings were dropped — kernel map pressure, or the sensor's 500-result-per-report cap, after which later findings are discarded permanently.
On a busy cluster the common cause is network volume: that policy carries no allow list by design, so in-cluster service-to-service traffic is reported alongside external traffic and competes for the same cap, and a genuine provider destination can be crowded out. This is the operator-visible symptom of an incomplete table — loss surfaces as a persistent lag figure, not as a low count with nothing to explain it.
Reconciliation payload fields
Top-level fields on /reconciliation, alongside enabled and reason.
| Field | Type | Meaning |
|---|---|---|
windowStartUnix / windowEndUnix | int | The window actually used, after clamping |
rows[] | array | One row per workload — see below |
networkDetectorDisabled | bool | True when the network-based detector stood down for this window. Two causes produce it — the egress-NAT auto-detect, and unusable cluster facts — so the flag alone does not say which. Network-sourced evidence is suppressed with the matching reason (egress-nat-detector-disabled or cluster-facts-unavailable); DNS-based findings are unaffected in either case |
disabledReason | string | Why, in prose, when networkDetectorDisabled is true. This is what distinguishes the two causes — present so a client can state the reason rather than render a thinner table with no explanation |
ingestLag | object | Repeats /status's lag figure so the table can show its banner without a second request |
networkDetectorDisabled: true means the window is thinner, not cleanerConnections are the dominant evidence class. A window where the network detector stood down is backed by DNS evidence alone, so a low bypassedDestinations in that window carries much less weight than the same number in an ordinary one. Surface disabledReason — never present the count on its own, and never assume the egress-NAT cause: read the reason to find out which stand-down fired.
Reconciliation row fields
| Field | Type | Meaning |
|---|---|---|
workloadKey | string | Canonical <namespace>/<kind>/<name> |
namespace, kind, name | string | The same, split out |
bypassedDestinations | int | The headline. Count of distinct unsuppressed provider destinations this workload demonstrably reached outside governance — by kernel-observed connection, or by resolving the provider name itself. Both evidence classes feed this one set; there is no separate connection count. A set count: unit-free, unaffected by DNS caching or connection reuse. 0 always means the set was empty |
m | int | Raw evidence. Kernel events (connections + resolutions) to unsuppressed external provider destinations |
n | int | Raw evidence. Gateway-recorded calls joined to this workload — every audited request type (MCP tool calls, LLM requests, and egress), not only provider traffic |
dnsOnly | bool | true means the row's entire unsuppressed evidence is DNS resolutions, with no observed connection behind any of it — a weaker finding than a connection-backed one. false means either at least one destination was connection-backed or the row has no unsuppressed evidence at all. It must be read together with m / bypassedDestinations, never alone |
joinMethod | string | Always name-based: a heuristic join of gateway identities to workloads, never a cryptographic binding |
attributionConfirmed | bool | False means the pod could not be resolved to an owning workload and the row fell back to pod-level attribution. The row keeps its full headline count — the evidence is real; only who is uncertain. Rendered as "attribution unconfirmed" |
attributionNote | string | pod-level (multi-container) when a known mesh or telemetry sidecar was detected: the sensor attributes to the pod cgroup, so sidecar egress cannot be separated from the application's |
destinations[] | array | Per-destination evidence, suppressed entries included |
m and n are not comparablem counts kernel events; n counts gateway requests; the populations do not overlap. HTTP/2 multiplexing means one connection carries many requests, and a governed call produces no workload-side kernel evidence at all. Never compute m - n — it is not a quantity. The headline is bypassedDestinations, and it is derived from kernel evidence alone.
dnsOnly: false is not by itself a connection-backed findingdnsOnly is true only when the row has unsuppressed evidence and all of it is resolutions. A row with no unsuppressed evidence — m: 0, bypassedDestinations: 0 — is therefore false as well, with no connection behind it. The network observation gives nearly every pod in the cluster a row, so most rows in a healthy cluster are exactly that shape. The pair to test is dnsOnly: false and a non-zero m; the admin console likewise does not show a network evidence badge on a row with no evidence.
Rows are sorted by bypassedDestinations descending, then by m descending, then by workload key.
destinations[] fields
| Field | Type | Meaning |
|---|---|---|
destName | string | Resolved display name, or ip:<address> (unattributed) when no identity could be established |
destIP | string | Destination address; empty for DNS-only evidence that never produced a connection |
source | network | dns | The strongest evidence that fired. network when any connection to this destination was observed, otherwise dns — the destination was resolved but no connection to it was seen. A resolution is not a connection; both are observed, and this field is which one you have |
behaviors[] | array | Every sensor behavior that fired for this destination |
count | int | Connection count when source is network; resolution count when dns |
governedCalls | int | Gateway-recorded calls this workload made to this same host. Rendered as "also governed (n)". On an unsuppressed finding this means split traffic and strengthens the finding; it never clears it. Sourced from egress-plane audit events only — see the note below |
rung / confidence | string / float | Destination identity ladder result — see below |
kind | provider | infra | imds | internal | Finding classification |
suppressed / suppressReason | bool / string | Whether this destination is excluded from the count, and why. Suppressed destinations remain in the list |
governedCalls requires the egress planeOnly audit events with a request type of egress carry a destination host, so governedCalls is derived from network egress filtering alone. Where that plane is not enabled, governedCalls is 0 on every destination and the "also governed" tag never appears — which is not evidence that traffic was unsplit. n is unaffected: it counts every audited request type.
Finding kinds
kind | Meaning |
|---|---|
provider | An external destination. Counts toward bypassedDestinations when unsuppressed |
infra | Package registries and similar. Suppressed from the count, retained in the list |
imds | The cloud instance metadata service (169.254.169.254). Never suppressed — it gets its own finding kind rather than being swept up with link-local traffic — and not counted as a provider connection |
internal | Cluster or node infrastructure suppressed by a deterministic rule |
Suppression reasons
suppressReason | What it excludes |
|---|---|
coredns | Cluster DNS |
kube-apiserver | The Kubernetes API server |
in-cluster | In-cluster addresses and *.svc.cluster.local names, derived from the API server rather than guessed from CIDR configuration |
node-or-link-local | Node and link-local addresses — except 169.254.169.254 |
aicontrols-gateway | AIControls' own Service, by IP or by resolved name. This is the expected shape for a correctly governed workload |
reverse-dns | in-addr.arpa PTR questions — resolver plumbing, not destinations |
search-domain-expansion | Variants generated by the Kubernetes default ndots:5 resolver configuration, which expands every multi-label question several-fold |
sidecar-control-plane | Service-mesh and telemetry control planes, which are never AI providers |
package-registry | Package registries (kind: infra) |
egress-nat-detector-disabled | Network-sourced evidence in a window where the egress-NAT auto-detect disabled the network detector. DNS-based findings are unaffected |
cluster-facts-unavailable | Network-sourced evidence in a window where the API-server-derived cluster facts were missing or stale, so an address could not be told apart from an in-cluster one. DNS-based findings are unaffected — they are classified by name |
The last two are stand-down reasons rather than per-destination rules: they apply to network-sourced evidence across the whole window and always come with networkDetectorDisabled: true and a disabledReason. Every other rule in the table matches on a name, which is why unusable cluster facts leave an address-only connection with nothing to classify it and force the stand-down.
Separately from suppression, findings attributed to AIControls' own pods — including a previous AIControls pod — are dropped before any counting. The proxy is an in-cluster hop, so a governed workload's provider calls leave the AIControls pod; counting them would inflate the number in the customer's favour.
Destination identity rungs
An observed connection yields an address, not a name: the kernel supplies IPv4 addresses and TLS SNI is invisible to the sensor. Destination names are therefore resolved through a confidence-graded ladder. Resolution is monotonic-upgrade-only within a window.
rung | Source | confidence |
|---|---|---|
R0 | The pod's own DNS answer was observed | 0.95 |
R2 | Cluster facts from the API server — Service ClusterIPs, EndpointSlice addresses, Pod IPs | 0.98 |
R6 | No identity could be established. destName is rendered ip:<address> (unattributed) rather than guessed | 0 |
Rungs R1, R3, R4, and R5 are reserved for later work and are not produced today; they would carry 0 confidence if they appeared.
R2 is what names the in-cluster half of a connection. The non-cluster half lands on R6 whenever the sensor did not observe the pod's own DNS answer for it — encrypted DNS, a cached answer, or a hardcoded IP. An R6 destination is a real, counted finding whose name is missing, not a discarded one. Closing that gap needs the reserved rungs, which are not implemented.
The sensor is IPv4-only pre-1.0, so an IPv6 destination produces no row at all — not an unattributed one, not a flagged one. No field, rung, or reason records that IPv6 traffic was outside the sensor's reach. Read an empty result as evidence about IPv4 alone: it cannot distinguish "no IPv6 traffic occurred" from "IPv6 traffic occurred and was never observed".
Observation families
/observations groups raw findings by signal family. AIControls authors one monitor-mode observation policy per family below (except other), covering every namespace but kube-system and kube-public. Families available depend on node capability — open and exec require nodes booted with lsm=bpf.
family | Admin console heading | Requires BPF-LSM | Feeds bypassedDestinations |
|---|---|---|---|
dns | Names resolved (DNS) | No | Yes |
network | Destinations reached (network) | No | Yes |
open | Paths opened (file) | Yes | No |
exec | Binaries executed (exec) | Yes | No |
other | Other observations | — | No |
The network policy carries a deny-all match with no allow list, so every connection a governed pod opens is reported, in-cluster traffic included. Allow-listing provider destinations would hide the traffic the feature exists to surface; uninteresting destinations are removed after the fact by suppression rules, which stay visible with a reason attached. The cost is volume: on a busy cluster in-cluster chatter competes with external traffic for the sensor's 500-result-per-report-per-node cap, and the symptom of losing that race is a persistent ingestLag figure, never a clean-looking result.
Each item carries rule, behavior, subject, destIP, destName, count, node, podName, namespace, result, mode, and observedAt (Unix seconds).
See also
- Runtime Guard — the model behind these fields
- Enable Runtime Guard — setup and verification
- Security Events — the gateway-side event log