Egress Proxy
The Egress Proxy is a controlled HTTP gateway that handles all outbound requests made by AI agents. Instead of giving agents direct access to external APIs (and the credentials required to authenticate), agents submit their outbound requests to the proxy, which:
- Validates the target domain against a per-agent allowlist
- Blocks requests to private/internal IP addresses (SSRF protection)
- Strips any credential-like headers the agent attempted to set
- Injects the correct authentication header from the Credential Vault
- Enforces per-agent per-domain rate limits
- Executes the request and returns the response to the agent
- Logs everything (with credentials redacted)
This architecture ensures that agents can call external APIs without ever holding, seeing, or being able to leak raw credentials.
Refs #2287, #2300, #2301
Base URL
https://api.ainative.studio/api/v1/cloud/egress
Endpoints
POST /request --- Proxy an outbound HTTP request
The primary endpoint. An agent submits the details of the HTTP request it wants to make, and the proxy executes it on the agent's behalf.
Request
curl -X POST https://api.ainative.studio/api/v1/cloud/egress/request \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent-001",
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"headers": {
"Content-Type": "application/json"
},
"body": "{\"model\":\"gpt-4\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}",
"credential_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"timeout_seconds": 30
}'
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | -- | ID of the agent making the request |
method | string | Yes | -- | HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
url | string | Yes | -- | Full target URL (max 2048 chars) |
headers | object | No | {} | Request headers (credential-like headers are stripped) |
body | string | No | null | Request body (max 1 MB) |
credential_id | string | No | null | ID of a credential in the vault to inject |
timeout_seconds | integer | No | 30 | Request timeout (1-120 seconds) |
Response 200 OK
{
"status_code": 200,
"headers": {
"content-type": "application/json",
"x-request-id": "req_abc123"
},
"body": "{\"id\":\"chatcmpl-xyz\",\"choices\":[{\"message\":{\"content\":\"Hello!\"}}]}",
"request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"latency_ms": 842
}
The request_id is a unique identifier for audit trail purposes. Use it to look up this request in the egress logs.
Error Responses
The proxy returns structured error responses (not HTTP-level errors) to give the agent actionable information:
status_code in response | Meaning |
|---|---|
| 403 | Domain not in allowlist, SSRF blocked, or credential not found/unauthorized |
| 429 | Rate limit exceeded for this agent on this domain |
| 502 | Connection error to the target (DNS failure, connection refused, etc.) |
| 504 | Request timed out |
Example error response:
{
"status_code": 403,
"headers": {},
"body": "{\"error\":\"Domain api.evil.com not in allowlist (default deny)\"}",
"request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"latency_ms": 0
}
GET /logs --- Audit log of outbound requests
Retrieve a paginated list of all egress proxy requests, optionally filtered by agent.
Request
curl -X GET "https://api.ainative.studio/api/v1/cloud/egress/logs?agent_id=agent-001&limit=20&offset=0"
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | No | -- | Filter logs by agent ID |
limit | integer | No | 50 | Results per page (1-200) |
offset | integer | No | 0 | Pagination offset |
Response 200 OK
{
"logs": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"agent_id": "agent-001",
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"status_code": 200,
"credential_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"latency_ms": 842,
"error": null,
"created_at": "2026-06-01T12:00:00+00:00"
},
{
"id": "e38bd20a-49bb-3361-b456-1f13a3b4c568",
"agent_id": "agent-001",
"method": "GET",
"url": "https://api.github.com/repos/org/repo",
"status_code": 403,
"credential_id": null,
"latency_ms": 0,
"error": "Domain api.github.com not in allowlist (default deny)",
"created_at": "2026-06-01T11:55:00+00:00"
}
],
"total": 2
}
Credential Injection
When the agent includes a credential_id in its egress request, the proxy:
- Retrieves the credential from the vault using
decrypt_for_egress(credential_id, agent_id). - Verifies that the requesting agent is in the credential's
agent_idslist (or that the list is empty, meaning all agents are allowed). - Injects the appropriate HTTP header based on the credential type:
| Credential Type | Header Injected |
|---|---|
bearer_token | Authorization: Bearer <value> |
api_key | X-API-Key: <value> |
basic_auth | Authorization: Basic <base64(value)> |
oauth2_client_credentials | Authorization: Bearer <value> |
If the credential is not found, has been deleted, or the agent is not authorized, the proxy returns a 403 without making the outbound request.
Stripped Headers
The proxy removes the following headers from every agent request before forwarding, regardless of whether a credential_id is provided. This prevents agents from smuggling credentials in headers:
| Header | Why it is stripped |
|---|---|
authorization | Prevents agents from injecting their own Bearer/Basic tokens |
x-api-key | Prevents agents from injecting API keys directly |
api-key | Alternative API key header used by some providers (e.g., Azure) |
x-auth-token | Custom auth token header |
proxy-authorization | Prevents proxy-level auth injection |
cookie | Prevents session hijacking via cookies |
set-cookie | Stripped from responses as well |
If an agent sets any of these headers, they are silently removed. The agent can still set non-credential headers like Content-Type, Accept, User-Agent, etc.
Domain Allowlist
The egress proxy enforces a default-deny policy. Every outbound request must target a domain that appears on either:
-
Platform-wide allowed domains -- always permitted for all agents:
api.openai.comapi.anthropic.comapi.cohere.aigenerativelanguage.googleapis.comapi.mistral.aiapi.groq.com
-
Per-agent allowlist -- configured by the user for each agent
Managing the Agent Allowlist
GET /api/v1/cloud/agents/{agent_id}/allowlist
Retrieve the domain allowlist for an agent.
curl -X GET https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist \
-H "Authorization: Bearer $TOKEN"
Response
{
"agent_id": "agent-001",
"domains": [
{
"domain": "api.github.com",
"added_by": "user-uuid",
"added_at": "2026-06-01T10:00:00+00:00"
},
{
"domain": "*.stripe.com",
"added_by": "user-uuid",
"added_at": "2026-06-01T10:00:00+00:00"
}
],
"platform_domains": [
"api.openai.com",
"api.anthropic.com",
"api.cohere.ai",
"generativelanguage.googleapis.com",
"api.mistral.ai",
"api.groq.com"
]
}
PUT /api/v1/cloud/agents/{agent_id}/allowlist
Replace the agent's domain allowlist. This is a full replacement, not a merge.
curl -X PUT https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domains": ["api.github.com", "*.stripe.com", "hooks.slack.com"]
}'
POST /api/v1/cloud/agents/{agent_id}/allowlist/check
Test whether a specific domain is allowed for an agent without making an actual request.
curl -X POST https://api.ainative.studio/api/v1/cloud/agents/agent-001/allowlist/check \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "api.github.com"}'
Response
{
"domain": "api.github.com",
"allowed": true,
"matched_rule": "agent:api.github.com",
"reason": "Allowed by agent domain allowlist"
}
Wildcard Patterns
Domain patterns support wildcard prefixes:
| Pattern | Matches | Does Not Match |
|---|---|---|
api.github.com | api.github.com | github.com, raw.github.com |
*.github.com | api.github.com, raw.github.com, github.com | github.io |
*.stripe.com | api.stripe.com, dashboard.stripe.com | stripe.com.evil.net |
Rate Limiting
The egress proxy enforces rate limits per agent per destination domain using a sliding window:
- Default limit: 60 requests per minute per agent per domain
- Window: 60-second sliding window
- Scope: Each
(agent_id, domain)pair has its own counter
When the limit is exceeded, the proxy returns:
{
"status_code": 429,
"headers": {},
"body": "{\"error\":\"Rate limit exceeded: 60 requests/min for api.openai.com\"}",
"request_id": "...",
"latency_ms": 0
}
The rate limiter is in-memory, so counters reset on service restart. This is intentional -- it protects external APIs from runaway agents without requiring persistent state.
SSRF Protection
Before every outbound request, the proxy resolves the target hostname and checks whether it points to a private or internal IP address. The following network ranges are blocked:
| Network | Description |
|---|---|
10.0.0.0/8 | RFC 1918 private |
172.16.0.0/12 | RFC 1918 private |
192.168.0.0/16 | RFC 1918 private |
127.0.0.0/8 | Loopback |
169.254.0.0/16 | Link-local |
::1/128 | IPv6 loopback |
fe80::/10 | IPv6 link-local |
fc00::/7 | IPv6 unique local |
This prevents agents from:
- Accessing internal services on the same network (
10.x.x.x,172.16.x.x) - Reaching the cloud metadata service (
169.254.169.254) - Probing localhost services (
127.0.0.1) - Using DNS rebinding attacks (hostnames are resolved and checked before the request is sent)
If a hostname resolves to a blocked IP, the request is rejected with:
{
"status_code": 403,
"body": "{\"error\":\"Blocked: internal.service resolves to 10.0.1.5 in private network 10.0.0.0/8\"}",
"request_id": "...",
"latency_ms": 0
}
Request Lifecycle
Agent Egress Proxy Allowlist Vault Target API
| | | | |
|-- POST /egress/request ->| | | |
| |-- is_request_allowed? -->| | |
| | (SSRF + domain check) | | |
| |<-- allowed/denied -------| | |
| | | | |
| |-- check rate limit | | |
| | | | |
| |-- strip cred headers | | |
| | | | |
| |-- decrypt_for_egress -->| | |
| | (credential_id, | (if cred_id) | |
| | agent_id) | | |
| |<-- {type, value} -------| | |
| | | | |
| |-- inject auth header | | |
| |-- forward request ----->| | |
| | | | (forwarded) |
| |<-- response ------------|----------------|------------------|
| | | | |
| |-- log to audit table | | |
| | | | |
|<-- EgressResponse -------| | | |
End-to-End Example
This example walks through storing a credential, configuring an allowlist, and making a proxied request.
1. Store a credential
curl -X POST https://api.ainative.studio/api/v1/cloud/credentials \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe Secret Key",
"credential_type": "bearer_token",
"credential_value": "sk_live_abc123def456",
"target_domain": "api.stripe.com",
"agent_ids": ["billing-agent"]
}'
Save the returned id (e.g., cred-uuid-here).
2. Add the domain to the agent's allowlist
curl -X PUT https://api.ainative.studio/api/v1/cloud/agents/billing-agent/allowlist \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domains": ["api.stripe.com"]}'
3. Make a proxied request
curl -X POST https://api.ainative.studio/api/v1/cloud/egress/request \
-H "Content-Type: application/json" \
-d '{
"agent_id": "billing-agent",
"method": "GET",
"url": "https://api.stripe.com/v1/charges?limit=5",
"headers": {"Content-Type": "application/json"},
"credential_id": "cred-uuid-here"
}'
The proxy will:
- Confirm
api.stripe.comis in the agent's allowlist - Verify
billing-agentis in the credential'sagent_ids - Strip any auth headers the agent may have set
- Inject
Authorization: Bearer sk_live_abc123def456 - Forward the GET to Stripe
- Return the response to the agent
4. Review the audit log
curl -X GET "https://api.ainative.studio/api/v1/cloud/egress/logs?agent_id=billing-agent&limit=5"
Timeout and Redirect Behavior
- Timeout: Configurable per request (1-120 seconds, default 30). Timed-out requests return status code 504.
- Redirects: The proxy follows up to 5 redirects automatically. The final response is returned to the agent.
- Response body logging: The audit log stores up to 4096 bytes of response body for debugging.