Skip to main content

Adding Policies

Now that you understand how to configure the AgentVisor™ runtime, let's see its security model in action. We'll create an agent that makes HTTP requests and control access with real policies.

What You'll Learn

  • Making HTTP requests from an agent
  • Policy-based allow/deny decisions
  • How MPE policies work

Prerequisites

This tutorial uses a fresh project directory, separate from chatbot-agent. Temporal was started with docker compose up -d inside chatbot-agent back in Your First Agent and runs detached — it's still up unless you've since run docker compose down there. chatbot-agent is the only project directory with a compose.yml, so if you need to restart Temporal, cd into it and run docker compose up -d from there before continuing.

Create a fresh project directory as a sibling of chatbot-agent (not nested inside it) for this tutorial:

mkdir -p my-http-agent/policies
cd my-http-agent

Create an HTTP Agent

Let's create an agent that fetches data from httpbin.org (a test HTTP service):

agent.py

"""Agent that demonstrates HTTP requests through the policy proxy."""

from typing import TypedDict, Annotated
import requests
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_core.messages import AIMessage
from agentvisor.langgraph import AgentVisorCheckpointer


class AgentState(TypedDict):
"""State for our HTTP agent."""
messages: Annotated[list, add_messages]


def fetch_and_respond(state: AgentState) -> dict:
"""Fetch data from httpbin.org and respond."""
messages = state.get("messages", [])

# Find the last user message
user_message = ""
for msg in reversed(messages):
if hasattr(msg, "type") and msg.type == "human":
user_message = msg.content
break
elif isinstance(msg, dict) and msg.get("role") == "user":
user_message = msg.get("content", "")
break

# Make an HTTP request - this goes through the policy proxy
try:
response = requests.get(
"https://httpbin.org/get",
params={"message": user_message},
timeout=10
)
# raise_for_status() converts 4xx/5xx responses to exceptions
# This includes 403 Forbidden when policy denies the request
response.raise_for_status()
result = f"HTTP {response.status_code}: Got response from httpbin.org"
except requests.exceptions.RequestException as e:
# Catches all request errors: connection, timeout, HTTP errors, etc.
result = f"Request blocked or failed: {e}"

return {"messages": [AIMessage(content=result)]}


# Build the graph
builder = StateGraph(AgentState)
builder.add_node("fetch", fetch_and_respond)
builder.set_entry_point("fetch")
builder.add_edge("fetch", END)

graph = builder.compile(checkpointer=AgentVisorCheckpointer())

langgraph.json

{
"dependencies": ["."],
"graphs": {
"http-demo": "./agent.py:graph"
}
}

requirements.txt

langgraph>=0.2.0
langchain-core>=0.3.0
requests>=2.31.0

Create a Policy

AgentVisor uses the Manetu PolicyEngine (MPE) for access control. Policies are defined in YAML using MPE's PolicyDomain format. Let's walk through each piece.

policies/domain.yml

apiVersion: iamlite.manetu.io/v1beta1
kind: PolicyDomain
metadata:
name: http-demo
spec:
policies:
# Tri-level operation policy (Phase 1)
- mrn: &policy-operation "mrn:iam:policy:operation"
name: operation
rego: |
package authz
default allow = 0 # simple 'allow everything' for now

# Resource phase policy - allow access
- mrn: &policy-allow "mrn:iam:policy:allow"
name: allow
rego: |
package authz
default allow = true

# Resource phase policy - deny access
- mrn: &policy-deny "mrn:iam:policy:deny"
name: deny
rego: |
package authz
default allow = false

# HTTP host-matching policy
- mrn: &http-policy "mrn:iam:policy:http"
name: http
rego: |
package authz
default allow = false

import rego.v1

is_host_allowed if {
some pattern in input.resource.annotations.allowed
regex.match(pattern, input.context.http.host)
}

allow if {
is_host_allowed
}

roles:
# Anonymous role - used by 'agentvisor serve' for local development
- mrn: "mrn:agentvisor:role:anonymous"
name: anonymous
policy: *policy-allow

operations:
- name: all-ops
selector: ["agentvisor:.*"]
policy: *policy-operation

