Skip to main content

Managing Secrets

This guide covers creating encrypted credentials, configuring injection routes, and rotating keys — all without redeploying agent containers.

Prerequisites

  • Tenant-level API key (secrets require tenant auth)
  • API credentials to store (OpenAI key, Anthropic key, etc.)

Creating a Secret

const response = await fetch("https://api.steward.fi/secrets", {
  method: "POST",
  headers: {
    "X-Steward-Key": "your-tenant-key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "openai-prod",
    value: "sk-proj-abc123...",
    description: "Production OpenAI API key",
    expiresAt: "2027-01-01T00:00:00Z", // optional
  }),
});

const { data } = await response.json();
console.log(data.id);   // "550e8400-..."
console.log(data.name); // "openai-prod"
// Note: the value is NEVER returned in responses

Creating a Route

Routes tell the proxy how to inject credentials when an agent makes a request:
// Route for OpenAI
await fetch("https://api.steward.fi/secrets/routes", {
  method: "POST",
  headers: {
    "X-Steward-Key": "your-tenant-key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    secretId: "550e8400-...", // the secret ID from above
    hostPattern: "api.openai.com",
    pathPattern: "/*",
    injectAs: "header",
    injectKey: "Authorization",
    injectFormat: "Bearer {value}",
    priority: 0,
    enabled: true,
  }),
});

Common Route Examples

{
  "secretId": "openai-secret-id",
  "hostPattern": "api.openai.com",
  "pathPattern": "/*",
  "injectAs": "header",
  "injectKey": "Authorization",
  "injectFormat": "Bearer {value}"
}
{
  "secretId": "anthropic-secret-id",
  "hostPattern": "api.anthropic.com",
  "pathPattern": "/*",
  "injectAs": "header",
  "injectKey": "x-api-key",
  "injectFormat": "{value}"
}
{
  "secretId": "birdeye-secret-id",
  "hostPattern": "public-api.birdeye.so",
  "pathPattern": "/defi/*",
  "injectAs": "header",
  "injectKey": "X-API-KEY",
  "injectFormat": "{value}"
}
{
  "secretId": "github-pat-id",
  "hostPattern": "api.github.com",
  "pathPattern": "/repos/acme/widgets/issues/1/comments",
  "method": "POST",
  "injectAs": "header",
  "injectKey": "Authorization",
  "injectFormat": "Bearer {value}"
}
Credentials are injected as request headers only. Query-parameter and request-body injection are not supported: upstream responses can reflect query strings and bodies, which would risk leaking the injected credential back to the agent.

GitHub Fine-Grained Tokens

GitHub is an allowlisted credential host, but routes for api.github.com are held to an extra narrowness standard so a single token can only be attached to the exact endpoint you intend:
  • The route must specify an explicit method (no wildcard *).
  • The pathPattern must have at least two path segments (/repos/acme/widgets, not / or /user).
A route that violates either rule is rejected at creation time.

1. Mint a fine-grained PAT

Create a fine-grained personal access token in GitHub with the minimum scope for the job:
  • Resource owner / repositories: select only the specific repositories the agent needs (for example acme/widgets), never “All repositories.”
  • Permissions: grant only what the endpoint requires (for example Issues: Read and write for posting issue comments). Leave everything else at No access.
  • Expiration: set a short expiration and rotate on schedule.

2. Store the token as a secret

curl -X POST https://api.steward.fi/secrets \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "github-acme-widgets",
    "value": "github_pat_...",
    "description": "Fine-grained PAT: acme/widgets issues read+write"
  }'

3. Create a narrow route

Scope the route to one method and one deep path. GitHub accepts both Authorization: Bearer <token> and Authorization: token <token>; either injectFormat works.
curl -X POST https://api.steward.fi/secrets/routes \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "secretId": "github-acme-widgets-id",
    "agentId": "your-agent-id",
    "hostPattern": "api.github.com",
    "pathPattern": "/repos/acme/widgets/issues/1/comments",
    "method": "POST",
    "injectAs": "header",
    "injectKey": "Authorization",
    "injectFormat": "Bearer {value}"
  }'

4. Call through the proxy

The agent calls the github alias with its own agent token. The proxy matches the route, injects the PAT as the Authorization header, and forwards the request. The agent never sees the token.
curl -X POST https://proxy.steward.fi/github/repos/acme/widgets/issues/1/comments \
  -H "Authorization: Bearer <agent-token>" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "body": "posted by the agent via steward" }'
Mutating proxy requests (POST, PUT, PATCH, DELETE) require an Idempotency-Key header.

Listing Secrets

curl https://api.steward.fi/secrets \
  -H "X-Steward-Key: your-key"
{
  "ok": true,
  "data": [
    {
      "id": "550e8400-...",
      "name": "openai-prod",
      "description": "Production OpenAI API key",
      "version": 1,
      "createdAt": "2026-03-26T12:00:00Z"
    },
    {
      "id": "660f9500-...",
      "name": "anthropic-prod",
      "description": "Anthropic production key",
      "version": 2,
      "createdAt": "2026-03-20T10:00:00Z"
    }
  ]
}

Rotating a Secret

When you need to update a credential (e.g., API key rotation), use the rotate endpoint:
curl -X POST https://api.steward.fi/secrets/550e8400-.../rotate \
  -H "X-Steward-Key: your-key" \
  -H "Content-Type: application/json" \
  -d '{ "value": "sk-proj-new-key-xyz..." }'
This:
  1. Creates a new encrypted version of the secret
  2. Routes automatically use the latest version
  3. No container redeployment needed — agents keep making the same proxy calls

Deleting a Secret

curl -X DELETE https://api.steward.fi/secrets/550e8400-... \
  -H "X-Steward-Key: your-key"
Deleting a secret will cause proxy requests that depend on it to fail with 403. Make sure no active routes reference the secret before deletion.

Managing Routes

List Routes

curl https://api.steward.fi/secrets/routes \
  -H "X-Steward-Key: your-key"

Update a Route

curl -X PUT https://api.steward.fi/secrets/routes/route-id \
  -H "X-Steward-Key: your-key" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

Delete a Route

curl -X DELETE https://api.steward.fi/secrets/routes/route-id \
  -H "X-Steward-Key: your-key"

Priority-Based Routing

When multiple routes match a request, the one with the highest priority wins:
// Trading API gets a privileged credential (priority 10)
await createRoute({
  secretId: "trading-api-prod",
  hostPattern: "api.example.com",
  pathPattern: "/v2/trading/*",
  priority: 10,
  // ...
});

// Everything else gets read-only credential (priority 0)
await createRoute({
  secretId: "api-read-only",
  hostPattern: "api.example.com",
  pathPattern: "/*",
  priority: 0,
  // ...
});