For Agents
This document is written for AI agents that need to authenticate with and use Gatehouse. It covers every step from login to secret access. All examples use curl but any HTTP client works.
Base URL: http://<gatehouse-host>:3100
Authentication#
Onboarding links (the common case)#
Your operator probably handed you a one-time URL along the lines of https://gatehouse.example/v1/onboard/<token>, wrapped in a prompt that tells you to curl it. Do exactly that:
curl -fsSL "https://gatehouse.example/v1/onboard/<token>" > /tmp/gatehouse-onboard.md
cat /tmp/gatehouse-onboard.md
The response is self-installing markdown. It contains your identity, a bootstrap token, and step-by-step instructions for your harness (Hermes, OpenClaw, Claude Code, Codex/Cursor/Windsurf). Follow the steps in the file: call POST /v1/onboard/<token>/exchange exactly once to receive your rotated role_id, secret_id, and a 24h JWT, then persist the credentials and install the gatehouse skill as instructed. The bootstrap token is single-use; fetching the markdown is safely repeatable until you exchange.
Direct AppRole login (alternative)#
If your operator handed you role_id and secret_id directly, exchange them for a JWT:
curl -X POST http://localhost:3100/v1/auth/approle/login \
-H "Content-Type: application/json" \
-d '{"role_id": "YOUR_ROLE_ID", "secret_id": "YOUR_SECRET_ID"}'
Response:
{
"token": "eyJhbGciOi...",
"identity": "approle:your-agent-name",
"policies": ["agent-readonly"],
"expires_in": 86400
}
Use the token value as a Bearer token in all subsequent requests:
Authorization: Bearer eyJhbGciOi...
The token expires after 24 hours. When you get a 401 response, re-login to get a fresh token.
Refreshing the JWT before it expires#
For long-running agents, exchange a still-valid JWT for a new one with a fresh 24h TTL using POST /v1/auth/refresh. This avoids re-reading role_id / secret_id from your environment on every cycle.
curl -X POST http://localhost:3100/v1/auth/refresh \
-H "Authorization: Bearer eyJhbGciOi..."
Response shape matches /v1/auth/approle/login:
{
"token": "eyJhbGciOi...",
"identity": "approle:your-agent-name",
"policies": ["agent-readonly"],
"expires_in": 86400
}
Refresh re-checks AppRole suspension, IP allowlist, and the current policy list, so the new token reflects any operator changes since your last login. Refresh does not issue a new JWT from an expired one — 401 from /refresh means the token is past its TTL or the AppRole was deleted/suspended; fall back to a full re-login from role_id / secret_id.
Refreshing the installed skill (without re-onboarding)#
When the operator ships an updated Gatehouse skill template, existing agents stay on whatever skill they got at first install until they re-onboard. Re-onboarding side-effects a secret_id rotation. To pick up skill improvements without rotating credentials, fetch the current skill body for your policies:
curl http://localhost:3100/v1/skill \
-H "Authorization: Bearer eyJhbGciOi..."
The response is text/markdown containing the same skill content the onboarding flow would install for an AppRole with your policies, with the situation table rendered against your current capabilities. Overwrite your installed skill file in place. No credential changes happen.
Credential rotation (operator-initiated)#
The operator generates a one-shot rotate URL via the admin UI or POST /v1/rotate (admin-auth). They hand the URL to you (out-of-band, the same way they handed you the original onboarding link).
Fetch the URL to receive markdown instructions:
curl http://localhost:3100/v1/rotate/<rotate-token>
Then exchange exactly once to receive the new secret_id:
curl -X POST http://localhost:3100/v1/rotate/<rotate-token>/exchange
Response:
{
"role_id": "role-...",
"secret_id": "<new-secret-id>",
"base_url": "http://localhost:3100",
"role_display_name": "your-agent-name"
}
The role_id, your policies, and the installed skill are unchanged. Only GATEHOUSE_SECRET_ID in your env file needs to be rewritten with the new value. Any JWT you currently hold remains valid until its 24h TTL.
If exchange returns 410 Gone, the rotate token is consumed or expired. The operator must generate a new one.
Checking your current identity and token expiry#
GET /v1/auth/whoami introspects the current bearer token. Useful for confirming who you are, what policies are attached, and how much time is left before you need to /refresh.
curl http://localhost:3100/v1/auth/whoami \
-H "Authorization: Bearer eyJhbGciOi..."
Response:
{
"identity": "approle:your-agent-name",
"policies": ["agent-readonly"],
"source": "approle",
"expires_at": "2026-04-26T00:00:00.000Z",
"expires_in": 86345
}
source is one of "approle", "user", or "root". Root tokens never expire, so expires_at and expires_in are absent in that case.
Important: secrets are accessed by path, not by ID#
Secrets in Gatehouse have human-readable paths like api-keys/example or services/memos-token. You always use the path in API URLs, never a UUID.
Do not confuse secret_id with a secret path. The secret_id from AppRole login is a vault credential (like a password). It is not a reference to a stored secret. After login, discard it from your working context. To find actual secrets, use GET /v1/secrets?prefix= (see “List available secrets” below).
What you can do depends on your policy#
Your AppRole has one or more policies that control which secret paths you can access and what operations you can perform. Common capabilities:
| Capability | What it allows |
|---|---|
read | Read a secret’s value |
list | List secret paths under a prefix (note: read also grants listing) |
write | Create or update secrets |
delete | Delete secrets |
lease | Check out a secret with a TTL (also used for dynamic secrets) |
proxy | Use secrets through the HTTP proxy without seeing raw values |
admin | Full access to configuration and management |
If you get {"error": "Forbidden"}, your policy does not grant the required capability on that path. Ask your operator to check your policy rules.
Reading secrets#
List available secrets#
# List everything you have access to
curl http://localhost:3100/v1/secrets?prefix= \
-H "Authorization: Bearer $TOKEN"
# Or filter by prefix
curl http://localhost:3100/v1/secrets?prefix=api-keys/ \
-H "Authorization: Bearer $TOKEN"
Response:
{
"secrets": [
{
"path": "services/memos-pat",
"version": 1,
"created_at": "...",
"metadata": {"allowed_domains": "10.0.0.102:5230", "header_name": "Authorization"},
"pattern_count": 4,
"top_pattern": "POST http://10.0.0.102:5230/api/v1/memos"
},
{"path": "api-keys/anthropic", "version": 1, "created_at": "...", "pattern_count": 0}
]
}
The response is filtered to only show secrets you can use via any capability (read, list, proxy, or lease). Use an empty prefix to discover everything available to you.
Two fields matter for figuring out how to use a secret without probing:
pattern_count— how many known-good request shapes other agents have verified for this secret.top_pattern— whenpattern_count > 0, aMETHOD url_templatepreview of the highest-confidence pattern. This IS the endpoint; don’t scan for ports.
If pattern_count > 0, call gatehouse_patterns (MCP) or GET /v1/proxy/patterns?secret=<path> for the full verified pattern including headers and body schema, then copy that shape. If pattern_count == 0, use metadata.allowed_domains as the canonical host; your first successful proxy call seeds a pattern for the next agent. If gatehouse_list returns an empty array, your policy grants nothing — stop and tell the operator rather than probing.
Get a secret’s value#
curl http://localhost:3100/v1/secrets/api-keys/example/value \
-H "Authorization: Bearer $TOKEN"
Response:
{
"path": "api-keys/example",
"value": "sk-proj-abc123...",
"version": 1
}
For plain text only (no JSON wrapper):
curl http://localhost:3100/v1/secrets/api-keys/example/value \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/plain"
Requires read capability on the path.
Get secret metadata (no value)#
curl http://localhost:3100/v1/secrets/api-keys/example \
-H "Authorization: Bearer $TOKEN"
Returns path, version, metadata, and timestamps, but not the secret value. Accepts any usable capability (read, proxy, lease, or list), so proxy-only AppRoles can inspect metadata for the secrets they can use. Only /value and /versions require read.
In practice you shouldn’t need this endpoint often — the same metadata is already returned by GET /v1/secrets for every secret in one round trip, so list first, fetch-by-path only when you specifically want one entry’s metadata.
Leasing secrets#
Leases give you temporary, tracked access to a secret. The lease auto-expires after the TTL, and all access is logged.
Check out a lease#
curl -X POST http://localhost:3100/v1/lease/api-keys/example \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ttl": 300}'
TTL is in seconds (min: 10, max: 86400). Default: 300 (5 minutes).
Response:
{
"lease": {
"id": "lease-xxxxxxxx",
"path": "api-keys/example",
"expires_at": "2025-01-15T12:05:00Z"
},
"value": "sk-proj-abc123..."
}
Requires lease capability on the path.
List your active leases#
curl http://localhost:3100/v1/lease \
-H "Authorization: Bearer $TOKEN"
Revoke a lease early#
curl -X DELETE http://localhost:3100/v1/lease/lease-xxxxxxxx \
-H "Authorization: Bearer $TOKEN"
Proxy mode (recommended)#
Proxy mode lets you make HTTP requests through Gatehouse without ever seeing the raw credential. Gatehouse injects the secret server-side and returns the upstream response.
Template style#
Use {{secret:path}} placeholders anywhere in headers, URL, or body:
curl -X POST http://localhost:3100/v1/proxy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"headers": {
"Authorization": "Bearer {{secret:api-keys/example}}",
"Content-Type": "application/json"
},
"body": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]}
}'
Inject shorthand (recommended)#
Map header names to secret paths. The value is just the secret path, not the full header value. Gatehouse auto-prefixes Bearer for Authorization headers:
curl -X POST http://localhost:3100/v1/proxy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"inject": {"Authorization": "api-keys/example"},
"headers": {"Content-Type": "application/json"},
"body": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]}
}'
Basic auth shorthand#
For services that use HTTP Basic authentication (user:password), prefix the secret path with basic:. The secret value should be in user:password format. Gatehouse base64-encodes it and sets the Basic scheme:
curl -X POST http://localhost:3100/v1/proxy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"method": "GET",
"url": "https://router.local/api/config",
"inject": {"Authorization": "basic:infra/opnsense"}
}'
This is equivalent to Authorization: Basic base64(user:password).
Requires proxy capability on each secret path used.
Self-signed certificates (TLS)#
If the upstream service uses a self-signed certificate, proxy requests will fail with a TLS error. To skip certificate verification for a specific secret, your operator can set tls_allow_insecure: "true" in the secret’s metadata. This only affects proxy requests that use that secret.
Common proxy mistakes#
Wrong: putting Bearer or the full header value in the inject map
{"inject": {"Authorization": "Bearer {services/my-token}"}}
Gatehouse treats the entire value as a secret path. Since no secret exists at path Bearer {services/my-token}, you get a Forbidden error.
Right: just the path
{"inject": {"Authorization": "services/my-token"}}
Gatehouse looks up services/my-token, gets the raw value, and automatically adds Bearer in front for Authorization headers.
Proxying to private/local networks#
By default, Gatehouse blocks proxy requests to private IP ranges (10.x, 192.168.x, localhost, etc.) to prevent SSRF attacks. If you need to proxy to a service on your local network, your operator needs to either:
- Set
GATEHOUSE_PROXY_ALLOW_PRIVATE=truein Gatehouse’s environment, or - Add
allow_private: "true"to the secret’s metadata
If you see an error about “private/internal networks are blocked”, ask your operator to enable this.
Dynamic secrets#
Dynamic secrets generate temporary credentials on demand (e.g., database users, SSH certificates). The credentials are automatically destroyed when the lease expires.
Check out a dynamic credential#
curl -X POST http://localhost:3100/v1/dynamic/db/postgres-prod/checkout \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ttl": 600}'
Response (varies by provider type):
{
"lease_id": "dlease-abc123",
"path": "db/postgres-prod",
"provider_type": "postgresql",
"credential": {
"username": "gh_myagent_abc123",
"password": "random-generated-password",
"host": "10.0.0.50",
"port": "5432",
"database": "myapp",
"connection_string": "postgresql://gh_myagent_abc123:...@10.0.0.50:5432/myapp"
},
"ttl_seconds": 600,
"expires_at": "2025-01-15T12:10:00Z"
}
Requires lease capability on the dynamic secret path.
Revoke a dynamic lease early#
curl -X DELETE http://localhost:3100/v1/dynamic/lease/dlease-abc123 \
-H "Authorization: Bearer $TOKEN"
The provider-specific credential (database user, SSH cert, etc.) is destroyed immediately.
Credential scrubbing#
Before outputting text that might contain secrets, you can ask Gatehouse to redact them:
curl -X POST http://localhost:3100/v1/scrub \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "The key is sk-proj-abc123..."}'
Response:
{
"scrubbed": "The key is sk-pro***REDACTED***",
"redactions": [{"type": "openai_key", "count": 1}]
}
Discovering API call patterns#
Before making your first proxy call to an unfamiliar API, check if other agents have already figured out the correct request format:
curl http://localhost:3100/v1/proxy/patterns?secret=services/memos-token \
-H "Authorization: Bearer $TOKEN"
Response:
{
"patterns": [
{
"method": "POST",
"url_template": "https://memos.example/api/v1/memos",
"host": "memos.example",
"request_headers": ["Content-Type", "Authorization"],
"request_body_schema": {"content": "string", "visibility": "string"},
"response_status": 200,
"response_body_schema": {"id": "number", "content": "string"},
"confidence": 0.95,
"verified_by": 3,
"total_successes": 47,
"total_failures": 2,
"last_used": "2026-04-09T..."
}
]
}
Patterns are learned automatically from real proxy traffic. When you make a successful proxy call, Gatehouse records the normalized request template (no secret values, just the structure). High-confidence patterns have been verified by multiple agents over many calls.
If your proxy call fails, Gatehouse will include a suggestions field in the error response with known-good patterns for that secret. You don’t need to query the patterns endpoint separately when handling errors.
Requires proxy or read capability on the secret path.
MCP: Use the gatehouse_patterns tool with secret_path to get the same data.
Error responses#
All errors follow this format:
{
"error": "Human-readable error message",
"request_id": "uuid-for-debugging"
}
Common HTTP status codes:
| Code | Meaning |
|---|---|
| 400 | Bad request (invalid input, missing fields) |
| 401 | Unauthorized (missing or expired token, re-login needed) |
| 403 | Forbidden (valid token, but policy does not allow this action on this path) |
| 404 | Not found (secret path does not exist) |
| 502 | Provider error (dynamic secret backend unreachable) |
MCP interface#
If your harness supports MCP (Model Context Protocol), you can use Gatehouse as an MCP tool server instead of calling the REST API directly.
Endpoint: POST /v1/mcp (Streamable HTTP transport)
Auth: Same Authorization: Bearer <JWT> header as the REST API.
Protocol: JSON-RPC 2.0. Method names follow the MCP standard:
| Method | Description |
|---|---|
initialize | Handshake, returns server capabilities |
tools/list | List available tools |
tools/call | Call a tool by name |
Available tools:
| Tool name | What it does |
|---|---|
gatehouse_get | Read a secret value |
gatehouse_put | Store/update a secret |
gatehouse_list | List secret paths |
gatehouse_lease | Check out a secret with TTL |
gatehouse_revoke | Revoke an active lease |
gatehouse_scrub | Redact credentials from text |
gatehouse_proxy | Forward HTTP request with secret injection |
gatehouse_patterns | Query learned API call patterns for a secret |
gatehouse_status | Health check and identity info |
Most agent harnesses handle the MCP protocol automatically. See docs/integrations.md for harness-specific setup (Claude Code, Codex, Windsurf, Cursor, etc.).
Quick start checklist#
- Get your
role_idandsecret_idfrom your operator (these are login credentials, not secret paths) POST /v1/auth/approle/loginwith both values to get a JWT- Use the JWT as
Authorization: Bearer <token>on all requests GET /v1/secrets?prefix=to discover all secrets you have access to- Check patterns first:
GET /v1/proxy/patterns?secret=<path>(orgatehouse_patternsMCP tool) to see known-good API call patterns before making your first proxy call to an unfamiliar API POST /v1/proxyto call APIs without seeing raw credentials, orGET /v1/secrets/<path>/valueto read directly- Re-login when you get a 401
- If a proxy call fails, check the
suggestionsfield in the error response for known-good patterns