Skip to content

Credentials#

A credential is a secret the platform holds on an agent's behalf, plus the rules for where it may be attached. The agent authenticates to real systems and never learns the secret.


Create a credential#

curl -fsS -X POST \
  -H "Authorization: Bearer $LENS_AGENTS_TOKEN" \
  -H "Content-Type: application/json" \
  https://agents.example.com/v1/projects/$PROJECT_ID/credentials \
  -d '{
    "name": "github-token",
    "value": "'"$GITHUB_TOKEN"'",
    "injections": [{
      "domain": "api.github.com",
      "headerName": "Authorization",
      "headerFormat": "Bearer {value}"
    }]
  }'
Create a credential called github-token in the production project.
Inject it on api.github.com as the Authorization header with the
format "Bearer {value}". I will paste the value.

Prefer the CLI with --value-stdin for real secrets — a prompt leaves the value in the agent's conversation history.

printf '%s' "$GITHUB_TOKEN" | nexusctl credential create \
  --project production \
  --name github-token \
  --value-stdin \
  --inject domain=api.github.com,header=Authorization,format="Bearer {value}"

Names are lowercase alphanumeric with hyphens. --inject is repeatable, so one secret can be attached to several domains.

Values are write-only. credential list and credential get return the metadata and never the secret.

Use --value-stdin rather than --value. The explicit form leaks the secret into your shell history.


Injection#

An injection says: on requests to this domain, set this header to this format.

Field Purpose
domain Target hostname.
headerName Header to set.
headerFormat Format string containing the {value} placeholder — Bearer {value}, token {value}, or the raw {value}.
rules Optional method and path restrictions. Omit to inject on every request to the domain.

Narrowing with rules#

An injection with no rules attaches the credential to every request to that host. Rules make it precise:

{
  "domain": "api.internal.example.com",
  "headerName": "X-API-Key",
  "headerFormat": "{value}",
  "rules": [
    { "method": "GET", "path": "/v1/reports/*" }
  ]
}

Now the credential is attached only to GET requests under /v1/reports/. A DELETE to the same host goes out unauthenticated and fails at the upstream — which is the point. The agent cannot use a read credential to write, because the write never carries it.

Paths are glob patterns. Omit method or path to match any.


Where the substitution happens#

sequenceDiagram
    participant A as Agent
    participant P as Egress proxy
    participant U as Upstream

    A->>P: Authorization: Bearer <placeholder>
    P->>P: Match domain, method, path
    P->>P: Substitute real value
    P->>U: Authorization: Bearer ghp_real...
    U-->>P: 200
    P-->>A: 200

The agent's environment holds a placeholder. The proxy substitutes the real value into the outbound request after policy has allowed it, using TLS interception via the sandbox's ephemeral certificate authority.

An agent that prints its environment, reads its filesystem, or is prompt-injected into exfiltrating its configuration leaks placeholders. There is no code path that puts the real value where the agent can read it.


OAuth client credentials#

Some SDKs authenticate with the OAuth 2.0 client-credentials grant, which trades a client secret for a token at a token endpoint, so a fixed header value cannot stand in for it. A credential can hold that OAuth client instead of a value, so the secret still stays out of the sandbox.

curl -fsS -X POST \
  -H "Authorization: Bearer $LENS_AGENTS_TOKEN" \
  -H "Content-Type: application/json" \
  https://agents.example.com/v1/projects/$PROJECT_ID/credentials \
  -d '{
    "name": "graph-bot",
    "oauthClientCredentials": {
      "tokenUrl": "https://login.example.com/oauth2/token",
      "clientId": "<client-id>",
      "clientSecret": "'"$CLIENT_SECRET"'",
      "scope": "https://api.example.com/.default"
    },
    "injections": [{
      "domain": "api.example.com",
      "headerName": "Authorization",
      "headerFormat": "Bearer {value}"
    }]
  }'

oauthClientCredentials replaces value; a credential has one or the other. tokenUrl must be https, and scope is optional. In the admin UI, choose Secret: OAuth client credentials in the credential dialog. A sandbox-owned credential takes the same oauthClientCredentials block.

The platform mints the access token and renews it, and injects the current token on the sandbox proxy path like any other value. The SDK inside the sandbox still runs its own grant, with the credential's placeholder as its client secret. The sandbox proxy answers that token request itself with a placeholder token, and the header injection swaps in the real token on the way to each API. Neither the client secret nor a working token ever enters the sandbox.

The first token is minted when the credential is created, so a client the provider refuses — or one whose tokens last 20 minutes or less — is refused then, not when an agent first needs it. The policy must allow the token endpoint's host as well as the API's, because the sandbox proxy answers only a token request the policy lets through.


Attaching credentials to agents#

A credential exists in a project. A policy grants it:

name: github-readonly
allowedDomains:
  - pattern: api.github.com
    verdict: allow
    transport: upstream
credentials:
  - credentialName: github-token
    envVarKey: GITHUB_TOKEN

envVarKey sets the environment variable the placeholder appears under, so tooling that expects GITHUB_TOKEN works unchanged.

Both parts are required. Allowing the domain without the credential gives an unauthenticated agent; attaching the credential without allowing the domain gives an agent that cannot connect.


Sandbox-owned credentials#

A credential only one agent needs can live on the sandbox itself rather than in the project catalog:

credentials:
  github-token:
    value: ${GITHUB_TOKEN}
    injections:
      - domain: api.github.com
        headerName: Authorization
        headerFormat: "Bearer {value}"

${VAR} is expanded from your environment at apply time, so the spec is committable. The credential is private to that sandbox and deleted with it.


Other credential paths#

Not everything is a header.

System Mechanism
Kubernetes Platform-signed requests, agent identity presented to the API server.
AWS SigV4 re-signing with temporary STS credentials.
MCP connectors and HTTP connectors Static bearer, OAuth client credentials, or per-actor OAuth authorization code, optionally through an OAuth application the operator configured for the whole install.
Models Provider key held by the install or the organization, never exposed to a policy or the sandbox.

All of them share the property that matters: the secret is applied outside the sandbox.


Storage#

Credential values are encrypted at rest with the install's encryption.key. Deleting a credential removes its header injections with it.