Skip to main content

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:

  1. Validates the target domain against a per-agent allowlist
  2. Blocks requests to private/internal IP addresses (SSRF protection)
  3. Strips any credential-like headers the agent attempted to set
  4. Injects the correct authentication header from the Credential Vault
  5. Enforces per-agent per-domain rate limits
  6. Executes the request and returns the response to the agent
  7. 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

FieldTypeRequiredDefaultDescription
agent_idstringYes--ID of the agent making the request
methodstringYes--HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
urlstringYes--Full target URL (max 2048 chars)
headersobjectNo{}Request headers (credential-like headers are stripped)
bodystringNonullRequest body (max 1 MB)
credential_idstringNonullID of a credential in the vault to inject
timeout_secondsintegerNo30Request 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 responseMeaning
403Domain not in allowlist, SSRF blocked, or credential not found/unauthorized
429Rate limit exceeded for this agent on this domain
502Connection error to the target (DNS failure, connection refused, etc.)
504Request 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

ParameterTypeRequiredDefaultDescription
agent_idstringNo--Filter logs by agent ID
limitintegerNo50Results per page (1-200)
offsetintegerNo0Pagination 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:

  1. Retrieves the credential from the vault using decrypt_for_egress(credential_id, agent_id).
  2. Verifies that the requesting agent is in the credential's agent_ids list (or that the list is empty, meaning all agents are allowed).
  3. Injects the appropriate HTTP header based on the credential type:
Credential TypeHeader Injected
bearer_tokenAuthorization: Bearer <value>
api_keyX-API-Key: <value>
basic_authAuthorization: Basic <base64(value)>
oauth2_client_credentialsAuthorization: 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:

HeaderWhy it is stripped
authorizationPrevents agents from injecting their own Bearer/Basic tokens
x-api-keyPrevents agents from injecting API keys directly
api-keyAlternative API key header used by some providers (e.g., Azure)
x-auth-tokenCustom auth token header
proxy-authorizationPrevents proxy-level auth injection
cookiePrevents session hijacking via cookies
set-cookieStripped 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:

  1. Platform-wide allowed domains -- always permitted for all agents:

    • api.openai.com
    • api.anthropic.com
    • api.cohere.ai
    • generativelanguage.googleapis.com
    • api.mistral.ai
    • api.groq.com
  2. 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:

PatternMatchesDoes Not Match
api.github.comapi.github.comgithub.com, raw.github.com
*.github.comapi.github.com, raw.github.com, github.comgithub.io
*.stripe.comapi.stripe.com, dashboard.stripe.comstripe.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:

NetworkDescription
10.0.0.0/8RFC 1918 private
172.16.0.0/12RFC 1918 private
192.168.0.0/16RFC 1918 private
127.0.0.0/8Loopback
169.254.0.0/16Link-local
::1/128IPv6 loopback
fe80::/10IPv6 link-local
fc00::/7IPv6 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.com is in the agent's allowlist
  • Verify billing-agent is in the credential's agent_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.