Credentials
Register credentials that get injected into outbound requests without the sandbox process ever holding them.
Sandboxes often need to call an authenticated API — GitHub, Slack, an internal service. The obvious way is to pass a token through env, but that puts it somewhere any code running in the sandbox can read, log, or accidentally echo back to you — including code an agent wrote itself.
sb.credentials.set() registers a credential and a rule for where it applies. From then on, matching outbound requests get the credential injected automatically, at the network layer — the value itself never enters the container's filesystem or environment.
Enabling it
Credential injection rides on the same opt-in egress layer as network policy — pass credentialProxy: true when creating the sandbox:
const sb = await client.sandbox({
image: "node:22",
resources: { cpu: "500m", memory: "512Mi" },
networkPolicy: { defaultAction: "allow", egress: [] },
credentialProxy: true,
});A sandbox created without networkPolicy/credentialProxy has ordinary, unrestricted egress — nothing here applies to it, and sb.credentials.* throws if you call it on one. networkPolicy itself is a separate concern (which hosts the sandbox may reach at all, allow/deny style — see Network policy); credentialProxy is what makes injection available. Setting defaultAction: "allow" with an empty egress list, as above, keeps egress wide open while turning injection on — tighten egress if you also want to restrict which hosts are reachable at all. credentialProxy also needs the server on egress.mode = "dns+nft" (the alineo init default).
Registering a credential
await sb.credentials.set("github", process.env.GH_TOKEN!, {
host: "api.github.com",
injection: { type: "header", name: "Authorization" },
});Any request the sandbox makes to api.github.com now gets that header added automatically — a plain curl https://api.github.com/user with no Authorization header of its own comes back authenticated. Requests to any other host are untouched. Run env inside the sandbox and the token isn't there.
CredentialBinding field | Type | Description |
|---|---|---|
host | string | FQDN the credential applies to |
pathPrefix | string (optional) | Narrows the binding to requests whose path starts with this |
injection | CredentialInjection | Where the value goes — see below |
Injection modes
type CredentialInjection =
| { type: "header"; name: string }
| { type: "substitution"; placeholder: string; in: Array<"path" | "query" | "header" | "body"> };header (recommended)
Adds name: <value> to every matching outbound request. This is the default and the right choice for anything that takes a bearer token or API-key header.
injection: { type: "header", name: "Authorization" }substitution
For APIs that want the secret in the URL or body rather than a header. The sidecar replaces every literal occurrence of placeholder in the listed request surfaces with the real value.
await sb.credentials.set("openai", process.env.OPENAI_API_KEY!, {
host: "api.example.com",
injection: { type: "substitution", placeholder: "__API_KEY__", in: ["query"] },
});The outbound request must already contain placeholder verbatim — the sidecar only substitutes, it doesn't append. Put it in the base URL you give your client:
// inside the sandbox
fetch("https://api.example.com/v1/data?key=__API_KEY__");
// leaves the sandbox as ...?key=<real value>sb.credentials.listBindings() is lossy for substitution bindings (the vault doesn't echo the substitution config back), but resume() / fork() recover the full shape from the ledger.
Migrating from query / path injection
The old { type: "query"; param } and { type: "path"; segment } shapes were removed in 0.4.0 (they only ever threw UnsupportedInjectionError). Replace { type: "query"; param: "k" } with { type: "substitution"; placeholder: "__CRED__"; in: ["query"] } and add ?k=__CRED__ to the request URL.
Removing a credential
await sb.credentials.remove("github");Requests to that host go out unauthenticated from then on.
Where the value comes from — source
set() takes an optional fourth argument describing how to re-derive the value later. It's not needed for a sandbox you register a credential on and close normally — only resume() and fork() (below) ever read it:
await sb.credentials.set(
"github",
process.env.GH_TOKEN!,
{ host: "api.github.com", injection: { type: "header", name: "Authorization" } },
{ type: "env", varName: "GH_TOKEN" },
);{ type: "env", varName }— re-read fromprocess.envautomatically, no extra code needed.{ type: "external" }(the default if you omitsourceentirely) — alineo has no way to reproduce this value on its own (a one-time minted token, something generated at call time). Resuming or forking a sandbox with one of these requires you to supply it explicitly — see below.
resume() and fork() never drop a credential silently
If a sandbox with bound credentials is resumed or forked and one of them can't be resolved, alineo throws rather than quietly continuing without it — a credential that silently stops being injected is a worse failure mode than a loud one.
const sb = await client.resume(sandboxId, {
resolveCredential: (name, source) => {
if (name === "one-time-token") return mintFreshToken();
return undefined; // let anything else fall through to the env-var default, if it has one
},
});Anything registered with { type: "env" } resolves on its own. Anything else needs resolveCredential to return a value for it, or the call throws naming exactly which credential it couldn't resolve.
sb.fork() carries over the parent's own bound credentials to the child automatically — the child starts out able to make the same authenticated requests the parent could, with no re-registration needed. The same resolution rules apply: pass resolveCredential to fork() if any of the carried-over credentials need one.
Custom credential backends
sb.credentials.* is backed by a CredentialBroker interface (the same shape as IStorageAdapter — see Storage adapters). @alineo-labs/vault's OpenSandboxCredentialBroker is the default, wired up automatically — nothing to configure for the common case. Pass SandboxClientOptions.credentialBroker to use a different implementation.