Content Middleware Reference
The operations content middleware reaches, the two wire contracts, and the audit record. For
what content mediation is, see Content Mediation; for how to
configure it, the Content Middleware guides, which cover chain
entries and raw hooks; the section's own settings are in the
Configuration Reference.
Hooked operations
| Operation | Request | Response | Policy-checked first | Chain | Rewritable |
|---|---|---|---|---|---|
agentvisor:http:request | ✓ | ✓ | yes | ✓ | ✓ (a reply: raw hook only) |
agentvisor:http:upgrade | ✓ | yes | |||
agentvisor:mcp:tool:list | ✓ | ✓ | yes | ||
agentvisor:mcp:tool:call | ✓ | ✓ | yes | ||
agentvisor:mcp:resource:list | ✓ | ✓ | yes | ||
agentvisor:mcp:resource:read | ✓ | ✓ | yes | ||
agentvisor:a2a:agent:list | ✓ | ✓ | yes | ||
agentvisor:a2a:agent:describe | ✓ | ✓ | yes | ||
agentvisor:a2a:task:send | ✓ | ✓ | yes | ||
agentvisor:a2a:task:get | ✓ | ✓ | yes | ||
agentvisor:a2a:task:cancel | ✓ | yes | |||
agentvisor:a2a:task:stream | ✓ | ✓ (per update) | yes | ||
agentvisor:store:create | ✓ | yes | |||
agentvisor:store:update | ✓ | yes | |||
agentvisor:store:read | ✓ | yes | |||
agentvisor:store:search | ✓ | yes | |||
agentvisor:thread-kv:write | ✓ | yes | |||
agentvisor:thread-kv:read | ✓ | yes | |||
content:run:input | ✓ (ingress) | yes, at run creation | |||
content:run:output | ✓ | no — the run was authorized when it was created | |||
content:stream:chunk | ✓ (per chunk) | no | |||
content:telemetry:spans | ✓ | no | |||
content:telemetry:log | ✓ | no | |||
content:checkpoint:save | ✓ | no | |||
content:checkpoint:write | ✓ | no | |||
content:checkpoint:load | ✓ | no | |||
content:checkpoint:list | ✓ | no |
- Request / Response — the phase on which the operation is hooked.
raw_requestruns on the request phase andraw_responseon the response; a hook or chain entry scoped to an operation on a phase it is not hooked on refuses to start.phaseis the direction of the RPC, so oncontent:run:inputthe request phase is content arriving at the agent. - Policy-checked first — whether the Policy Engine evaluates the call before content
middleware sees it. An
agentvisor:operation is one the Policy Engine also evaluates; acontent:operation exists for content mediation alone, and no PolicyDomain rule reaches it. - Chain — whether chain entries run there. Elsewhere only raw hooks run.
- Rewritable — whether a
modifyverdict is applied. Elsewhere it is ignored and logged.
Operation patterns in operations: and exclude_operations: accept a trailing * as a prefix
glob, and must match at least one operation in this table on the hook's phase.
Checker contract
What an http chain entry sends and expects. Versioned by the X-AgentVisor-Content-Protocol
request header, currently 1. See Writing a Content Checker.
Request
| Field | Type | Meaning |
|---|---|---|
operation | string | always agentvisor:http:request today |
resource | string | the destination's MRN |
phase | string | request or response |
principal | object | {"claims": {…}} — the subject only, unless widened by principal_claims; absent when principal_claims: [] |
thread_id, run_id, agent, agentvisor_id | string | where the call came from |
spans | array | {role, text, ref, number} — the text to inspect; an empty list on the response phase, which is recorded but not enforced and reaches a checker only with observe_response: true. ref is a JSON Pointer, or key:<object pointer>#<n> for an object key, n counting the object's keys from 0, each once, in byte order after JSON decoding; a key cannot be edited. number is true on a span lifted from a JSON number, which cannot be edited either, and absent otherwise. ref is empty ("") on a span holding a whole body no extractor recognizes, or its text with escapes decoded; such a span is read-only. role is the provider's own, or empty where there is none |
fields | object | what AgentVisor knows about the call: url and method on both phases; model, max_tokens, max_completion_tokens, route, path_model and body_unreadable on the request; model, request_model, extractor, usage and usage_absent on the response. See Checker fields |
Checker fields
What AgentVisor sends in the request's fields. It is kept apart from the top-level keys, so the
top-level shape stays the same as keys are added.
| key | phase | what it is |
|---|---|---|
url, method | both | the outbound URL and HTTP method |
model | request | the model the request names, if any |
max_tokens, max_completion_tokens | request | the output limit the request asked for, under whichever name it used |
route | request | the extractor whose path the URL matched, e.g. openai_chat, on a recognized LLM API; absent elsewhere |
path_model | request | the model the URL names: a Bedrock model ID or ARN, or an Azure deployment name |
body_unreadable | request | true when the call is to a recognized LLM API but its body is not a JSON object, or names model, max_tokens or max_completion_tokens more than once, or in another letter case such as Model |
model | response | the model the provider answered with |
request_model | response | the model the request asked for |
extractor | response | the extractor that recognized the call, e.g. openai_chat |
usage, usage_absent | response | the provider's token counts, or why there are none |
model and the output limits are read from top-level JSON fields, so they arrive even when no
extractor recognized the request. The last three rows arrive only when one did.
Response
| Field | Type | Meaning |
|---|---|---|
action | string | required — permit, deny or modify |
reason | string | why; recorded |
edits | array | {ref, text} — replacement text for a span; honored on modify only |
parameter_edits | array | {ref, value} — a numeric generation parameter, at one of /max_tokens, /max_completion_tokens, /temperature, /top_p, /top_k, /n, /presence_penalty, /frequency_penalty. Honoured on modify only; value is required. A token limit is a whole number of at least 1 and is only ever lowered: the lowest edit wins, and none exceeds what the request asked for; on a request that set none, the edit adds it |
findings | array | {ref, type, confidence} — recorded whatever the verdict |
fields | object | your annotations, recorded as checker_fields and not passed to later checks or the provider; every checker's fields share one record, a key from a checker listed later in the chain replacing one of the same name from a checker listed earlier, on either phase; keys AgentVisor sent in the request's fields are dropped, and past 4 KiB encoded the whole object is replaced by {"truncated": true, "bytes": N} |
A non-2xx status, unparseable body or unrecognised action is an error, handled by the entry's
on_error. Redirects are not followed: a 3xx is a failed call.
Raw hook contract
What raw_request and raw_response send and expect. See
Raw Content Hooks.
Request
| Field | Type | Meaning |
|---|---|---|
operation | string | one of the hooked operations |
resource | string | the MRN the call names |
phase | string | request or response |
principal | object | {"claims": {…}} — every claim in the caller's token. An http checker is sent only the subject by default; a raw hook always gets them all |
thread_id, run_id, agent, agentvisor_id | string | where the call came from |
method, url | string | HTTP only; absent elsewhere |
headers | object | HTTP only — name to list of values |
status | number | HTTP response only — the upstream's status code |
batch | object | raw_response with delivery: batched only — {correlation_id, sequence, final} |
body | string | base64. On HTTP, the bytes as sent or received. For content:run:input and content:run:output, the run's input or output as JSON; for content:stream:chunk, {"chunk_type": …, "data": …}; elsewhere, the RPC message as protojson |
Response
| Field | Type | Meaning |
|---|---|---|
action | string | required — permit, deny or modify |
reason | string | why; recorded whole. Where it ends a run (content:run:output, content:stream:chunk), the run's error keeps at most 4 KiB of it, marked as truncated |
headers | object | replacement headers; applied on a rewritable operation only. On a request, names are matched regardless of case, so authorization is the Authorization header that credential substitution reads; two spellings of one name are one header with both values |
body | string | replacement body, base64; applied on a rewritable operation only |
Any status other than 200 is an error, handled by the hook's on_error. Redirects are not
followed. A modify on a rewritable operation must carry headers, body or both. There is no
field for the destination. The response may be at most four times the size of the request it
answers, or 4 MiB, whichever is larger.
Service URLs
The url of an http checker or a raw hook must be http or https with a host, and is checked
at startup. Logs, errors and the audit record show only its scheme and host, never its credentials,
path or query. Percent-encode any credentials in it: a URL with an @ after its host is refused,
since that is how an unencoded password is misread as a host.
Audit record
Each verdict is emitted as an OTLP log record with the body content middleware verdict and these
attributes. Empty values are omitted.
| Attribute | Meaning |
|---|---|
operation, resource, phase | what was inspected; on HTTP, resource names the host and path |
method | HTTP only — the request method. The URL's query string is never recorded |
enforced | whether this record's verdict was acted on. false on every record from an async entry, which nothing waits for, and on every response record where a chain entry ran, since the response phase only observes. It describes the chain's verdict, so a response record that also names your raw hook in evaluated is still false even though your hook's allow decided the reply |
verdict | permit, deny or modify; absent when nothing was inspected |
reason | the verdict's reason, or the error |
decided_by | the entry that denied; absent when AgentVisor itself refused, under on_unknown_format: deny or when a verdict's edits could not be applied. On a reply a raw_response hook refused, it names the hook and reason is the hook's; any findings and checker_fields come from response-phase entries that observed the reply, since a raw hook reports neither |
modified_by | each entry whose edits were applied. Empty on a deny, since nothing was sent |
raw_rewritten_by | the raw hook that replaced the payload. Kept on a deny, because the chain judged the replaced payload |
evaluated | every entry that ran, listed after your raw hook when it ran and let the payload through. An async entry is listed only on its own record, and an entry that abstained is not listed |
failed_open | each entry or raw hook that failed and was let through by on_error: allow. It is listed in evaluated too, because it was called |
spans | how many spans were inspected |
max_spans_exceeded | how many spans the request had, when that was more than max_spans, so it was handled by on_unknown_format; set on whatever record the request produced |
findings | JSON array of {ref, type, confidence} — never the matched text |
parameter/<name> | a generation parameter that was changed, with its new value — e.g. parameter/max_tokens. Absent on a deny, since nothing was sent |
usage.<name> | token counts, one attribute per count. When more than one entry reports counts, or why there are none, the record keeps the answer of the entry listed first in the chain, on either phase |
usage_absent | why there are no counts, when a meter looked and found none, including a reply cut off before the provider reported them |
checker_fields | JSON object of what an http checker returned in fields, up to 4 KiB |
not_inspected | why a payload was not examined. A payload that could not be rendered as JSON for a raw hook goes ahead uninspected, whatever on_error says |
principal | the subject only |
thread_id, run_id, agent, agentvisor_id, execution.id, trace_id | correlation |
A record has warning severity when an entry failed open, or when a verdict that was enforced denied; otherwise it has info severity.
A run's content:run:input record is written when the run is created, before any execution
exists, so it carries no execution.id. Each attempt's content:run:output record carries that
attempt's. Join a run's records on run_id. When the input is refused, no run is created, so that
record's run_id, and a stateless run's thread_id, name nothing else.
AgentVisor never writes conversation content into an attribute. Text your checker or raw hook
returns is recorded as sent, in reason, in findings and in checker_fields, so whatever it
puts there reaches the audit stream.