Inspect — observe, learn, and enforce agent behavior¶
Inspect watches the MCP tool calls flowing through the gateway, lets you
learn a behavior profile from real traffic, and then enforce it — so an
agent (or a stolen credential) that suddenly starts doing something it never did
before is flagged or blocked.
The model is borrowed from AppArmor's learning workflow and from service-mesh traffic views (think Kiali for agent tool calls):
OFF ──▶ OBSERVE ──▶ DRY-RUN (complain) ──▶ ENFORCE
│ │ │
record calls compute what WOULD be block calls that
only (zero blocked, but still allow fall outside the
risk) — review before enforcing learned profile
You are never auto-enforced into a corner: rules are suggested from observed
traffic, a human accepts them, you watch a dry-run prove the profile
against live traffic, and only then do you switch to enforce. Exactly the
complain → aa-genprof → aa-logprof → enforce loop, applied to tool calls.
Why¶
The gateway already has RBAC, a policy engine, redaction, and an audit log —
all of which answer "is this principal allowed to call this tool at all?".
Inspect answers a different question: "is this call normal for this
principal, compared to what it has actually been doing?" That catches the class
of problem RBAC can't — a credential that is legitimately allowed to call
query_logs, but is now calling it against namespaces it never touched, at 50×
its usual rate, right after it leaked.
The three modes¶
Set the starting mode with OMCP_INSPECT (default observe); it can also be
changed at runtime from the UI or PUT /api/inspect/mode.
| Mode | Records | Evaluates against profile | Blocks | Use it to |
|---|---|---|---|---|
off |
– | – | – | disable entirely |
observe |
✅ | – | – | learn what normal looks like (default, zero risk) |
dryrun |
✅ | ✅ (logs "would block") | – | prove a profile against live traffic before enforcing |
enforce |
✅ | ✅ | ✅ | block calls outside the accepted profile (entitled feature) |
observe and dry-run — the live Flows graph, learning a profile, and seeing
would-block deviations — are free. enforce (active blocking) requires an
entitlement (the inspect-enforce feature); without it the mode switch is
refused and the gateway keeps running in dry-run. Visibility is free;
enforcement is the licensed control.
observe is the default and is completely read-only — it adds no decision
to the call path, only a non-blocking recorder. enforce is the only mode that
can deny a call, and only ever denies calls that fall outside an accepted
rule (suggested-but-unreviewed rules never block anything).
What gets captured (and what doesn't)¶
For every tool call the recorder stores a signature, never the raw arguments:
- identity: principal id, auth kind, tenant
- tool: the tool name (e.g.
query_logs) - resource dimensions:
source/service/namespace— the real values (these are the equivalent of AppArmor's file paths) - argument shape: scalar args reduced to coarse buckets (e.g.
enrich_ipsbatch size →≤10 / ≤100 / ≤1000; a query window →5m / 1h / 1d); free-text queries (PromQL/LogQL) reduced to a structural fingerprint, not the literal query text - outcome: ok / error, latency
- decision: allow / would-block / blocked (in dry-run / enforce)
Arguments are passed through the gateway's existing
redaction layer before any shape is derived, so secrets and
PII never reach the inspection store. The store keeps shapes, not payloads — by
design, the literal query you ran is never persisted by Inspect.
The profile¶
A profile is a set of rules. Each rule says "this subject may call this tool within these bounds":
yaml
rules:
- subject: "key:ci-bot" # principal, or role:/product:/*
tool: query_logs
resources:
service: [payment-service, order-service]
args:
window: ["5m", "1h"] # learned buckets
provenance:
learnedFrom: 412 # observations
window: "2026-06-01..2026-06-13"
confidence: 0.98
status: accepted # suggested | accepted | rejected
Rules are derived from the observed window (POST /api/inspect/profile/derive),
land as suggested, and a reviewer accepts, edits, or rejects each
one. Only accepted rules participate in dry-run/enforce. Profiles persist to
OMCP_INSPECT_PROFILE_FILE (falls back to an in-memory profile when unset).
A call is a deviation when no accepted rule covers it. Deviation kinds:
new-tool— principal called a tool it has no rule fornew-resource— known tool, but a source/service/namespace outside the rulearg-out-of-range— known tool+resource, but an argument bucket outside the rulenew-principal— an identity with no rules at all
UI — the Inspect tab¶
Three sub-tabs, consistent with the rest of the console:
- Flows — a live service-mesh-style graph, Identities → Tools → Backends. Edge thickness is call volume; colour is green / amber / red for allowed / deviation / blocked. Click a node or edge to inspect the calls, the argument-shape distribution, and to turn an observed edge straight into a rule. A time-window selector and a live pause control sit on top, with the current mode shown as a chip.
- Profile — the learning workflow: a mode control (Observe → Dry-run → Enforce, with a confirmation before enforce), a "Learn from traffic" button that fills a review queue of suggested rules (accept / edit / reject, in bulk), and a table of accepted rules.
- Deviations — every call that fell outside the profile: who, which tool, what was unusual, and a one-click "accept into profile" or "confirm anomaly". In dry-run these are would-block; in enforce they are blocked.
The current mode is also shown as a chip in the masthead, so the governance posture is always visible.
API¶
All reads require the inspection:read permission; mutations require
inspection:write and are written to the audit log.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/inspect/mode |
current mode + counts |
| PUT | /api/inspect/mode |
set off/observe/dryrun/enforce |
| GET | /api/inspect/flows?window= |
aggregated flow graph (nodes + edges) |
| GET | /api/inspect/events?from&to&principal&tool&outcome&decision&limit |
raw observation stream |
| GET | /api/inspect/profile |
accepted + suggested rules |
| POST | /api/inspect/profile/derive |
derive suggestions from the observed window |
| PATCH | /api/inspect/profile/rules/:id |
accept / reject / edit a rule |
| GET | /api/inspect/deviations?window= |
calls outside the profile |
Configuration¶
| Env | Default | Meaning |
|---|---|---|
OMCP_INSPECT |
observe |
starting mode: off / observe / dryrun / enforce |
OMCP_INSPECT_FILE |
(unset) | JSONL path for the observation store; in-memory ring when unset |
OMCP_INSPECT_PROFILE_FILE |
(unset) | YAML/JSON path for the persisted profile |
OMCP_INSPECT_WINDOW |
24h |
default learning / flow window |
Air-gapped & privacy posture¶
Inspect makes no outbound calls — it only observes traffic that already
flows through the gateway, so the air-gapped guarantee is unchanged. It stores
argument shapes, not payloads, and runs everything through the existing
redaction layer first. The recorder is strictly non-blocking and never throws,
so observation can never slow down or fail a tool call.