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 fieldTypeDescription
hoststringFQDN the credential applies to
pathPrefixstring (optional)Narrows the binding to requests whose path starts with this
injectionCredentialInjectionWhere the value goes — see below

Injection modes

type CredentialInjection =
  | { type: "header"; name: string }
  | { type: "substitution"; placeholder: string; in: Array<"path" | "query" | "header" | "body"> };

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 from process.env automatically, no extra code needed.
  • { type: "external" } (the default if you omit source entirely) — 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.