Skip to content

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_ips batch 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 for
  • new-resource — known tool, but a source/service/namespace outside the rule
  • arg-out-of-range — known tool+resource, but an argument bucket outside the rule
  • new-principal — an identity with no rules at all

UI — the Inspect tab

Three sub-tabs, consistent with the rest of the console:

  1. 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.
  2. 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.
  3. 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.