Policy Enforcement
AgentVisor™ uses the Manetu PolicyEngine (MPE) to enforce fine-grained access control on all agent operations.
Overview
Every operation goes through policy evaluation:
Unauthenticated Requests and the Anonymous Principal
AgentVisor does not reject unauthenticated requests at the edge. A request
with no valid credentials still reaches policy evaluation like any other —
it is simply evaluated as an anonymous principal (sub: "anonymous", role
mrn:agentvisor:role:anonymous) rather than turned away before MPE ever
sees it.
This is a deliberate design choice, not an oversight: "reject callers without credentials" is itself an access-control decision, and access-control decisions belong in policy, evaluated by MPE, not hardcoded ahead of it. Keeping authentication ("who is calling, if anyone") separate from authorization ("what may this caller do") means every rule — including "anonymous callers get nothing" — is expressed the same way: as an auditable, testable PolicyDomain rule, not as fixed logic a code change is required to alter.
What this means for your PolicyDomain: the policies/domain.yml shipped
by agentvisor template create langgraph/chatbot-agent (and the other
built-in templates) assigns the built-in mrn:agentvisor:role:anonymous
role to a policy that grants any caller with a non-empty sub — which
includes the anonymous principal itself. This is a deliberately permissive
onboarding default for local development, not a production posture. If your
deployment should deny unauthenticated callers, write that as an explicit
rule, e.g.:
allow if {
input.principal.sub != ""
input.principal.sub != "anonymous"
}
PolicyEngine Modes
AgentVisor supports three authorization modes:
Embedded Mode
PolicyEngine runs as an embedded library inside the host runtime:
export AGENTVISOR_AUTHZ_TYPE=embedded
export AGENTVISOR_AUTHZ_EMBEDDED_POLICY_DOMAIN_FILES=./policies/domain.yml
- Low latency (no network call)
- Policies loaded at startup
- Best for single-tenant deployments
HTTP Mode
Policies evaluated by an external Policy Decision Point (PDP):
export AGENTVISOR_AUTHZ_TYPE=http
export AGENTVISOR_AUTHZ_HTTP_URL=https://pdp.example.com:9000
export AGENTVISOR_AUTHZ_HTTP_TLS_CA_FILE=/etc/agentvisor/pdp-ca.pem
- Centralized policy management
- Hot reload without restart
- Best for enterprise deployments
- Supports TLS, mTLS, and custom auth headers — see the Connecting to a Remote PDP guide for full configuration options including mutual TLS and bearer token injection
Dangerous Disable Mode
Never use in production! Bypasses all authorization checks.
export AGENTVISOR_AUTHZ_TYPE=allowall
- Logs loud warnings
- All requests allowed
- Development only
Manetu Resource Names (MRNs)
Resources are identified by MRNs:
mrn:agentvisor:<type>:<identifier>
| Resource Type | MRN Format | Example |
|---|---|---|
| HTTP Endpoints | mrn:agentvisor:http:<host>/<path> | mrn:agentvisor:http:api.openai.com/v1/chat |
| Agent Tools | mrn:agentvisor:tool:<name> | mrn:agentvisor:tool:web-search |
| Store Items | mrn:agentvisor:store:<namespace>/<key> | mrn:agentvisor:store:memories/* |
| Threads | mrn:agentvisor:thread:<thread_id> | mrn:agentvisor:thread:* |
| Runs | mrn:agentvisor:run:<thread_id>/<run_id> | mrn:agentvisor:run:*/* |
| Thread State | mrn:agentvisor:state:<thread_id> | mrn:agentvisor:state:* |
| Thread KV | mrn:agentvisor:thread-kv:<thread_id> | mrn:agentvisor:thread-kv:* |
| Checkpoints | mrn:agentvisor:checkpoint:<id> | mrn:agentvisor:checkpoint:* |
| Agents | mrn:agentvisor:agent:<name> | mrn:agentvisor:agent:* |
| MCP Server | mrn:agentvisor:mcp:<server_name> | mrn:agentvisor:mcp:github |
| MCP Tool | mrn:agentvisor:mcp:<server_name>/<tool_name> | mrn:agentvisor:mcp:github/list_repos |
| MCP Resource | mrn:agentvisor:mcp:<server_name>/resource/<uri> | mrn:agentvisor:mcp:github/resource/* |
| A2A Agent | mrn:agentvisor:a2a:<agent_name> | mrn:agentvisor:a2a:research-agent |
| A2A Task | mrn:agentvisor:a2a:<agent_name>/task/<task_id> | mrn:agentvisor:a2a:research-agent/task/* |
| Filesystem | mrn:agentvisor:fs:<canonical-host-path> | mrn:agentvisor:fs:/home/user/workspace/* |
| Temporal Namespace | mrn:agentvisor:temporal:<namespace> | mrn:agentvisor:temporal:default |
Operations
Agent Operations (Guest → Host)
| Operation | Description | Status |
|---|---|---|
agentvisor:http:request | HTTP proxy requests | Enforced |
agentvisor:http:upgrade | HTTP proxy protocol upgrades (e.g. WebSocket) | Enforced |
agentvisor:store:read | Read from store | Enforced |
agentvisor:store:create | Create a store item | Enforced |
agentvisor:store:update | Update a store item | Enforced |
agentvisor:store:delete | Delete a store item | Enforced |
agentvisor:store:search | Search store items | Enforced |
agentvisor:store:list | List store namespaces | Enforced |
agentvisor:thread-kv:read | Read thread-scoped KV entry | Enforced |
agentvisor:thread-kv:write | Write thread-scoped KV entry | Enforced |
agentvisor:thread-kv:delete | Delete thread-scoped KV entry | Enforced |
agentvisor:thread-kv:list | List thread-scoped KV entries | Enforced |
agentvisor:checkpoint:save/load and agentvisor:tool:call do not appear
in this table because they aren't policy-evaluated operations at all —
see the note below.
SaveCheckpoint, LoadCheckpoint, and EmitStreamChunk deliberately skip
authorization — no Authorize call, no Op* constant. Checkpoints are
internal to a run whose access was already adjudicated at run creation, so
re-adjudicating every read/write would be redundant; stream chunks are a
high-frequency output path where per-chunk evaluation is prohibitive. See
SECURITY-DESIGN.md (Decision 2) for the full rationale. There is also no
built-in agentvisor:tool:call or agentvisor:event:emit operation —
neither exists in the current Op* constants
(internal/host/grpcserver/server.go).
API Operations (External → Host)
| Operation | Description | Status |
|---|---|---|
agentvisor:thread:create | Create thread | Enforced |
agentvisor:thread:read | Read thread | Enforced |
agentvisor:thread:update | Update thread | Enforced |
agentvisor:thread:delete | Delete thread | Enforced |
agentvisor:thread:list | List/search threads | Enforced |
agentvisor:run:create | Create run | Enforced |
agentvisor:run:read | Read run | Enforced |
agentvisor:run:cancel | Cancel run | Enforced |
agentvisor:run:delete | Delete run | Enforced |
agentvisor:run:list | List runs | Enforced |
agentvisor:state:read | Read state | Enforced |
agentvisor:state:update | Update state | Enforced |
agentvisor:agent:list | List agents | Enforced |
agentvisor:agent:read | Read agent metadata | Enforced |
MCP Gateway Operations (Agent → External MCP Servers)
MCP Gateway policy uses a two-tier model. The server-scope check gates whether the agent may enumerate anything from a given server at all. Per-item checks then gate individual item visibility in list responses.
Server-scope (coarse gate):
| Operation | MRN Scope | Description |
|---|---|---|
agentvisor:mcp:tool:list | mrn:agentvisor:mcp:<server> | List all tools from a server |
agentvisor:mcp:tool:call | mrn:agentvisor:mcp:<server>/<tool> | Call a specific tool |
agentvisor:mcp:resource:list | mrn:agentvisor:mcp:<server> | List all resources from a server |
agentvisor:mcp:resource:read | mrn:agentvisor:mcp:<server>/resource/<uri> | Read a specific resource |
Per-item (fine-grained visibility):
| Operation | MRN Scope | Description |
|---|---|---|
agentvisor:mcp:tool:read | mrn:agentvisor:mcp:<server>/<tool> | Per-tool visibility in McpListTools response |
agentvisor:mcp:resource:describe | mrn:agentvisor:mcp:<server>/resource/<uri> | Per-resource visibility in McpListResources response |
When a server-scope check (...:list) is denied, the entire list call is short-circuited. When a per-item check (...:tool:read, ...:resource:describe) is denied, that item is silently dropped from the response — the agent sees only tools/resources it is allowed to access.
A2A Gateway Operations (Agent → External A2A Agents)
A2A Gateway policy also uses a two-tier model.
Server-scope (coarse gate):
| Operation | MRN Scope | Description |
|---|---|---|
agentvisor:a2a:agent:list | mrn:agentvisor:a2a | List all configured agents |
agentvisor:a2a:agent:describe | mrn:agentvisor:a2a:<agent> | Get agent card |
agentvisor:a2a:task:send | mrn:agentvisor:a2a:<agent> | Send a task to an agent |
agentvisor:a2a:task:get | mrn:agentvisor:a2a:<agent>/task/<id> | Get task status |
agentvisor:a2a:task:cancel | mrn:agentvisor:a2a:<agent>/task/<id> | Cancel a task |
agentvisor:a2a:task:stream | mrn:agentvisor:a2a:<agent>/task/<id> | Stream task updates |
Per-item (fine-grained visibility in A2AListAgents):
| Operation | MRN Scope | Description |
|---|---|---|
agentvisor:a2a:agent:describe | mrn:agentvisor:a2a:<agent> | Per-agent visibility in A2AListAgents response |
agentvisor:a2a:agent:describe gates both individual agent card retrieval (A2AGetAgentCard) and per-agent visibility within A2AListAgents. A single policy rule covering this operation controls both discovery and card access.
Codec Server Operations (Temporal Web UI → Host)
The codec server authorizes /encode and /decode per-namespace only when temporal.codec.server.auth.enabled is set. A request with no valid Bearer token is rejected before either operation is evaluated.
| Operation | MRN Scope | Description |
|---|---|---|
agentvisor:temporal:codec-decode | mrn:agentvisor:temporal:<namespace> | Decrypt payloads for a namespace (/decode) |
agentvisor:temporal:codec-encode | mrn:agentvisor:temporal:<namespace> | Encrypt payloads for a namespace (/encode) |
PolicyDomain Structure
Policies are defined in YAML following MPE's v1beta1 schema. A domain needs
two sections beyond policies:/resource-groups:/resources: to actually
grant or deny anything: operations: (without it, MPE hard-denies every
request before any resource policy runs) and roles: (without a role
matching the caller's mroles/mgroups claims — including the built-in
anonymous principal's own mrn:agentvisor:role:anonymous — MPE denies
regardless of what the resource policy decides; see Unauthenticated
Requests and the Anonymous
Principal above):
apiVersion: iamlite.manetu.io/v1beta1
kind: PolicyDomain
metadata:
name: my-agent-policies
spec:
policies:
# Operation phase - tri-state (-1 deny, 0 continue, >0 grant+skip)
- mrn: &policy-operation "mrn:iam:policy:agentvisor-operation"
name: agentvisor-operation
rego: |
package authz
import rego.v1
default allow := -1
allow := 0 if input.principal.sub != ""
- mrn: &policy-allow "mrn:iam:policy:allow-authenticated"
name: allow-authenticated
rego: |
package authz
import rego.v1
default allow := false
allow if input.principal.sub != ""
- mrn: &policy-deny "mrn:iam:policy:deny-all"
name: deny-all
rego: |
package authz
default allow = false
# Identity phase - the anonymous principal carries this role automatically
roles:
- mrn: "mrn:agentvisor:role:anonymous"
name: anonymous
policy: *policy-allow
resource-groups:
- mrn: "mrn:iam:resource-group:allowed"
name: allowed
policy: *policy-allow
- mrn: "mrn:iam:resource-group:denied"
name: denied
default: true
policy: *policy-deny
resources:
- name: openai-api
selector:
- "mrn:agentvisor:http:api\\.openai\\.com.*"
group: "mrn:iam:resource-group:allowed"
- name: default
selector:
- "mrn:agentvisor:http:.*"
group: "mrn:iam:resource-group:denied"
# Operation phase - a single entry routes every AgentVisor operation
# through the tri-state policy above
operations:
- name: agentvisor-ops
selector: ["agentvisor:.*"]
policy: *policy-operation
Policy Components
Policies
Rego rules that evaluate to allow/deny:
policies:
# Grants any authenticated principal - including the anonymous one, see
# Unauthenticated Requests and the Anonymous Principal above
- mrn: "mrn:iam:policy:allow-authenticated"
name: allow-authenticated
rego: |
package authz
default allow = false
allow {
input.principal.sub != ""
}
Resource Groups
Collections of resources sharing a policy:
resource-groups:
- mrn: "mrn:iam:resource-group:llm-apis"
name: llm-apis
policy: "mrn:iam:policy:allow-authenticated"
Resources
MRN patterns mapped to groups:
resources:
- name: openai
selector:
- "mrn:agentvisor:http:api\\.openai\\.com.*"
group: "mrn:iam:resource-group:llm-apis"
Common Patterns
Allow Specific APIs
resources:
# OpenAI
- name: openai
selector: ["mrn:agentvisor:http:api\\.openai\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Anthropic
- name: anthropic
selector: ["mrn:agentvisor:http:api\\.anthropic\\.com.*"]
group: "mrn:iam:resource-group:allowed"
# Your internal API
- name: internal-api
selector: ["mrn:agentvisor:http:api\\.mycompany\\.com.*"]
group: "mrn:iam:resource-group:allowed"
Block Internal Networks
resources:
- name: rfc1918
selector:
- "mrn:agentvisor:http:10\\..*"
- "mrn:agentvisor:http:192\\.168\\..*"
- "mrn:agentvisor:http:172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"
group: "mrn:iam:resource-group:denied"
- name: localhost
selector:
- "mrn:agentvisor:http:localhost.*"
- "mrn:agentvisor:http:127\\..*"
group: "mrn:iam:resource-group:denied"
- name: metadata
selector:
- "mrn:agentvisor:http:169\\.254\\..*" # AWS metadata
group: "mrn:iam:resource-group:denied"
Per-Principal Policies
policies:
- mrn: "mrn:iam:policy:admin-only"
name: admin-only
rego: |
package authz
default allow = false
allow {
input.principal.role == "admin"
}
- mrn: "mrn:iam:policy:team-access"
name: team-access
rego: |
package authz
default allow = false
allow {
input.principal.team == input.resource.team
}
Rate Limiting (via Rego)
policies:
- mrn: "mrn:iam:policy:rate-limited"
name: rate-limited
rego: |
package authz
default allow = false
# Allow if under rate limit
allow {
count(input.context.requests_today) < 1000
}
# Always allow specific principals
allow {
input.principal.tier == "unlimited"
}
Evaluation Flow
Debugging Policies
Enable Debug Logging
AGENTVISOR_LOG_LEVEL=debug agentvisor serve ...
Test Policies with MPE CLI
# Validate policy syntax
mpe lint -f ./policies/domain.yml
# Test policy evaluation
mpe test decisions -b ./policies/domain.yml -i ./policies/test-cases.yml
Check Policy Match
Every decision logs at info level (no AGENTVISOR_LOG_LEVEL=debug needed)
as a pair of MPE authorization check / MPE authorization result
messages, tagged with the principal, operation, and resource MRN:
INFO [host] MPE authorization check {"principal": "anonymous", "operation": "agentvisor:thread:create", "resource": "mrn:agentvisor:thread:*"}
INFO [host] MPE authorization result {"allowed": true, "principal": "anonymous", "operation": "agentvisor:thread:create", "resource": "mrn:agentvisor:thread:*"}
Best Practices
- Default deny: Always include a catch-all deny rule
- Specific selectors: Use precise patterns, not overly broad
- Block internals first: Deny internal networks before allowing external
- Test thoroughly: Use MPE's test framework
- Audit decisions: Enable audit logging for compliance
- Version control: Keep policies in git with your agent code