resource-groups:
# HTTP resource groups (resolved via resources section below)
- mrn: &http-resources "mrn:iam:resource-group:http"
name: http-resources
policy: *http-policy
annotations:
- name: allowed
value:
- "httpbin.org.*"

- mrn: &group-blocked "mrn:iam:resource-group:blocked"
name: blocked
policy: *policy-deny

# AgentVisor API resource groups (sent directly in PORC by AgentVisor)
- mrn: "mrn:agentvisor:resourcegroup:threads"
name: threads
policy: *policy-allow

- mrn: "mrn:agentvisor:resourcegroup:runs"
name: runs
policy: *policy-allow

- mrn: "mrn:agentvisor:resourcegroup:state"
name: state
policy: *policy-allow

- mrn: "mrn:agentvisor:resourcegroup:agents"
name: agents
policy: *policy-allow

- mrn: "mrn:agentvisor:resourcegroup:store"
name: store
policy: *policy-allow

resources:
# Matches any HTTP request
- name: default-http
selector: ["mrn:agentvisor:http:.*"]
group: *http-resources

Understanding the Policy

Let's break down the key pieces:

Policies are written in Rego, a declarative policy language. Each policy evaluates to allow = true, allow = false, or a numeric value for operation-phase policies.

The operation policy uses MPE's tri-level policy operation phase. The value 0 means GRANT — negative values deny, zero grants, and positive values grant with override. This is evaluated first for every request.

Roles map identities to policies. The anonymous role is used by agentvisor serve for local development — every request runs as the anonymous principal.

Operations define which actions are subject to policy. The selector: ["agentvisor:.*"] pattern matches all AgentVisor operations.

Resource groups bind policies to sets of resources. The http-resources group uses the http policy with an allowed annotation listing permitted host patterns. The resources section maps MRN patterns to resource groups — here, all HTTP requests (mrn:agentvisor:http:.*) are routed to the http-resources group.

Putting it all together:

  1. The operation policy grants all operations (Phase 1)
  2. The anonymous role is allowed by default (Phase 2)
  3. AgentVisor API resources (threads, runs, state, agents, store) are allowed (Phase 3)
  4. HTTP requests to httpbin.org are allowed via the host-matching policy (Phase 3)
  5. HTTP requests to any other host are denied (Phase 3)

Why the Anonymous Role?

By default, agentvisor serve runs without authentication — every request uses an anonymous principal:

"principal": {
"sub": "anonymous",
"mroles": ["mrn:agentvisor:role:anonymous"]
}

Your policies grant access to this role for development. In production, you'd configure real authentication so each request has a verified identity. See Authentication for details.

Run and Test

# Serve the agent
agentvisor serve .

In another terminal:

# Create a thread
THREAD=$(curl -sX POST http://localhost:8090/threads | jq -r '.thread_id')

# Test allowed request
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=30s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Hello!"}]}}' \
| jq '.output.messages[-1].content'

Expected output:

"HTTP 200: Got response from httpbin.org"

See Policy Denial

Now let's modify the agent to try a blocked endpoint. Update the URL in agent.py:

# Change this line:
response = requests.get(
"https://example.com/", # Not in our allow list!
timeout=10
)

Restart the agent and test again:

# Restart (Ctrl+C and re-run)
agentvisor serve .
# Create a new thread
THREAD=$(curl -sX POST http://localhost:8090/threads | jq -r '.thread_id')

# Test blocked request
curl -sX POST "http://localhost:8090/threads/$THREAD/runs?wait=30s" \
-H "Content-Type: application/json" \
-d '{"input": {"messages": [{"role": "user", "content": "Hello!"}]}}' \
| jq '.output.messages[-1].content'

Expected output:

"Request blocked or failed: 403 Client Error: Forbidden for url: https://example.com/"

The policy proxy blocked the request before it reached the external server. The agent caught the 403 error and returned a friendly message instead of crashing.

tip

Use --access-log-pretty-print (covered in Debugging Your Agent) to see the full policy evaluation for each request — which phases passed, which denied, and why.

Next Steps