Agent Proxy
Use the Agent Proxy Node.js library to give application-built agents host-scoped credentials.
@stashbase/agent-proxy is a Node.js library for applications that build or run their own AI agents. It gives tools placeholders instead of real credentials, then injects a credential only into an approved outbound request to an approved host.
Use the CLI agent workflow when you want to launch an existing coding agent from a profile. Use this library when your Node.js application owns the agent harness, secret resolution, or tool execution.
What the library protects
The trusted application resolves a secret and creates a local proxy policy. Agent code and isolated tools receive a value such as ${STASHBASE_GITHUB_TOKEN}, never the real token. When a proxy-aware tool sends that exact placeholder to api.github.com in the configured header, the local proxy replaces it with the real value. The request is denied for any other destination.
Trusted application → local Agent Proxy → approved API
│ ▲
└─ placeholder-only agent tools ─┘The library is standalone: it does not require the Stashbase Node SDK. The SDK is a useful way for the trusted application to resolve the secret before constructing the policy.
Install
Node.js 20 or later is required.
npm add @stashbase/agent-proxyStart a proxy
Create a binding for every credential an agent tool may use. Each binding has a secret, the permitted destination hosts, and optional header formatting. egressHosts separately allows destinations that do not receive a credential.
import { AgentProxy } from '@stashbase/agent-proxy'
const proxy = new AgentProxy({
// For example, permit an LLM API without granting it a credential binding.
egressHosts: ['api.openai.com'],
bindings: {
GITHUB_TOKEN: {
// Resolve this value in the trusted application.
secret: process.env.GITHUB_TOKEN!,
hosts: ['api.github.com'],
header: 'authorization',
env: 'GITHUB_TOKEN',
},
},
})
await proxy.start()
try {
// Give agent tools proxy.childEnv, not process.env.
console.log(proxy.placeholders.GITHUB_TOKEN)
// ${STASHBASE_GITHUB_TOKEN}
} finally {
await proxy.stop()
}proxy.childEnv contains the configured binding placeholders and the proxy and CA settings required by proxy-aware Node HTTP clients. It is designed for a minimal child-process environment rather than inheriting the trusted application's environment.
Treat binding hosts, egressHosts, and denyHosts as strict allowlists. A credential binding can
be used only for its configured hosts; unbound traffic must match egressHosts and must not match
denyHosts.
Binding options
| Option | Purpose |
|---|---|
secret | The private value retained by the local proxy. |
hosts | Exact outbound hosts where this credential may be injected. |
header | Request header that receives the credential. Defaults to authorization. |
valueTemplate | Header value template. Authorization defaults to Bearer {secret}; other headers default to {secret}. |
env | Environment variable exposed to the tool as a placeholder. It defaults to the binding name. |
Use startLocalAgentProxy(policy) if you prefer a convenience function that starts the proxy immediately. Use AgentProxy when your application needs explicit start, stop, and restart lifecycle control.
OpenAI client
The library can wrap the official OpenAI SDK so the model client's HTTPS transport uses the local policy. Pass an existing client to retain its application-owned API key.
import OpenAI from 'openai'
import { AgentProxy } from '@stashbase/agent-proxy'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! })
const proxy = await new AgentProxy({
egressHosts: [new URL(openai.baseURL).hostname],
bindings: {
GITHUB_TOKEN: {
secret: process.env.GITHUB_TOKEN!,
hosts: ['api.github.com'],
header: 'authorization',
},
},
}).start()
const proxiedOpenAI = proxy.createOpenAIClient(openai)
try {
// Use proxiedOpenAI in the trusted application.
} finally {
await proxy.stop()
}For a client that does not honor proxy environment variables, use createOpenAIProxyFetch(proxy) to obtain an explicit HTTPS-proxy fetch implementation.
Run isolated tools
createSandboxedToolExecutor runs one exported module function in a fresh Node process for every invocation. The worker gets configured placeholders and proxy settings, not secret values.
import { createSandboxedToolExecutor } from '@stashbase/agent-proxy'
const createIssue = createSandboxedToolExecutor({
proxy,
module: new URL('./tools/github.mjs', import.meta.url),
exportName: 'createIssue',
sandbox: true,
timeoutMs: 30_000,
})
const result = await createIssue.execute({
title: 'Example issue',
body: 'Created by an agent tool',
})For modules with several tools, configure shared policy once with createSandboxedToolModule, then expose only the intended named exports. runSandboxedTool is available when you want to invoke a single exported function directly.
sandbox: true restricts a worker's network access to the local proxy. It is supported on macOS with sandbox-exec and on configured Linux hosts with an accessible systemd user manager. It is unsupported on Windows and commonly unavailable in Docker, ECS/Fargate, and minimal Linux images. Without it, a tool that bypasses its proxy configuration can make a direct connection.
Observe requests without secrets
Use lifecycle hooks for audit events, metrics, and tracing. Context is metadata only: it includes fields such as host, port, method, binding name, status, and duration—never request bodies, header values, placeholders, or secret values. Hook failures do not affect proxy policy or traffic.
const proxy = new AgentProxy({
egressHosts: ['api.openai.com'],
bindings: {},
hooks: {
beforeRequest: ({ host }) => metrics.increment('agent_proxy.request', { host }),
afterResponse: ({ durationMs }) => metrics.timing('agent_proxy.duration', durationMs),
onDenied: (event) => audit.warn('agent_proxy.denied', event),
},
})Security boundary
Agent Proxy reduces accidental credential disclosure by keeping resolved secrets out of agent inputs, worker environments, and normal logs. It does not defend against malicious code running with the same operating-system user as the trusted application, which may inspect that process or its files. Use a dedicated account or host when that isolation is required.
The destination API necessarily receives the credential in its authorized request. Agent Proxy does not prevent a tool from returning sensitive data it retrieved with that credential, so continue to validate tool inputs and authorize the operations tools may perform.
Remote Agent Proxy
Use RemoteAgentProxy when Stashbase should resolve the credentials and enforce the policy remotely. The trusted application creates a short-lived Stashbase session with its API key; agents and tool workers receive only placeholders, a localhost relay URL, and CA settings. They never receive resolved secret values or the session token.
Choose the local AgentProxy when your application already resolves secrets and you want all policy enforcement to remain on the host. The remote proxy is a Stashbase-managed control-plane session, so your project and environment permissions are checked when the session is created or refreshed.
Before starting a remote session, create the referenced secrets in the selected Stashbase project and environment. The API key must be allowed to create sessions and access those secrets. Keep the API key in the trusted application only; never pass it to an agent or tool worker.
import { RemoteAgentProxy } from '@stashbase/agent-proxy'
const proxy = new RemoteAgentProxy({
apiKey: process.env.STASHBASE_API_KEY!,
project: 'platform',
environment: 'development',
egressHosts: ['api.openai.com'],
bindings: {
GITHUB_TOKEN: {
// The Stashbase secret name. It defaults to the binding name.
from: 'GITHUB_TOKEN',
hosts: ['api.github.com'],
header: 'authorization',
env: 'GITHUB_TOKEN',
},
},
})
const started = await proxy.start()
if (!started.ok) throw new Error(started.error.message)
try {
// Pass this minimal environment to the agent or tool process.
console.log(proxy.childEnv.GITHUB_TOKEN)
// ${STASHBASE_GITHUB_TOKEN}
} finally {
const stopped = await proxy.stop()
if (!stopped.ok) console.error('Could not fully stop the remote session:', stopped.error)
}start() and stop() return structured results: { ok, data, error, status }. Use startOrThrow() if exception-based startup fits your application, or startRemoteAgentProxy(options) to create and start a session in one call.
Remote bindings and policy
Remote bindings use the same outbound request controls as local bindings, but omit secret. Instead, from identifies the secret in the selected Stashbase project and environment; it defaults to the binding name. hosts, header, valueTemplate, env, and placeholder have the same purpose as their local equivalents.
egressHosts permits unbound destinations, while denyHosts rejects destinations even if they would otherwise be allowed. Keep both lists narrow. The remote session is authoritative for access checks and resolves secret values only for approved binding hosts.
Agent and SDK integration
Pass proxy.childEnv to an agent or createSandboxedToolExecutor exactly as with a local proxy. With sandbox: true, tool workers can reach only the localhost relay while the relay authenticates to the remote session.
The existing adapters work with remote sessions too:
const openai = proxy.createOpenAIClient(OpenAI)
const anthropic = proxy.createAnthropicClient(configuredAnthropic)
const fetch = proxy.createVercelAIFetch()createOpenAIProxyFetch(proxy) is also available for SDKs or clients that need an explicit proxy-aware fetch implementation.
End-to-end: OpenAI and a GitHub tool
This application asks OpenAI for a response and runs a GitHub tool in a sandboxed worker. Create OPENAI_API_KEY and GITHUB_TOKEN secrets in the selected Stashbase environment first. The application, OpenAI client, and worker all use placeholders; Stashbase resolves each secret only at its approved host.
import OpenAI from 'openai'
import { RemoteAgentProxy, createSandboxedToolExecutor } from '@stashbase/agent-proxy'
const proxy = new RemoteAgentProxy({
apiKey: process.env.STASHBASE_API_KEY!,
project: 'platform',
environment: 'development',
egressHosts: [],
bindings: {
OPENAI_API_KEY: {
hosts: ['api.openai.com'],
env: 'OPENAI_API_KEY',
},
GITHUB_TOKEN: {
hosts: ['api.github.com'],
env: 'GITHUB_TOKEN',
},
},
})
const started = await proxy.start()
if (!started.ok) throw new Error(started.error.message)
try {
const openai = proxy.createOpenAIClient(OpenAI)
const getGitHubUser = createSandboxedToolExecutor({
proxy,
module: new URL('./tools/github.mjs', import.meta.url),
exportName: 'getGitHubUser',
sandbox: true,
})
const user = await getGitHubUser.execute({})
const response = await openai.responses.create({
model: 'gpt-5.5',
input: `The authenticated GitHub user is ${user.login}. Say hello.`,
})
console.log(response.output_text)
} finally {
await proxy.stop()
}export async function getGitHubUser() {
const response = await fetch('https://api.github.com/user', {
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
})
if (!response.ok) throw new Error(`GitHub request failed: ${response.status}`)
return response.json()
}CA file and observability
The remote proxy writes its public CA to a temporary ca.pem file and exposes its path through NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, CURL_CA_BUNDLE, and GIT_SSL_CAINFO in childEnv. The file is removed when stop() completes. To choose a managed location, set caFilePath; parent directories are created automatically. Use an absolute path for server applications, since relative paths resolve from the application working directory.
Use remote hooks for operational visibility. They receive metadata only—never credentials, session tokens, request paths, or request bodies.
const proxy = new RemoteAgentProxy({
// session configuration
hooks: {
onSessionRefresh: (event) => {
if (event.state === 'failed') logger.warn(event.error, { retryInMs: event.retryInMs })
},
onRelayError: (event) => logger.warn(event.error, { host: event.host, kind: event.kind }),
},
})Troubleshooting
- If
start()returnsok: false, check itserror.code,error.message, andstatus. Confirm the API key can access the specified project, environment, and secret names. - If a session starts but a request is denied, verify the binding
hostsexactly matches the destination hostname and that the tool sends the configured placeholder in the configured header. - If a worker cannot connect, pass the supplied
proxyto the sandboxed executor and do not overwrite itschildEnvproxy or CA variables. On macOS and supported Linux hosts,sandbox: trueis the stronger boundary; it is not supported on Windows. - Use
onSessionRefreshfor refresh failures andonRelayErrorfor localhost-relay failures. Both hooks intentionally omit request bodies, paths, credentials, and session tokens.
API reference
The package exports AgentProxy, startLocalAgentProxy, RemoteAgentProxy, startRemoteAgentProxy, createOpenAIProxyClient, createOpenAIProxyFetch, createAnthropicProxyClient, createVercelAIProxyFetch, createSandboxedToolExecutor, createSandboxedToolModule, and runSandboxedTool.
See the package on npm for the published release and types.