# Credential-Scoped Agent
URL: /docs/cookbooks/credential-scoped-agent

An agent that calls an authenticated API with a token it can never read — injected at the egress layer.



<CookbookMeta difficulty="Advanced" time="~10 min" primitives="[&#x22;credentialProxy&#x22;, &#x22;Credential Vault&#x22;, &#x22;egress injection&#x22;]" />

An agent that does real work against an authenticated API — GitHub, here — using a token it can
never read. The token is registered as a **credential**, not an environment variable: it's
injected into matching outbound requests at the egress layer, so the agent can call
`api.github.com` as you while the value itself never enters the container's filesystem or
environment. Revoking it takes effect immediately, mid-session, without touching the running
sandbox.

## Setup [#setup]

<Steps>
  <Step>
    Start OpenSandbox in Docker (one-time setup):

    ```bash
    bunx alineo-cli init
    ```
  </Step>

  <Step>
    Export the agent's model key and a GitHub token:

    ```bash
    export NVIDIA_API_KEY=...   # https://build.nvidia.com — free tier
    export GH_TOKEN=...         # a GitHub PAT (classic or fine-grained); read-only scopes are enough
    ```

    `GH_TOKEN` is just the raw token — the agent spec (`agents/github-agent.json`) wraps it as
    `Bearer ${GH_TOKEN}` so the injected `Authorization` header is well-formed for GitHub.
  </Step>

  <Step>
    Credential injection rides on the same egress layer as network policy, so the server needs
    `egress.image` configured and `egress.mode = "dns+nft"` — the default for `alineo init` since
    this feature landed. On an older local config, add this to `~/.config/alineo/server.toml` and
    restart the server:

    ```toml
    [egress]
    image = "opensandbox/egress:v1.1.7"
    mode = "dns+nft"
    ```
  </Step>
</Steps>

## Run it [#run-it]

<CookbookPlayground
  cwd="cookbooks/credential-scoped-agent"
  repoHref="https://github.com/DrejT/alineo/tree/main/cookbooks/credential-scoped-agent"
  commands="[
  { command: &#x22;bun install&#x22; },
  { command: &#x22;bun start&#x22; },
]"
  output="`Loading agents/github-agent.json — GITHUB_TOKEN is a credential binding, not a plain env var.
credentialProxy: true — token registered with the egress sidecar's Credential Vault.

Agent: curl https://api.github.com/user
→ 200 OK — authenticated as &#x22;your-username&#x22;

Audit — env | grep TOKEN inside the sandbox: (nothing)
Audit — bare curl with no Authorization header: 200 OK (injected at the sidecar, not by the agent)

Revoking GITHUB_TOKEN via agent.sandbox.credentials.remove(&#x22;GITHUB_TOKEN&#x22;)...
Audit — same bare request, again: 401 Unauthorized`"
/>

## What it does [#what-it-does]

<Steps>
  <Step>
    **`Alineo.start()` reads `agents/github-agent.json`**, whose `env.GITHUB_TOKEN` is a
    `{ credential, host, injection }` binding rather than a string. Because at least one binding is
    present, `load()` creates the sandbox with `credentialProxy: true` and registers the token with
    the egress sidecar's Credential Vault — it never becomes a container env var.
    `env.NVIDIA_API_KEY`, an ordinary string, is exported the normal way.
  </Step>

  <Step>
    **The agent does authenticated GitHub work** — told to `curl https://api.github.com/user` and
    report the login, it does — with no token and no `Authorization` header of its own, and the call
    comes back authenticated as you. The recipe echoes the agent's `bash` command and its output, so
    you see the real request and the real response.
  </Step>

  <Step>
    **Audit** — `env | grep` inside the sandbox turns up nothing, and a bare
    `curl https://api.github.com/user` with no `Authorization` header of its own still returns
    `HTTP 200`: the credential reached the request at the sidecar, not through anything the agent
    could see.
  </Step>

  <Step>
    **Revoke** — `agent.sandbox.credentials.remove("GITHUB_TOKEN")`, and the same bare request now
    returns `HTTP 401`. The agent's own next call to `api.github.com` would fail the same way.
  </Step>
</Steps>

<Callout title="The point">
  `AgentSpec.env` is the obvious place to hand an agent a secret, but a plain env var is readable by
  anything running in the sandbox — including code the agent wrote itself, and including a
  prompt-injected instruction to print it. A credential binding gives the agent the *capability*
  (authenticated calls to one host) without the *secret*, and leaves you holding the leash: one
  `remove()` call cuts access at the network layer, with no redeploy and nothing to clean up inside
  the container.
</Callout>

<Accordions>
  <Accordion title="Where to go next">
    * [`examples/credential-injection`](https://github.com/DrejT/alineo/tree/main/examples/credential-injection)
      — the same mechanism at the raw `Sandbox` level (no agent), plus `fork()` carrying a bound
      credential to the child automatically and the `source` / `resolveCredential` contract for
      `resume()`.
    * [Credentials](https://docs.alineo.tech/docs/core/concepts/credentials) in the docs for the full
      `sb.credentials.*` API, `pathPrefix` scoping, and how bindings behave across
      `resume()` / `fork()` / spawned children.
  </Accordion>

  <Accordion title="Binding types and injection modes">
    The binding here uses `credential: "Bearer ${GH_TOKEN}"`, an interpolated string rather than a
    bare `${GH_TOKEN}` — so its `CredentialSource` is `{ type: "external" }`, not `{ type: "env" }`.
    That only matters for `resume()` / `fork()` (which would then need an explicit
    `resolveCredential` callback); this recipe loads the agent, uses it, and closes it, so it never
    comes up. Use a bare `${GH_TOKEN}` (and a token that already includes its scheme) if you want
    env-based auto-resolution.

    Two `injection` types are supported: `{ type: "header", name }` (the common case, shown above)
    and `{ type: "substitution", placeholder, in }`, which replaces a literal placeholder string in
    the request's path/query/header/body with the value — use it for APIs that take the key in the
    URL. The request must already contain the placeholder verbatim (e.g. put it in a base URL).
  </Accordion>
</Accordions>
