API Reference
LatchGate exposes two socket interfaces with separate endpoint sets. In dev mode (TCP), all endpoints are merged onto a single listener.
Authentication models
Section titled “Authentication models”Lease-based DPoP (client socket) — the caller presents a Lease JWT in Authorization: DPoP <lease> and a DPoP proof in the DPoP header. The proof is bound to the HTTP method and URI. Used by agent processes via the SDKs.
Operator auth (admin socket) — the caller presents an operator API key with DPoP proof (Authorization: DPoP <key> + DPoP: <proof>). Used by the CLI and operator tooling.
Unauthenticated — health probes and action discovery do not require authentication.
Client socket endpoints
Section titled “Client socket endpoints”Served on listen_uds_path (default: /run/latchgate/gate.sock). In dev mode with unsafe_expose_http, served on the TCP listen_http_addr.
Health
Section titled “Health”GET /healthz
Section titled “GET /healthz”Liveness probe. Returns 200 when the server process is running. No authentication required.
Response: 200 OK
{ "status": "ok" }GET /readyz
Section titled “GET /readyz”Readiness probe. Checks that all dependencies are operational. No authentication required.
Response: 200 OK when ready or degraded, 503 Service Unavailable when not ready.
{ "status": "ready", "redis": true, "opa": true, "ledger": true, "approval_store": true, "egress_proxy": null, "actions_registered": 21}Status values: ready (all checks pass), degraded (core pipeline works but egress proxy is down — returns 200, keeps routing), not_ready (core dependency failure — returns 503). egress_proxy is null when no proxy is configured, true/false when configured.
Leases
Section titled “Leases”POST /v1/leases
Section titled “POST /v1/leases”Issue a Lease JWT bound to the caller’s DPoP public key. This is the authentication bootstrapping endpoint — it is intentionally unauthenticated. Access control relies on transport isolation (UDS), DPoP key binding, short TTL, and rate limiting (50 req/s).
Returns 503 with {"error": "draining"} when the gate is in drain mode.
Request body:
{ "scopes": ["tools:call"], "dpop_jwk": { "kty": "EC", "crv": "P-256", "x": "<base64url>", "y": "<base64url>" }, "budgets": { "max_calls": 100 }}scopes is required. dpop_jwk must be an EC P-256 public key. budgets is optional — when present, the Gate initialises stateful budget counters in Redis.
Response: 200 OK
{ "lease_jwt": "<JWT>", "session_id": "ses_01J...", "lease_jti": "lea_01J...", "expires_at": "2025-01-01T00:05:00Z"}Errors: 400 (invalid request), 403 (identity denied), 429 rate_limit_exceeded, 503 (identity provider unavailable or draining).
GET /.well-known/jwks.json
Section titled “GET /.well-known/jwks.json”Returns the Gate’s JWKS (JSON Web Key Set) for lease JWT verification. Unauthenticated. Useful for external systems that need to verify lease signatures independently.
Response: 200 OK — standard JWKS format.
Actions
Section titled “Actions”GET /v1/actions
Section titled “GET /v1/actions”List all registered actions. Returns summaries (action ID, version, risk level, description). No digests, schemas, or secrets. Unauthenticated — discovery is not a side effect.
Response: 200 OK
[ { "action_id": "http_fetch", "version": "1.0.0", "risk_level": "low", "description": "...", "database_mode": null }]The database_mode field is reserved for the planned in the next releases database provider (where it carries values like "hybrid"). In v0.1 responses it is always null. SDKs and the MCP adapter use it to signal controlled SQL support once the provider ships.
GET /v1/actions/{action_id}
Section titled “GET /v1/actions/{action_id}”Get full manifest details for a single action. Returns provider module reference, required imports, resource limits, schemas, egress profile, secrets declarations, and risk level. Unauthenticated.
Response: 200 OK — full manifest as JSON. 404 if the action is not registered.
GET /v1/actions/{action_id}/schema/request
Section titled “GET /v1/actions/{action_id}/schema/request”Return the request JSON Schema declared in the action manifest. Used by the MCP adapter to populate inputSchema in tools/list responses. Unauthenticated.
Response: 200 OK — JSON Schema object. 404 if the action is unknown or has no declared schema.
POST /v1/actions/{action_id}/execute
Section titled “POST /v1/actions/{action_id}/execute”Execute a protected action through the full enforcement pipeline. Requires lease-based DPoP authentication.
Headers:
Authorization: DPoP <lease_jwt>DPoP: <proof>Content-Type: application/jsonThe DPoP proof must be bound to POST and the full URI ({public_base_url}/v1/actions/{action_id}/execute).
Request body: JSON matching the action’s declared request schema.
{ "url": "https://httpbin.org/get" }Response: 200 OK
{ "trace_id": "trc_01J...", "action_id": "http_fetch", "grant_id": "grant_01J...", "receipt_id": "rcpt_01J...", "output": { "status": 200, "body": "..." }, "verification_outcome": "verified", "verification": { "outcome": "verified", "is_fully_successful": true }, "runtime": { "duration_ms": 142, "exit_code": 0, "fuel_consumed": 12345 }}Field notes:
verification_outcome(top-level string) is a flat string for SIEM/dashboard convenience. Possible values:verified,verification_failed,unverifiable_declared,provider_failed_before_verification,skipped.verification.is_fully_successfulistrueonly when the provider succeeded and verification confirmed the effect (or the action explicitly opted out viaunverifiable_declared).approvalandlearned_domainare optional fields, present only when the action came through the human-approval path (approvalcarries operator identity andapproval_id) or when an approval was made withlearn_domain(learned_domainechoes the domain that was persisted to the action’s allowlist).
Response: 202 Accepted — action requires human approval.
{ "decision": "pending_approval", "approval_id": "apr_01J...", "request_hash": "sha256:...", "trace_id": "trace_01J..."}Errors:
| Status | Code | Meaning |
|---|---|---|
| 401 | lease_expired | Lease JWT has expired — reconnect |
| 401 | invalid_lease | Malformed or unverifiable lease |
| 401 | invalid_dpop | DPoP proof verification failed |
| 401 | replay_detected | DPoP jti already seen — replay attempt |
| 401 | missing_auth_header | Authorization or DPoP header missing |
| 403 | policy_denied | OPA policy denied the action (includes sanitized deny_reason) |
| 403 | action_not_registered | Action digest not in trust registry |
| 403 | action_digest_mismatch | Provider module digest does not match manifest |
| 403 | budget_exhausted | Call budget depleted |
| 403 | budget_session_not_found | Budget session ID not found in Redis |
| 404 | action_not_found | Action ID not in registry |
| 413 | — | Request body exceeds the transport-level size limit (1 MB) |
| 422 | schema_violation | Request body fails JSON Schema validation |
| 500 | evidence_persistence_failed | Provider executed but receipt/audit write failed — check for unresolved execution intents in the ledger |
| 500 | internal_error | Unrecoverable internal failure (no internal details exposed) |
| 502 | action_execution_failed | WASM provider returned an error |
| 503 | draining | Gate is in graceful drain mode — not accepting new requests |
| 503 | policy_engine_unavailable | OPA is unreachable |
| 503 | policy_engine_timeout | OPA did not respond in time |
| 503 | replay_cache_unavailable | Redis is unreachable |
| 503 | budget_store_unavailable | Redis budget operation failed |
| 503 | clock_error | System clock error |
Approval polling
Section titled “Approval polling”GET /v1/approvals/{approval_id}/poll
Section titled “GET /v1/approvals/{approval_id}/poll”Lightweight agent-accessible endpoint for polling the status of a pending approval. Requires lease-based DPoP authentication. The agent can only poll approvals that belong to its own session — the endpoint verifies the session_id from the lease matches the approval’s session.
Used by the SDKs (get_approval_status / getApprovalStatus) after catching LatchGateApprovalRequired.
Response: 200 OK
{ "approval_id": "apr_01J...", "state": "pending"}State values: pending, claimed, approved, denied, expired.
Errors: 401 (auth failure), 403 (session mismatch — agent cannot poll another session’s approvals), 404 (approval not found or expired).
Receipts
Section titled “Receipts”GET /v1/receipts/{receipt_id}
Section titled “GET /v1/receipts/{receipt_id}”Retrieve a stored execution receipt. Requires lease-based DPoP authentication on the client socket (or operator auth on the admin socket). The DPoP proof must be bound to GET and the full URI including the receipt ID.
Receipt IDs are UUID v7 (unguessable). Any caller with a valid lease can retrieve any receipt by ID — the caller already knows the receipt_id from the execute() response.
Response: 200 OK
{ "receipt_id": "rcpt_01J...", "grant_id": "grant_01J...", "provider_module_digest": "sha256:...", "provider_receipt": { "status": 200, "body": "..." }, "normalized_result": { "kind": "success", "summary": "HTTP 200 OK" }, "verification_outcome": { "status": "verified", "evidence": { "status_code": 200 } }, "effect_evidence": [], "result_hash": "sha256:...", "receipt_signature": "<hex>", "signing_key_id": "key-2025-01-abc123", "started_at": "2025-01-01T00:00:00Z", "finished_at": "2025-01-01T00:00:01Z", "failure_class": null, "signature_status": "verified"}Fields:
provider_module_digest— SHA-256 digest of the.wasmprovider module that executed.normalized_result— tagged enum onkind:success,provider_failure,timeout,cancelled,internal_error. Carries variant fields (summary,reason) alongside.verification_outcome— tagged enum onstatus:verified(withevidence),verification_failed(withreason),unverifiable_declared,provider_failed_before_verification,skipped.effect_evidence— array of structured evidence captured by verifiers (e.g. message IDs, rows-affected counts). May be empty.failure_class— one ofprovider_error,timeout,policy_violation,verification_failed,internal_error, ornullon full success.receipt_signature— Ed25519 signature over the canonical result envelope, hex-encoded. Absent on receipts created before signing was enabled.signing_key_id—kidto look up the verifying key in/v1/receipt-keys(JWKS) for post-rotation verification.signature_status— added by the API handler. Values:verified,unsigned,unknown_kid,signature_invalid. The receipt body itself is unchanged from the ledger; this field reports the current verification result against the active JWKS.
The SDK exposes a derived is_fully_successful property — equivalent to checking that normalized_result.kind == "success" and verification_outcome.status is one of verified or unverifiable_declared. This is a method on the SDK side, not a field in the JSON.
Errors: 401 (auth failure), 404 (receipt not found), 503 (infra failure).
Admin socket endpoints
Section titled “Admin socket endpoints”Served on listen_admin_uds_path (default: /run/latchgate/gate-admin.sock). All endpoints require operator authentication.
Rate limits on operator endpoints: 20 req/s on write endpoints (approve, deny, drain, revoke, domain/path/policy mutations), 100 req/s on read endpoints (list, show, audit, domain/path/policy queries). Both are token buckets shared per process — see Configuration.
Health
Section titled “Health”GET /healthz and GET /readyz are available on the admin socket with identical behavior to the client socket.
Approvals
Section titled “Approvals”GET /v1/approvals
Section titled “GET /v1/approvals”List approvals. Returns newest-first summaries.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by state: pending, claimed, approved, denied, failed. Omit for all. |
limit | integer | Max results (default 50, capped at 200). |
Response: 200 OK
{ "approvals": [ { "approval_id": "apr_01J...", "action_id": "github_create_issue", "state": "pending", "...": "..." } ], "count": 3}GET /v1/approvals/{approval_id}
Section titled “GET /v1/approvals/{approval_id}”Get full details of an approval for operator review. Returns lifecycle metadata alongside the immutable execution plan fields needed for a safe approval decision: action version, risk level, approved targets, secret names (never values), egress profile, budget snapshot, verifier kind, provider module digest, plan_hash, and expiry.
Response: 200 OK — full approval record with plan review fields. 404 if not found or expired.
POST /v1/approvals/{approval_id}/approve
Section titled “POST /v1/approvals/{approval_id}/approve”Approve a pending action and execute it through the hardened kernel path. The stored execution plan (targets, secrets, egress, budgets) is used — never re-derived from the live manifest. Operator identity (approved_by) and DPoP JWK thumbprint (operator_binding) are bound into the grant signature.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
learn_domain | string | Optional. Domain to learn for future use by this action’s egress allowlist. Persisted only on successful execution. |
Response: 200 OK — same response body as POST /v1/actions/{action_id}/execute (with approval and, when learn_domain was set, learned_domain populated). 404 if not found, already consumed, or expired.
POST /v1/approvals/{approval_id}/deny
Section titled “POST /v1/approvals/{approval_id}/deny”Deny a pending action. The action is not executed. The record is retained for forensics.
Request body (optional):
{ "reason": "Not authorized for production database" }Response: 200 OK
{ "decision": "deny", "trace_id": "trc_01JA...", "action_id": "github_create_issue", "approval_id": "apr_01J...", "denied_by": "alice", "deny_reason": "Not authorized for production database"}GET /v1/audit/events
Section titled “GET /v1/audit/events”Query the evidence ledger. Returns audit events ordered by timestamp descending (newest first).
Query parameters:
| Parameter | Type | Description |
|---|---|---|
trace_id | string | Filter by trace ID |
action_id | string | Filter by action ID |
principal | string | Filter by principal name |
session_id | string | Filter by session ID |
decision | string | Filter by decision: allow, deny, error, pending_approval |
after | string | ISO 8601 timestamp — events after this time |
before | string | ISO 8601 timestamp — events before this time |
limit | integer | Max results (default 100, capped at 1000) |
Response: 200 OK
{ "events": [ { "trace_id": "...", "action_id": "http_fetch", "decision": "allow", "...": "..." } ], "count": 5}GET /v1/audit/verify
Section titled “GET /v1/audit/verify”Verify the integrity of the ledger’s tamper-evident hash-chain. Walks every event in insertion order and checks that each prev_hash matches the SHA-256 of the preceding event’s JSON. Backs the latchgate verify CLI command.
Response: 200 OK
{ "intact": true, "events_checked": 1024, "broken_at": null}When the chain is broken, intact is false and broken_at identifies the first event whose hash does not match.
Receipts
Section titled “Receipts”GET /v1/receipts/{receipt_id} (admin auth)
Section titled “GET /v1/receipts/{receipt_id} (admin auth)”Same endpoint as on the client socket, but requires operator authentication instead of lease-based auth. Response body is identical. Used by operator tooling and the CLI (latchgate audit).
Metrics
Section titled “Metrics”GET /metrics
Section titled “GET /metrics”Prometheus text exposition format. Requires operator authentication to prevent information disclosure.
Includes: action call counters by ID and outcome, DPoP rejection counters, policy decision counters, provider execution duration histograms, budget exhaustion counters, audit write error counter, response schema violation counters, OPA request duration histogram (by operation), Redis request duration histogram (by operation), unresolved intents gauge, oldest pending approval age gauge, webhook outbox pending gauge, ledger write duration histogram, readyz degraded counters by reason, webhook drop counter.
Response: 200 OK with Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8.
Admin controls
Section titled “Admin controls”GET /v1/admin/status
Section titled “GET /v1/admin/status”Operational status snapshot. Returns version, uptime, dependency health, pending approval count, revocation epoch, drain state, in-flight execution count, unresolved intent count, and webhook dispatcher status. Designed for frequent polling by orchestrators and monitoring dashboards.
Not audited — unlike revoke-all and receipt-keys, this endpoint is called every 30 seconds by monitoring tools. Auditing it would flood the evidence ledger.
Response: 200 OK
{ "status": "ok", "version": "0.1.0", "uptime_seconds": 3600, "actions_registered": 21, "pending_approvals": 2, "revocation_epoch": 0, "draining": false, "in_flight_executions": 0, "dependencies": { "redis": true, "opa": true }, "webhooks": { "active": true, "pending_deliveries": 0 }}status values:
"ok"— all dependencies healthy, not draining"degraded"— Redis or OPA is unreachable"draining"— graceful drain is active (new requests rejected, in-flight completing)
POST /v1/admin/drain
Section titled “POST /v1/admin/drain”Initiate graceful drain. After this call, new action calls and lease requests are rejected with 503 draining. In-flight WASM executions complete normally. Admin endpoints (status, drain, approvals) remain operational.
The drain is irreversible within the process lifetime. Callers should poll GET /v1/admin/status and wait for in_flight_executions: 0 before sending SIGTERM.
Idempotent: calling drain on an already-draining gate returns 200 with already_draining: true.
Operator identity is recorded in the tamper-evident audit ledger (AdminDrain event type).
Response: 200 OK
{ "draining": true, "already_draining": false, "in_flight_executions": 3}POST /v1/admin/revoke-all
Section titled “POST /v1/admin/revoke-all”Emergency kill-switch. Advances the revocation epoch — all outstanding leases and grants from prior epochs are immediately invalidated. New leases carry the new epoch. Operator identity is logged for forensics.
Response: 200 OK
{ "previous_epoch": 0, "current_epoch": 1 }GET /v1/admin/epoch
Section titled “GET /v1/admin/epoch”Return the current revocation epoch. Read-only diagnostic.
Response: 200 OK
{ "current_epoch": 1 }POST /v1/admin/reload
Section titled “POST /v1/admin/reload”Hot-reload manifests and policy data without restarting the gate. Re-reads the manifests directory and policy files, atomically swapping the active registry and policy engine. If any step fails, the previous configuration remains active — no partial reload.
Operator identity is recorded in the tamper-evident audit ledger (AdminReload event type) since the reload changes the enforcement surface.
Response: 200 OK
{ "manifests_loaded": 21, "policy_reloaded": true, "reloaded_at": "2025-06-01T12:00:00Z"}Errors: 500 reload_failed (manifest parse error, policy compilation error — previous config remains active).
Learned domains
Section titled “Learned domains”GET /v1/admin/domains
Section titled “GET /v1/admin/domains”List all learned domains. Learned domains are added through the approval flow or via latchgate domains add and are included in the effective egress allowlist.
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "domains": [ { "domain": "api.newservice.com", "action_id": "web_read", "source": "approval", "added_at": "2025-03-28T14:30:00Z" }, { "domain": "internal.example.com", "action_id": "http_fetch", "source": "cli", "added_at": "2025-03-29T10:00:00Z" } ]}POST /v1/admin/domains
Section titled “POST /v1/admin/domains”Add a learned domain for a specific action. The domain is included in the effective egress allowlist and, in live-sync mode, written to the Squid allowlist file.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "web_read", "domain": "api.newservice.com" }Response: 200 OK
{ "domain": "api.newservice.com", "action_id": "web_read" }| Status | Error | Cause |
|---|---|---|
| 400 | invalid_domain | Domain format validation failed |
| 409 | domain_already_exists | Domain already learned for this action |
DELETE /v1/admin/domains
Section titled “DELETE /v1/admin/domains”Remove a learned domain.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "web_read", "domain": "api.newservice.com" }Response: 200 OK
{ "removed": true }DELETE /v1/admin/domains/clear
Section titled “DELETE /v1/admin/domains/clear”Remove all learned domains. In live-sync mode, the Squid allowlist is rewritten to contain only manifest-declared domains.
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "cleared": true }Learned paths
Section titled “Learned paths”GET /v1/admin/paths
Section titled “GET /v1/admin/paths”List all learned filesystem paths (for use with the filesystem provider).
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "paths": [ { "path": "/data/reports", "action_id": "fs_read", "source": "cli", "added_at": "2025-03-28T14:30:00Z" } ]}POST /v1/admin/paths
Section titled “POST /v1/admin/paths”Add a learned filesystem path for a specific action.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "fs_read", "path": "/data/reports" }Response: 200 OK
{ "path": "/data/reports", "action_id": "fs_read" }DELETE /v1/admin/paths
Section titled “DELETE /v1/admin/paths”Remove a learned filesystem path.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "fs_read", "path": "/data/reports" }Response: 200 OK
{ "removed": true }DELETE /v1/admin/paths/clear
Section titled “DELETE /v1/admin/paths/clear”Remove all learned filesystem paths.
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "cleared": true }Policy management
Section titled “Policy management”GET /v1/admin/policy
Section titled “GET /v1/admin/policy”Return the full policy ACL — all principals with their granted actions and auto-derived sinks.
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "policy_version": "strict-init-2", "principals": { "agent-support": { "actions": ["http_fetch", "github_read", "slack_post"], "sinks": ["http_read", "http_write"] }, "agent-ops": { "actions": ["http_fetch", "http_post", "http_delete"], "sinks": ["http_read", "http_write"] } }}GET /v1/admin/policy/{principal}
Section titled “GET /v1/admin/policy/{principal}”Return the ACL for a single principal, including a risk breakdown of their granted actions.
Auth: Operator (API key + DPoP proof).
Response: 200 OK
{ "principal": "agent-support", "actions": ["http_fetch", "github_read", "slack_post"], "sinks": ["http_read", "http_write"], "risk_breakdown": { "low": ["http_fetch", "github_read"], "medium": ["slack_post"], "high": [], "critical": [] }}| Status | Error | Cause |
|---|---|---|
| 404 | principal_not_found | No ACL entry for this principal |
POST /v1/admin/policy/grant
Section titled “POST /v1/admin/policy/grant”Grant actions to a principal. Sinks are auto-derived from manifest declared_side_effects — they cannot be set manually. Action IDs are validated against loaded manifests before writing.
Auth: Operator (API key + DPoP proof).
Request:
{ "principal": "agent-support", "actions": ["http_fetch", "github_read", "slack_post"]}Response: 200 OK
{ "principal": "agent-support", "actions": ["http_fetch", "github_read", "slack_post"], "sinks": ["http_read", "http_write"]}| Status | Error | Cause |
|---|---|---|
| 400 | unknown_action | One or more action IDs not found in loaded manifests |
POST /v1/admin/policy/revoke
Section titled “POST /v1/admin/policy/revoke”Revoke actions from a principal. Sinks are recomputed from the remaining actions. If all actions are revoked, the principal entry is removed.
Auth: Operator (API key + DPoP proof).
Request:
{ "principal": "agent-support", "actions": ["slack_post"] }Response: 200 OK
{ "principal": "agent-support", "actions": ["http_fetch", "github_read"], "sinks": ["http_read"]}Approval allowlist
Section titled “Approval allowlist”The allowlist manages per-(action, principal) entries that bypass the approval hold (step 5b in the enforcement pipeline). All other deny rules — trust, ACL, scope, budget, sink — still apply unconditionally. Mutations are validated against the registry (unknown action IDs are rejected) and recorded in the tamper-evident audit ledger.
POST /v1/admin/policy/allowlist
Section titled “POST /v1/admin/policy/allowlist”Add an allowlist entry. The matched (action, principal) pair bypasses approval on future calls. The action ID must exist in the loaded registry — this prevents typos from creating silent policy holes.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "http_fetch", "agent_id": "agent-ops" }Response: 200 OK
{ "ok": true, "action_id": "http_fetch", "agent_id": "agent-ops" }| Status | Error | Cause |
|---|---|---|
| 400 | invalid_input | ID empty, too long, or invalid characters |
| 404 | action_not_found | Action ID not in loaded registry |
The OPA evaluator is hot-reloaded after the write — the change takes effect immediately without a gate restart.
DELETE /v1/admin/policy/allowlist
Section titled “DELETE /v1/admin/policy/allowlist”Remove an allowlist entry. If the action’s allowlist map becomes empty, the action key is cleaned up automatically.
Auth: Operator (API key + DPoP proof).
Request:
{ "action_id": "http_fetch", "agent_id": "agent-ops" }Response: 200 OK
{ "ok": true, "action_id": "http_fetch", "agent_id": "agent-ops" }| Status | Error | Cause |
|---|---|---|
| 400 | invalid_input | ID empty, too long, or invalid characters |
| 404 | allowlist_entry_not_found | No matching entry for this action+agent pair |
GET /v1/receipt-keys
Section titled “GET /v1/receipt-keys”Return all receipt signing public keys (current + historical) in JWKS format. Allows external verifiers to validate receipt signatures after key rotation without access to the signing key. Look up by signing_key_id (kid) from the receipt.
Response: 200 OK — standard JWKS format with all historical verifying keys.
DPoP proof construction
Section titled “DPoP proof construction”Both authentication models (lease-based and operator) use DPoP (RFC 9449). The proof is a signed JWT with these claims:
| Claim | Value |
|---|---|
htm | HTTP method (GET, POST) |
htu | Full URI ({public_base_url}/v1/actions/{action_id}/execute) |
iat | Current timestamp |
jti | Unique identifier (UUID v7, single-use) |
ath | SHA-256 hash of the access token (base64url-encoded) |
The htu is validated server-side against public_base_url from config — never derived from the Host header. The jti is checked against Redis for replay protection (fail-closed if Redis is unavailable).
The SDKs construct DPoP proofs automatically. For manual integration, see the SDK source code.
Error response format
Section titled “Error response format”All error responses follow a consistent JSON structure:
{ "error": "error_code" }For 403 policy_denied, the response includes an additional sanitized deny_reason field:
{ "error": "policy_denied", "deny_reason": "action not in ACL for principal 'agent-1'"}Control characters are stripped from deny_reason to prevent log injection. Reason text is truncated to 500 characters.
For 5xx errors, only the error code is included — no internal details are exposed. Error specifics are logged server-side with the trace_id.
For the full execution pipeline, see Core Concepts. For SDK usage, see SDKs. For operator CLI commands, see CLI Reference.