The auth proxy lets sandbox code call external APIs (OpenAI, Anthropic, GitHub, etc.) without hardcoding credentials. When configured on a sandbox, a proxy sidecar automatically injects authentication headers into matching outbound requests using your workspace secrets or write-only credentials you provide in the proxy config.
You must configure your secrets (e.g., OPENAI_API_KEY) in your LangSmith workspace settings before creating a sandbox that references them.
Egress and network access control
The same proxy_config that injects credentials also controls which destinations a sandbox can reach.
Default egress posture
By default (no access_control configured):
- HTTP and HTTPS (ports 80 and 443) to any host are allowed. Outbound HTTP(S) is transparently routed through the proxy, where your
rules and callbacks inject credentials.
- All other raw TCP is blocked. Connections to non-HTTP ports—databases (
psql/dbt on 5432), SSH (22), Redis (6379), and so on—are dropped unless you explicitly allow them.
This means raw protocols are not blocked because the proxy “can’t speak” them—they are blocked by default and opened per host and port via access_control.
Allow and deny lists
Add an access_control object to proxy_config with either an allow_list or a deny_list (not both—the request is rejected if both are set):
| Mode | Behavior |
|---|
allow_list | Default-deny. Only listed destinations are reachable—including HTTP/HTTPS. If you set an allow_list, you must also list every HTTP(S) host the sandbox needs. |
deny_list | Default-allow for HTTP/HTTPS. All HTTP(S) hosts remain reachable except those listed. A deny_list cannot open raw TCP ports. |
Raw TCP egress (e.g. PostgreSQL on 5432) can only be enabled with an allow_list entry that specifies an explicit non-HTTP port (host:PORT). The default posture and deny_list mode only ever permit HTTP/HTTPS.
Pattern syntax
Each allow_list/deny_list entry uses the following forms:
| Pattern | Meaning |
|---|
host | Bare host → ports 80 and 443 only (HTTP/S). |
host:PORT | Host on exactly PORT. :22 grants only 22, not additive with 80/443—list the host twice if you need both. |
*.example.com | Glob (RFC 1034-style). The apex (example.com) is not included. |
~regex | Opaque regex match; no port suffix parsing. |
1.2.3.4 / 10.0.0.0/8 | Literal IP or CIDR. CIDRs cannot carry a port (HTTP/S only in allow mode; block all ports in deny mode). |
[::1]:22 | IPv6 literal uses the bracketed form when specifying a port. |
Connecting to a database (raw TCP)
To let sandbox code reach an external PostgreSQL database with psql, dbt, or any driver, allow-list the host on its port. Because allow_list is default-deny, also list any HTTP(S) hosts the sandbox needs:
curl -X POST "$LANGSMITH_ENDPOINT/v2/sandboxes/boxes" \
-H "x-api-key: $LANGSMITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "db-sandbox",
"wait_for_ready": true,
"proxy_config": {
"access_control": {
"allow_list": [
"db.example.com:5432",
"api.openai.com"
]
}
}
}'
The connection to db.example.com:5432 is passed through at the TCP layer with no interception, so the PostgreSQL wire protocol—and TLS, host-key checking, and any other end-to-end protocol on top of it—works unchanged.
from langsmith.sandbox import SandboxClient
client = SandboxClient()
client.create_sandbox(
name="db-sandbox",
proxy_config={
"access_control": {
"allow_list": ["db.example.com:5432", "api.openai.com"]
}
},
)
Add a proxy_config when creating a sandbox, or update an existing sandbox by patching its proxy_config. Each rule specifies:
| Field | Description |
|---|
match_hosts | Hosts to intercept (supports globs like *.github.com) |
match_paths | Paths to match (empty = all paths) |
headers | Headers to inject, each with a name, type, and value |
no_proxy | Hosts to bypass the proxy entirely (e.g. localhost) |
Each header has a type that controls how its value is stored and displayed:
| Type | Description |
|---|
workspace_secret | References a workspace secret using {KEY} syntax. Resolved when the proxy configuration is applied. |
plaintext | Value is stored and returned as-is. Use for non-sensitive headers. |
opaque | Write-only. Value is encrypted at rest and never returned via the API. |
Single API example
Create a sandbox that automatically injects an OpenAI API key into outbound requests:
curl -X POST "$LANGSMITH_ENDPOINT/v2/sandboxes/boxes" \
-H "x-api-key: $LANGSMITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "openai-sandbox",
"wait_for_ready": true,
"proxy_config": {
"rules": [
{
"name": "openai-api",
"match_hosts": ["api.openai.com"],
"headers": [
{
"name": "Authorization",
"type": "workspace_secret",
"value": "Bearer {OPENAI_API_KEY}"
}
]
}
]
}
}'
The sandbox can now call OpenAI with no API key setup—the proxy injects it automatically.
Multiple API example
Add multiple rules to authenticate with several services at once:
curl -X POST "$LANGSMITH_ENDPOINT/v2/sandboxes/boxes" \
-H "x-api-key: $LANGSMITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "multi-api-sandbox",
"wait_for_ready": true,
"proxy_config": {
"rules": [
{
"name": "openai-api",
"match_hosts": ["api.openai.com"],
"headers": [
{
"name": "Authorization",
"type": "workspace_secret",
"value": "Bearer {OPENAI_API_KEY}"
}
]
},
{
"name": "anthropic-api",
"match_hosts": ["api.anthropic.com"],
"headers": [
{
"name": "x-api-key",
"type": "workspace_secret",
"value": "{ANTHROPIC_API_KEY}"
},
{
"name": "anthropic-version",
"type": "plaintext",
"value": "2023-06-01"
}
]
},
{
"name": "github-api",
"match_hosts": ["api.github.com"],
"match_paths": ["/repos/*", "/user"],
"headers": [
{
"name": "Authorization",
"type": "workspace_secret",
"value": "Bearer {GITHUB_TOKEN}"
}
]
}
],
"no_proxy": ["localhost", "127.0.0.1"]
}
}'
GitHub example
Open SWE authenticates GitHub access by minting a short-lived GitHub App installation token outside the sandbox, then patching the sandbox with write-only opaque proxy rules. This keeps the short-lived GitHub access token out of the sandbox filesystem and out of deployment environment variables.
Configure two rules:
| Host | Header |
|---|
api.github.com | Authorization: Bearer <github-token> for gh and REST API calls |
github.com, *.github.com | Authorization: Basic <base64("x-access-token:<github-token>")> for Git over HTTPS operations like clone, fetch, and push |
import base64
import os
from typing import Any
import httpx
def github_proxy_rules(github_token: str) -> list[dict[str, Any]]:
basic_auth = base64.b64encode(
f"x-access-token:{github_token}".encode()
).decode()
return [
{
"name": "github-api",
"match_hosts": ["api.github.com"],
"headers": [
{
"name": "Authorization",
"type": "opaque",
"value": f"Bearer {github_token}",
}
],
},
{
"name": "github",
"match_hosts": ["github.com", "*.github.com"],
"headers": [
{
"name": "Authorization",
"type": "opaque",
"value": f"Basic {basic_auth}",
}
],
},
]
def configure_github_proxy(sandbox_name: str, github_token: str) -> None:
endpoint = os.environ.get(
"LANGSMITH_ENDPOINT", "https://api.smith.langchain.com"
)
response = httpx.patch(
f"{endpoint}/v2/sandboxes/boxes/{sandbox_name}",
headers={"x-api-key": os.environ["LANGSMITH_API_KEY"]},
json={"proxy_config": {"rules": github_proxy_rules(github_token)}},
timeout=30.0,
)
response.raise_for_status()
Call configure_github_proxy after creating or reattaching to a sandbox. GitHub App installation tokens expire, so refresh the proxy config whenever you reuse a sandbox for a new run.
Inside the sandbox, set a non-secret placeholder token when a CLI requires a local credential before it sends a request:
GH_TOKEN=dummy gh repo view langchain-ai/langchain
GH_TOKEN=dummy gh pr list --repo langchain-ai/langchain
GH_TOKEN=dummy gh repo clone langchain-ai/langchain
The placeholder only satisfies the gh CLI’s local check. The proxy injects the real Authorization header into the outbound request.
from langsmith.sandbox import SandboxClient
client = SandboxClient()
client.create_sandbox(
name="openai-sandbox",
proxy_config={
"rules": [
{
"name": "openai-api",
"match_hosts": ["api.openai.com"],
"headers": [
{
"name": "Authorization",
"type": "workspace_secret",
"value": "Bearer {OPENAI_API_KEY}",
}
],
}
]
},
)
Callback credential example
Static workspace_secret rules pull credentials from your workspace when the proxy configuration is applied, and opaque rules let your application patch in short-lived credentials such as the GitHub token example. For credentials that must be resolved by your own service at proxy time, use a callback. The proxy POSTs to a URL you provide, your endpoint returns the headers to inject, and the proxy caches the result.
Callbacks are configured alongside rules under proxy_config:
| Field | Description |
|---|
match_hosts | Hosts to intercept (same syntax as rules; supports globs like *.github.com). |
url | Your callback endpoint. Must be an http:// or https:// URL reachable from the proxy. |
request_headers | Headers attached to the proxy → callback request, e.g., an HMAC or shared secret your endpoint uses to verify the request. Only plaintext and opaque types are permitted (no workspace_secret). |
ttl_seconds | How long resolved headers are cached before re-invoking the callback. Must be between 60 and 3600. |
Static rules win. If any rule in rules matches the host, the callback is skipped for that host. Within rules, first-match-wins; the same applies between callbacks if multiple match.
Callback contract
The proxy makes the following request whenever it needs to resolve credentials for a matched host on a cache miss:
POST <callback.url>
Content-Type: application/json
<request_headers from your config, attached verbatim>
{"host": "api.example.com", "port": 443}
Your endpoint must respond 2xx with a JSON body:
{
"headers": {
"Authorization": "Bearer <token>",
"X-Org-Id": "..."
}
}
The proxy injects every header in the response into the sandbox’s outbound request and caches the response for ttl_seconds. Any non-2xx response, transport error, or malformed JSON fails closed: the sandbox’s request is rejected with 502 callback resolution failed (no headers injected, response not cached).
Example
Use a callback when your OAuth tokens are minted on demand by your own service:
curl -X POST "$LANGSMITH_ENDPOINT/v2/sandboxes/boxes" \
-H "x-api-key: $LANGSMITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"snapshot_id": "<snapshot-uuid>",
"name": "callback-sandbox",
"wait_for_ready": true,
"proxy_config": {
"callbacks": [
{
"match_hosts": ["api.github.com", "*.githubusercontent.com"],
"url": "https://auth.your-app.example.com/sandbox-credentials",
"request_headers": [
{
"name": "X-Integrator-Secret",
"type": "opaque",
"value": "<shared-secret-your-endpoint-verifies>"
}
],
"ttl_seconds": 300
}
]
}
}'
from langsmith.sandbox import SandboxClient
client = SandboxClient()
client.create_sandbox(
name="callback-sandbox",
proxy_config={
"callbacks": [
{
"match_hosts": ["api.github.com", "*.githubusercontent.com"],
"url": "https://auth.your-app.example.com/sandbox-credentials",
"request_headers": [
{
"name": "X-Integrator-Secret",
"type": "opaque",
"value": "<shared-secret-your-endpoint-verifies>",
}
],
"ttl_seconds": 300,
}
]
},
)