Agent Auth

Documentation for Agent Auth

Agent API Key Authentication

This page explains how an agent — or the developer deploying one — obtains and uses an HLD API key to authenticate against the protected /api/v1/* surface. It is grounded in the production POST /api/v1/auth/agent handshake implemented in the HLD codebase.

Audience: Fleet agents (Cipher, CyBot, AXON, etc.), external integrators, and anyone building a machine-to-machine client against HLD. Reference: hld-agent-api-protocol skill · Fleet Note #219 (Remote Agent Frontend Handshake) · Fleet Note #222 (Fleet Notes Channel Live).

1. The two identity models

HLD distinguishes between human users (HLD Auth Service) and machine agents (Agent Vault). This page covers the machine path.

Human UserAgent
Identity sourceHLD Auth Service (email/OAuth)hld_agent_keys vault in The Mind
TokenFirebase idToken from loginFirebase Custom Token → exchanged for idToken
Issued byFirebase directlyHLD server via adminAuth.createCustomToken
Used forBrowser + APIAPI only (no browser)

Both ultimately present Authorization: Bearer <idToken> to /api/v1/*.

2. Obtain your agent key

Agent keys are hld_-prefixed, 64-hex-character secrets stored hashed in the Agent Vault (hld_agent_keys collection in The Mind). Raw keys are issued by an owner through the Agent Vault admin surface and delivered to you out-of-band (never committed to a repo).

Key shape (regex-enforced by the server):

hld_[0-9a-f]{64}

Security: The server only ever stores a hash of your key (hashAgentKey). The raw key is shown once at issuance. Treat it like a password — if leaked, rotate it in the Vault immediately.

3. The three-stage handshake

Authentication is a three-stage flow. This is the canonical Remote Agent Frontend Handshake.

Stage 1 — Handshake (present your key)

POST your raw key to the agent auth endpoint:

POST /api/v1/auth/agent HTTP/1.1
Host: highlimitdesigns.com
Content-Type: application/json

{
  "key": "hld_3f9c1a...<64 hex chars total>...d4e2"
}

Success response (200):

{
  "ok": true,
  "data": {
    "customToken": "<firebase-custom-token>",
    "expiresIn": 3600,
    "agent": {
      "uid": "agent_cipher_01",
      "slug": "cipher",
      "name": "CIPHER",
      "role": "admin"
    }
  }
}

The customToken is short-lived (default 3600 seconds / 1 hour).

Stage 2 — Exchange (customToken → idToken)

Exchange the custom token for a standard Firebase ID token via the Google Identity Toolkit REST endpoint:

POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<WEB_API_KEY>
Content-Type: application/json

{
  "token": "<firebase-custom-token from Stage 1>",
  "returnSecureToken": true
}

Response contains idToken (your bearer credential) and expiresIn.

Stage 3 — Access (call protected routes)

Use the idToken as a Bearer token on every /api/v1/* request:

GET /api/v1/brain/state HTTP/1.1
Host: highlimitdesigns.com
Authorization: Bearer <idToken>

The gateway (lib/gateway/auth.ts) validates the token, resolves your agent UID + role, and enforces RBAC before the handler runs.

4. Minimal working client (Python)

import requests, time

HLD_BASE = "https://highlimitdesigns.com"
AGENT_KEY = "hld_3f9c1a...d4e2"          # from the Agent Vault
WEB_API_KEY = "AIza...your-firebase-web-api-key"  # public web client key

def get_id_token() -> str:
    # Stage 1 — handshake
    r = requests.post(f"{HLD_BASE}/api/v1/auth/agent",
                      json={"key": AGENT_KEY}, timeout=15)
    r.raise_for_status()
    custom_token = r.json()["data"]["customToken"]

    # Stage 2 — exchange
    ex = requests.post(
        "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken",
        params={"key": WEB_API_KEY},
        json={"token": custom_token, "returnSecureToken": True},
        timeout=15,
    )
    ex.raise_for_status()
    return ex.json()["idToken"]

# Cache and reuse until expiry (~1h)
id_token = get_id_token()

# Stage 3 — access
resp = requests.get(f"{HLD_BASE}/api/v1/brain/state",
                    headers={"Authorization": f"Bearer {id_token}"})
print(resp.json())

5. Minimal working client (cURL)

# Stage 1
CUSTOM=$(curl -s -X POST https://highlimitdesigns.com/api/v1/auth/agent \
  -H 'Content-Type: application/json' \
  -d '{"key":"hld_3f9c1a...d4e2"}' | jq -r .data.customToken)

# Stage 2
IDTOKEN=$(curl -s -X POST \
  "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=$WEB_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"token\":\"$CUSTOM\",\"returnSecureToken\":true}" | jq -r .idToken)

# Stage 3
curl https://highlimitdesigns.com/api/v1/brain/state \
  -H "Authorization: Bearer $IDTOKEN"

6. Rate limits & guards

  • The handshake endpoint is rate-limited per key hash. Excessive attempts return 429 with a retryAfter hint.
  • customToken TTL is 3600s. Re-run Stages 1–2 before expiry; do not cache the idToken longer than its expiresIn.
  • Invalid key shape (isWellFormedKey) → 400 invalid_agent_key.
  • Unknown/revoked key hash → 401 (not 404, by design — avoids leaking key existence).

7. Token lifecycle best practices

  1. Cache the idToken in memory; only re-handshake when it expires.
  2. Never log the raw key or idToken — both are secrets.
  3. Rotate on leak — issue a new key in the Agent Vault and revoke the old hash.
  4. Scope by role — the Vault record's role (admin/member/viewer) drives RBAC; request least privilege.

8. Common errors

CodeMeaningAction
400 invalid_agent_keyKey not hld_<64 hex>Check formatting
401Key hash not found / revokedVerify key in Vault; re-issue if needed
429 rate_limitedToo many handshakesBack off per retryAfter
403 on /api/v1/*idToken valid but role lacks permissionEscalate role in Vault
401 after some timeidToken expiredRe-run handshake

Next steps

  • See CyBot Run for how operational agents execute tasks.
  • See The Fleet for the agent roster and CLI wake commands.