HTTP Proxy
AgentVisor™'s HTTP proxy transparently intercepts all outbound HTTP requests from agents, enabling policy enforcement without code changes.
How It Works
1. Agent code makes HTTP request
│ response = requests.get("https://api.openai.com/v1/models")
│
▼
2. Request intercepted by HTTPS_PROXY
│ → http://localhost:<dynamic-port>
│ → CONNECT api.openai.com:443
│
▼
3. Guest proxy terminates TLS
│ • Ephemeral CA signs per-host certificate
│ • TLS handshake with agent's HTTP client
│ • Decrypted request headers and body now visible
│
▼
4. Guest Runtime securely forwards decrypted request to Host
│ Over the private host channel (gRPC over UDS or TCP+mTLS)
│
▼
5. Host Runtime processes request
│
├─▶ 5a. Policy Check (MPE)
│ • Resource: mrn:agentvisor:http:api.openai.com/v1/models
│ • Operation: GET
│ • Principal: from context
│ │
│ ├─ ALLOWED → continue
│ └─ DENIED → return 403
│
├─▶ 5b. Audit Log
│ • Record request details
│ • Include policy decision
│
├─▶ 5c. Credential Substitution
│ • Replace symbolic tokens (mav-tok-...) with real credentials
│
├─▶ 5d. SSRF / Destination Check
│ • Resolves the host and checks the *resolved* address — not the
│ hostname string the policy check in 5a just authorized
│ • Always on, independent of the policy decision
│ │
│ ├─ ALLOWED → continue
│ └─ BLOCKED → return 502 (empty body; reason is host-log only)
│
└─▶ 5e. Execute Request
• Make actual HTTPS request to upstream
• Return response to guest
See Egress Controls for why a policy allow on step 5a does not exempt a request from step 5d, and for the separate guest-layer check that rejects an IP-literal destination even earlier, before the request reaches the host at all.
Transparent Proxying
The proxy is transparent - no code changes required:
# These all work automatically:
import requests
response = requests.get("https://api.openai.com/v1/models")
import httpx
async with httpx.AsyncClient() as client:
response = await client.get("https://api.anthropic.com/v1/complete")
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
response = llm.invoke("Hello!")
How Transparency Works
The guest runtime sets environment variables:
HTTP_PROXY=http://localhost:12345
HTTPS_PROXY=http://localhost:12345
NO_PROXY=localhost,127.0.0.1
Python's requests, httpx, urllib3, and aiohttp all honor these variables automatically.
HTTPS and TLS Termination
For HTTPS requests, the guest proxy terminates TLS using an ephemeral Certificate Authority. This enables the proxy to inspect request headers and bodies, which is required for credential brokering and full policy enforcement.
1. Client sends CONNECT api.openai.com:443
2. Proxy accepts the CONNECT tunnel
3. Proxy performs TLS handshake with the client
• Uses ephemeral CA to sign a certificate for api.openai.com
• Client trusts this cert via SSL_CERT_FILE CA bundle
4. Decrypted HTTP request is read from the TLS connection
5. Request securely forwarded to host over the hostlink session (gRPC over UDS or TCP+mTLS, depending on sandbox mode)
6. Host performs policy check, credential substitution, and upstream HTTPS request
This means:
- The proxy can inspect all request headers and body content
- Symbolic credential tokens are visible for credential brokering
- Full request details are available for policy enforcement and audit logging
- Agent HTTP libraries trust the proxy CA transparently via
SSL_CERT_FILE
Per-host certificates are generated on demand and cached (1-hour validity). The ephemeral CA is regenerated on each guest restart (30-day validity, configurable via AGENTVISOR_GUEST_CA_VALIDITY). No code changes are needed in the agent.
Streaming Support
The proxy supports SSE (Server-Sent Events) for LLM streaming:
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-20250514", streaming=True)
for chunk in llm.stream("Tell me a story"):
print(chunk.content, end="", flush=True)
How Streaming Works
1. LLM request with stream=true
│
▼
2. Guest Runtime forwards the streaming request to Host
│
▼
3. Host makes request, receives SSE stream
│
▼
4. Host streams chunks back over the private host channel
│ Each chunk carries response data plus a done flag (and an
│ optional error if the upstream stream fails mid-flight)
│
▼
5. Guest Runtime converts to HTTP chunked response
│
▼
6. Python receives streaming response
Policy Integration
Every request is evaluated against the policy:
MRN Format
HTTP requests are converted to MRNs:
mrn:agentvisor:http:<host><path>
Examples:
• https://api.openai.com/v1/chat/completions
→ mrn:agentvisor:http:api.openai.com/v1/chat/completions
• http://httpbin.org/get?foo=bar
→ mrn:agentvisor:http:httpbin.org/get
Policy Example
resources:
# Allow OpenAI API
- name: openai
selector:
- "mrn:agentvisor:http:api\\.openai\\.com.*"
group: "mrn:iam:resource-group:allowed"
# Default deny
- name: default-http
selector:
- "mrn:agentvisor:http:.*"
group: "mrn:iam:resource-group:denied"
Don't add a resource selector here to block internal networks (RFC 1918 ranges, cloud
metadata addresses, localhost). An MRN is built from the request's host string, before
any DNS lookup — a hostname that happens to resolve to an internal address sails past any
selector written this way. That protection is handled separately, always on, and checked
against the resolved address rather than the hostname text. See
Egress Controls for how it works.
Configuration
Request timeout and max body size for outbound proxy requests are configured under proxy.*.
See the Configuration Reference for the full field list.
The always-on SSRF guard is configured separately via proxy.ssrf_allowed_cidrs, which grants
narrow exemptions from the default-blocked address ranges (loopback, link-local, private, and
CGNAT). See Egress Controls for the full model, including why the
allowlist only takes effect for hostname destinations, not IP literals.
Troubleshooting
For proxy issues — requests denied by policy, TLS/CA bundle errors in the sandbox, or symbolic
tokens not being substituted — see the Troubleshooting Guide,
particularly Guest TLS Errors for SSL_CERT_FILE/CA
bundle problems and Proxy Credential Substitution
for symbolic-token issues.
- Request denied because the destination is blocked (not by policy) — a 400 or 502 response means the SSRF guard rejected the destination, not MPE. See Outbound Destination Blocked for the layer-by-layer breakdown and fixes.
Bypassing the Proxy
Bypassing the proxy removes all policy protection and credential brokering — and also the SSRF guard described in Egress Controls. A request that bypasses the proxy is never evaluated for policy, never checked against a blocked address, and reaches its destination directly.
Only when running with --sandbox=none, you can set NO_PROXY to bypass specific hosts:
import os
os.environ["NO_PROXY"] = "localhost,127.0.0.1,internal.example.com"
In gVisor and Docker (container) modes, the proxy cannot be bypassed: either there is no network interface at all (gVisor, Docker on Linux), or outbound traffic is restricted by in-guest nftables to the hostlink address only (Docker on macOS/Windows). Either way, the proxy is the only route out.