Authorization Enrichment
Authorization enrichment lets you translate low-level gateway signals (tool names, arguments, metadata) into rich, semantic operation and resource identifiers before the policy engine evaluates the request. This decouples PDP policies from implementation details that change independently.
Motivation
Without enrichment, the Policy Decision Point (PDP) evaluates requests using the raw MRNs that AgentVisor generates automatically:
operation: agentvisor:mcp:tool:call
resource: mrn:agentvisor:mcp:github/list_commits
This works for coarse access control ("can this principal call any GitHub tool?"), but fine-grained rules often need argument-level semantics: "can this principal list commits on this specific repository?" Writing argument-aware policy inside the PDP tightly couples policy to tool semantics—every new tool requires a PDP change.
Enrichment solves this by running a lightweight, per-server enrichment step on the host before the PDP is consulted. The enrichment step transforms the raw signal into a stable, semantically rich identifier that the PDP can evaluate using simple MRN selectors.
# Before enrichment
operation: agentvisor:mcp:tool:call
resource: mrn:agentvisor:mcp:github/list_commits
# After enrichment
operation: mcp:tool:github:repo:api:commits:list
resource: mrn:agentvisor:github:repo:api
The PDP now evaluates a structured, argument-aware identifier. Policies can grant access to specific repositories without knowing anything about MCP tool argument structures.
How Enrichment Works
This section describes enrichment independent of how the decision logic is implemented. AgentVisor ships two built-in providers — Rego (an in-process OPA policy) and HTTP (a remote service) — and the concepts below apply to both equally.
Request Flow
- Agent calls a gateway operation (MCP tool call or A2A task)
- Enrichment pipeline executes before the PDP is consulted
- If enrichment produces a result, the operation and/or resource are replaced
- The original values are preserved in the policy context (
context.mcp.original_*orcontext.a2a.original_*) - PDP evaluates against the (potentially enriched) operation and resource
Input Signals
Enrichment receives a common set of signals about the request. Fields populated depend on the gateway type.
| Field | Type | MCP | A2A | Description |
|---|---|---|---|---|
server_name | string | ✓ | — | MCP server name as configured in mcp.servers[].name |
tool_name | string | ✓ | — | MCP tool name being called |
agent_name | string | — | ✓ | A2A agent name as configured in a2a_gateway.agents[].name |
agentvisor_id | string | ✓ | ✓ | The resolved AgentVisor ID (config section id:), identifying the AgentVisor instance making the request. Empty string "" when id: is not configured. Distinct from agent_name, which identifies the LangGraph graph/agent, not the AgentVisor instance. |
operation | string | ✓ | ✓ | Original operation (e.g., agentvisor:mcp:tool:call, agentvisor:a2a:task:send) |
resource | string | ✓ | ✓ | Original resource MRN (e.g., mrn:agentvisor:mcp:github/list_commits) |
arguments | object | ✓ | — | Tool call arguments (empty object when none) |
metadata | object | — | ✓ | Task metadata passed by the agent (empty object when none) |
message_role | string | — | ✓ | Role field from the A2A message (e.g., user) |
principal_claims | object | ✓ | ✓ | JWT claims from the calling principal (e.g., sub, email, custom claims) |
site_context | object | ✓ | ✓ | Site-specific overrides from the site's enrichment.context config (see Configuration) |
For absent fields (e.g., arguments on an A2A call, or site_context when no context is configured), the value is an empty object {}, never undefined.
How these signals are delivered depends on the provider: a rego policy reads them as input.<field> (see Rego Provider); an http service receives them as top-level fields in a JSON request body with the same names (see HTTP Provider).
The Enrichment Result
Enrichment produces a result made of up to three optional pieces of data. Any field may be omitted (or left empty) to keep the original value.
| Field | Type | Description |
|---|---|---|
operation | string | Replaces the operation passed to the PDP. Omit or set to "" to keep the original. |
resource | string | Replaces the resource MRN passed to the PDP. Omit or set to "" to keep the original. |
context | object | Key-value pairs merged into the top-level policy context. Useful for passing enriched data to the PDP without replacing the resource. |
How this result is expressed depends on the provider — a rego policy returns it as a result rule (see Rego Provider); an http service returns it as a JSON response body (see HTTP Provider).
Fail-Open Behavior
Enrichment is fail-open: if no result is produced, the original operation and resource are used as-is.
| Condition | Behavior |
|---|---|
| Provider produces no result (e.g., no matching condition) | Nil result; original values used |
| Provider returns an error (e.g., evaluation error, network failure, timeout, malformed response) | Error logged as warn; nil result; original values used |
result.operation is empty string | Original operation used |
result.resource is empty string | Original resource used |
This means enrichment can never block a request on its own — only the PDP can deny. Adding enrichment to an existing server is a safe, non-breaking change. The specific conditions that trigger each row above differ by provider — see Rego Provider and HTTP Provider for details.
Original Values in Policy Context
When enrichment produces a result, the original operation and resource are preserved in the policy context so PDP rules can reference them if needed:
- MCP:
context.mcp.original_operation,context.mcp.original_resource - A2A:
context.a2a.original_operation,context.a2a.original_resource
Configuration
Enrichment is configured per MCP server or per A2A agent using an enrichment: block, either fully inline or by referencing a shared, centrally-defined configuration. Two built-in providers are available: rego (an in-process OPA policy, see Rego Provider below) and http (delegates to a remote HTTP service, see HTTP Provider below).
Inline configuration
This is the original model and continues to work unchanged — each server or agent can configure its own isolated provider inline:
mcp:
servers:
- name: github
transport: streamable_http
url: "https://api.githubcopilot.com/mcp/"
credentials:
type: bearer_token
source: env
env_var: GITHUB_TOKEN
enrichment:
provider: rego
rego_policy: /etc/agentvisor/policies/github-enrichment.rego
| Field | Required | Description |
|---|---|---|
provider | Conditional | Provider type: rego or http. Required unless ref is set; mutually exclusive with ref. |
options | Conditional | Provider-specific options (required for http). Mutually exclusive with ref. |
rego_policy | Conditional | Path to a Rego policy file on the host filesystem (rego provider). |
rego_policy_inline | Conditional | Inline Rego policy string (rego provider). |
ref | No | Name of a central enrichment.definitions[] entry to use instead of inline provider/options/rego fields. |
context | No | Arbitrary key/value map layered onto the resolved configuration as site_context (see below). |
headers | No | http-provider-only header overrides layered onto the resolved configuration (see below). |
Fields specific to a single provider (rego_policy/rego_policy_inline for rego; options.* for http) are detailed in that provider's section below.
Central definitions and ref
When multiple MCP servers or A2A agents should delegate to the same enrichment backend, define it once under the top-level enrichment.definitions section and reference it by name — instead of copy-pasting the same provider/options/rego fields at every site:
enrichment:
definitions:
- name: corp-http-enricher
provider: http
options:
url: https://enrich.example.com/enrich
timeout: 5s
tls:
mode: verify-full
ca_file: /etc/ssl/enrich-ca.pem
headers:
- { name: Authorization, value_env: ENRICH_TOKEN }
- name: repo-rego
provider: rego
rego_policy: /etc/agentvisor/policies/enrich.rego
mcp:
servers:
- name: github
enrichment:
ref: corp-http-enricher
context:
site: github-mcp
environment: prod
- name: gitlab
enrichment:
ref: corp-http-enricher # same backend, different site
context:
site: gitlab-mcp
environment: prod
- Definition
namevalues must be unique — a duplicate name is a config validation error at startup. - Referencing an unknown
reffails startup with an error naming the site and the missing ref, not a first-request failure. - Setting
reftogether with any inlineprovider/options/rego_policy/rego_policy_inlineon the same site is a startup validation error (mutually exclusive). - Definitions themselves must be fully inline — a definition cannot itself set
ref.
Per-site context and headers overrides
Sites can layer additional, site-specific data on top of a shared or inline configuration — this works identically whether or not the site uses ref:
context— an arbitrary map merged into the provider'ssite_context. It's provider-agnostic: exposed asinput.site_contexttoregopolicies (see Input Signals) and as asite_contextfield in thehttpprovider's request body. Use it to tell a shared backend which site, tenant, or environment a request came from, beyond whatserver_name/agent_name/operationalready convey.headers— a list of{name, value, value_env}entries merged into the resolvedhttpprovider's headers, by header name (a matching name overrides, a new name is appended). Meaningless forrego: settingheaderson a site whose resolved provider isn'thttplogs a startup warning and is dropped rather than failing.
See the Configuration Reference for the full field reference, including the http provider's options.* schema.
Rego Provider
The rego provider evaluates an in-process OPA policy on the host. It's the default, most broadly applicable option: it requires no external service, runs with the lowest latency, and is the right starting point unless enrichment logic already lives in a system you'd rather delegate to (see HTTP Provider).
Package and Query Path
All enrichment policies must use the package agentvisor.authz.enrichment and produce a rule named result. The host evaluates the query data.agentvisor.authz.enrichment.result. result's fields are exactly those described in The Enrichment Result — all optional.
package agentvisor.authz.enrichment
result := {
"operation": "mcp:tool:github:repo:api:commits:list",
"resource": "mrn:agentvisor:github:repo:api",
"context": {
"tenant": "acme"
}
}
Partial Rule Pattern
Use partial (conditional) rules to match specific tools or metadata values. When no condition matches, the rule is undefined and the original values are used (fail-open).
package agentvisor.authz.enrichment
# Only fires when tool_name == "list_commits" and the repo argument is present
result := {"operation": op, "resource": res} if {
input.tool_name == "list_commits"
repo := input.arguments.repo
op := sprintf("mcp:tool:github:repo:%v:commits:list", [repo])
res := sprintf("mrn:agentvisor:github:repo:%v", [repo])
}
Write one partial rule per tool (or group of tools), with mutually exclusive conditions — Rego has no rule-ordering/priority mechanism here, so "first match wins" is not how this is evaluated. If no rule's condition is true, result is undefined and the original values pass through (fail-open). If more than one rule's condition is simultaneously true and they disagree on the produced value, OPA raises an eval-conflict error ("complete rules must not produce multiple outputs") — the provider's Enrich() call fails, and the pipeline treats that the same as no result: it logs the error and falls back to the original, un-enriched operation/resource (fail-open, same outcome as no rule matching).
OPA v1 Syntax
Enrichment policies must be valid OPA v1 Rego. Use if for rule bodies and := for local variable assignment.
# OPA v1 — use `if` keyword
result := value if {
condition
}
Rego Configuration
A rego policy is supplied one of two ways — set exactly one:
| Field | Description |
|---|---|
rego_policy | Path to a Rego policy file on the host filesystem. Recommended for non-trivial policies. |
rego_policy_inline | Inline Rego policy string. Convenient for short policies in development or for policies managed entirely in agentvisor.yaml. |
rego_policy and rego_policy_inline are mutually exclusive — setting both is a config validation error.
Examples
MCP: GitHub Server
Map GitHub MCP tool calls to repository-scoped operation and resource MRNs. This lets PDP policies grant or deny access per repository without knowing about GitHub's tool argument structure.
Policy (the exact Rego policy AgentVisor ships and tests against):
# Enrichment policy for the GitHub MCP server.
#
# Maps tool name + argument values to semantic operation and resource MRNs.
# The PDP evaluates these stable identifiers rather than raw MCP tool names,
# enabling repository-scoped access control without coupling policy to tool
# argument structure.
#
# When no rule matches (e.g. an unlisted tool), the rule is undefined and
# the original AgentVisor MRNs pass through to the PDP unchanged (fail-open).
package agentvisor.authz.enrichment
# list_commits — scope policy by the target repository.
result := {"operation": op, "resource": res} if {
input.tool_name == "list_commits"
repo := input.arguments.repo
op := sprintf("mcp:tool:github:repo:%v:commits:list", [repo])
res := sprintf("mrn:agentvisor:github:repo:%v", [repo])
}
# create_issue — scope policy by the target repository.
result := {"operation": op, "resource": res} if {
input.tool_name == "create_issue"
repo := input.arguments.repo
op := sprintf("mcp:tool:github:repo:%v:issue:create", [repo])
res := sprintf("mrn:agentvisor:github:repo:%v", [repo])
}
# search_repositories — uses a stable, query-independent MRN.
# The query text is not included in the resource to avoid unbounded
# cardinality in PDP resource selectors.
result := {
"operation": "mcp:tool:github:search:repos",
"resource": "mrn:agentvisor:github:search",
} if {
input.tool_name == "search_repositories"
}
Configuration:
mcp:
servers:
- name: github
transport: streamable_http
url: "https://api.githubcopilot.com/mcp/"
credentials:
type: bearer_token
source: env
env_var: GITHUB_TOKEN
enrichment:
provider: rego
rego_policy: /etc/agentvisor/policies/mcp-github.rego
PDP policy (in MPE policy domain YAML) can now use stable, per-repository patterns. As with any resource selector list, order matters — specific allows go first, the catch-all deny goes last:
resources:
# Allow every enriched GitHub operation on this one repository
- name: github-repo-api
selector:
- "mrn:agentvisor:github:repo:api"
group: "mrn:iam:resource-group:allowed"
# Catch-all: deny every other repository
- name: github-repo-default
selector:
- "mrn:agentvisor:github:repo:.*"
group: "mrn:iam:resource-group:denied"
A2A: Multi-Tenant Routing Control
Map A2A task dispatch to tenant-scoped identifiers, so PDP policies can control which principals may route tasks to which tenants.
Policy (the exact Rego policy AgentVisor ships and tests against):
package agentvisor.authz.enrichment
# Scope task dispatch by tenant (from metadata) and calling principal.
# Produces a structured resource MRN that PDP policies can match on.
result := {"operation": op, "resource": res} if {
tenant := input.metadata.tenant
sub := input.principal_claims.sub
op := sprintf("a2a:task:send:%v", [tenant])
res := sprintf("mrn:agentvisor:a2a:tenant:%v/principal:%v", [tenant, sub])
}
Configuration:
a2a_gateway:
agents:
- name: orchestrator
url: "https://orchestrator.example.com/a2a"
credentials:
type: principal_passthrough
enrichment:
provider: rego
rego_policy: /etc/agentvisor/policies/a2a-tasks.rego
PDP policy restricts tenant dispatch by caller identity:
policies:
- mrn: "mrn:iam:policy:tenant-match"
name: tenant-match
rego: |
package authz
default allow = false
# Principal may only route to their own tenant
allow {
input.principal.tenant == input.context.a2a.metadata.tenant
}
resources:
- name: a2a-task-dispatch
selector:
- "mrn:agentvisor:a2a:tenant:.*"
group: "mrn:iam:resource-group:tenant-match"
When input.metadata.tenant is absent (rule undefined), the original mrn:agentvisor:a2a:orchestrator resource is used — the PDP evaluates the coarse-grained rule as usual.
HTTP Provider
The http provider delegates enrichment decisions to a remote HTTP service instead of an in-process Rego policy — useful when the decision logic already lives in a system you operate (an internal CMDB, ticketing system, or proprietary rules engine) that you'd rather call than reimplement as Rego.
Like every enrichment provider (see Fail-Open Behavior), http is fail-open. Don't confuse this with the HTTP provider used for MPE/PDP authorization itself (see Policy Enforcement), which is fail-closed — a PDP outage denies the request, whereas an enrichment backend outage only degrades fine-grained authorization to coarse-grained, un-enriched PDP rules. If a policy depends on enrichment succeeding, pair it with a coarse-grained PDP rule as a safety net.
HTTP Configuration
enrichment:
definitions:
- name: corp-http-enricher
provider: http
options:
url: https://enrich.example.com/enrich
timeout: 5s # default: 5s
tls:
mode: verify-full # disable | require | verify-ca | verify-full
ca_file: /etc/ssl/enrich-ca.pem
cert_file: /etc/ssl/client.pem # optional, for mTLS
key_file: /etc/ssl/client-key.pem
server_name: enrich.internal # optional SNI override
headers:
- { name: Authorization, value_env: ENRICH_TOKEN }
retry: # optional; shown values are the defaults
enabled: true
max_attempts: 3
initial_interval: 100ms
max_interval: 1s
max_elapsed_time: 1.5s
url is the only required option; timeout defaults to 5s. See the Configuration Reference for the full options.* field table.
Retry
By default, the provider retries transient failures — network errors and 429/502/503/504 responses — with bounded exponential backoff (options.retry, enabled by default). Total retry time is capped by max_elapsed_time regardless of max_attempts, so a struggling backend can never turn into an unbounded latency spike on this synchronous, pre-authorization call. Set retry.enabled: false to disable it.
Retry only reduces how often a transient blip causes this provider to be skipped — it does not make failures more consequential. Once retries are exhausted (or retry is disabled), the outcome is identical to the fail-open behavior described above: the original, un-enriched operation/resource is used and a warn-level log is emitted. A non-retryable outcome (a non-retryable status, a malformed response body) is never retried and fails open immediately, exactly as before this feature existed.
Wire Protocol (v1)
The provider issues POST <url> verbatim — no path is appended (unlike the HTTP PDP provider, which appends /decision).
Request headers: Content-Type: application/json, Accept: application/json, X-AgentVisor-Enrichment-Protocol: 1, plus any static headers from options.headers merged with a per-site headers override.
Request body mirrors the Input Signals above (snake_case), plus site_context:
{
"server_name": "github",
"tool_name": "list_commits",
"agent_name": "",
"agentvisor_id": "prod-instance-01",
"operation": "agentvisor:mcp:tool:call",
"resource": "mrn:agentvisor:mcp:github/list_commits",
"arguments": {"owner": "my-org", "repo": "api"},
"metadata": {},
"message_role": "",
"principal_claims": {"sub": "..."},
"site_context": {"site": "github-mcp", "environment": "prod"}
}
Absent map fields (arguments, metadata, principal_claims, site_context) serialize as {}, never null.
Response:
| Status | Body | Result |
|---|---|---|
200 OK | {"operation"?, "resource"?, "context"?} | Maps to the enrichment result (see The Enrichment Result). A missing or empty operation/resource preserves the original value. An empty {} body means "no change." |
204 No Content | (none) | No enrichment — the same effect as an undefined Rego rule. |
| Anything else (non-2xx status, network error, timeout, malformed JSON) | — | A provider error — handled fail-open per the note above. |
The request timeout (default 5s) is enforced via both context.Context and http.Client.Timeout.
Relationship to MPE Authorization
Enrichment and MPE authorization are two separate steps in the request pipeline:
McpCallTool / A2ASendTask
│
▼
Enrichment Pipeline (host-side)
• Reads: tool arguments, metadata, principal claims
• Writes: operation, resource MRN, context fields
│
▼
MPE / PDP Authorization
• Reads: principal, operation, resource, context
• Writes: allow/deny decision
│
▼
Gateway executes (if allowed)
The PDP evaluates after enrichment. It sees the enriched operation and resource, not the raw MCP/A2A values. PDP policies can use simple MRN pattern selectors without inspecting tool arguments.
The PDP's input.context map contains all the fields that enrichment added via result.context, as well as the original values under context.mcp.original_* or context.a2a.original_*. PDP Rego can reference these if needed.
Related Documentation
- Policy Enforcement — How MPE authorization works, including the fail-closed HTTP PDP provider
- Gateways — MCP and A2A gateway architecture
- Configuration Reference — Full
enrichment:field documentation, including centraldefinitionsand thehttpprovider'soptions.*schema