Skip to content

API Reference

LatchGate exposes two socket interfaces with separate endpoint sets. In dev mode (TCP), all endpoints are merged onto a single listener.

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.


Served on listen_uds_path (default: /run/latchgate/gate.sock). In dev mode with unsafe_expose_http, served on the TCP listen_http_addr.

Liveness probe. Returns 200 when the server process is running. No authentication required.

Response: 200 OK

{ "status": "ok" }

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.

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).

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.

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 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.

Execute a protected action through the full enforcement pipeline. Requires lease-based DPoP authentication.

Headers:

Authorization: DPoP <lease_jwt>
DPoP: <proof>
Content-Type: application/json

The 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_successful is true only when the provider succeeded and verification confirmed the effect (or the action explicitly opted out via unverifiable_declared).
  • approval and learned_domain are optional fields, present only when the action came through the human-approval path (approval carries operator identity and approval_id) or when an approval was made with learn_domain (learned_domain echoes 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:

StatusCodeMeaning
401lease_expiredLease JWT has expired — reconnect
401invalid_leaseMalformed or unverifiable lease
401invalid_dpopDPoP proof verification failed
401replay_detectedDPoP jti already seen — replay attempt
401missing_auth_headerAuthorization or DPoP header missing
403policy_deniedOPA policy denied the action (includes sanitized deny_reason)
403action_not_registeredAction digest not in trust registry
403action_digest_mismatchProvider module digest does not match manifest
403budget_exhaustedCall budget depleted
403budget_session_not_foundBudget session ID not found in Redis
404action_not_foundAction ID not in registry
413Request body exceeds the transport-level size limit (1 MB)
422schema_violationRequest body fails JSON Schema validation
500evidence_persistence_failedProvider executed but receipt/audit write failed — check for unresolved execution intents in the ledger
500internal_errorUnrecoverable internal failure (no internal details exposed)
502action_execution_failedWASM provider returned an error
503drainingGate is in graceful drain mode — not accepting new requests
503policy_engine_unavailableOPA is unreachable
503policy_engine_timeoutOPA did not respond in time
503replay_cache_unavailableRedis is unreachable
503budget_store_unavailableRedis budget operation failed
503clock_errorSystem clock error

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).

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 .wasm provider module that executed.
  • normalized_result — tagged enum on kind: success, provider_failure, timeout, cancelled, internal_error. Carries variant fields (summary, reason) alongside.
  • verification_outcome — tagged enum on status: verified (with evidence), verification_failed (with reason), 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 of provider_error, timeout, policy_violation, verification_failed, internal_error, or null on full success.
  • receipt_signature — Ed25519 signature over the canonical result envelope, hex-encoded. Absent on receipts created before signing was enabled.
  • signing_key_idkid to 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).


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.

GET /healthz and GET /readyz are available on the admin socket with identical behavior to the client socket.

List approvals. Returns newest-first summaries.

Query parameters:

ParameterTypeDescription
statusstringFilter by state: pending, claimed, approved, denied, failed. Omit for all.
limitintegerMax results (default 50, capped at 200).

Response: 200 OK

{
"approvals": [
{
"approval_id": "apr_01J...",
"action_id": "github_create_issue",
"state": "pending",
"...": "..."
}
],
"count": 3
}

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.

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:

ParameterTypeDescription
learn_domainstringOptional. 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.

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"
}

Query the evidence ledger. Returns audit events ordered by timestamp descending (newest first).

Query parameters:

ParameterTypeDescription
trace_idstringFilter by trace ID
action_idstringFilter by action ID
principalstringFilter by principal name
session_idstringFilter by session ID
decisionstringFilter by decision: allow, deny, error, pending_approval
afterstringISO 8601 timestamp — events after this time
beforestringISO 8601 timestamp — events before this time
limitintegerMax results (default 100, capped at 1000)

Response: 200 OK

{
"events": [
{
"trace_id": "...",
"action_id": "http_fetch",
"decision": "allow",
"...": "..."
}
],
"count": 5
}

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.

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).

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.

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)

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
}

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 }

Return the current revocation epoch. Read-only diagnostic.

Response: 200 OK

{ "current_epoch": 1 }

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).

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"
}
]
}

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" }
StatusErrorCause
400invalid_domainDomain format validation failed
409domain_already_existsDomain already learned for this action

Remove a learned domain.

Auth: Operator (API key + DPoP proof).

Request:

{ "action_id": "web_read", "domain": "api.newservice.com" }

Response: 200 OK

{ "removed": true }

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 }

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"
}
]
}

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" }

Remove a learned filesystem path.

Auth: Operator (API key + DPoP proof).

Request:

{ "action_id": "fs_read", "path": "/data/reports" }

Response: 200 OK

{ "removed": true }

Remove all learned filesystem paths.

Auth: Operator (API key + DPoP proof).

Response: 200 OK

{ "cleared": true }

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"]
}
}
}

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": []
}
}
StatusErrorCause
404principal_not_foundNo ACL entry for this principal

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"]
}
StatusErrorCause
400unknown_actionOne or more action IDs not found in loaded manifests

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"]
}

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.

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" }
StatusErrorCause
400invalid_inputID empty, too long, or invalid characters
404action_not_foundAction ID not in loaded registry

The OPA evaluator is hot-reloaded after the write — the change takes effect immediately without a gate restart.

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" }
StatusErrorCause
400invalid_inputID empty, too long, or invalid characters
404allowlist_entry_not_foundNo matching entry for this action+agent pair

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.


Both authentication models (lease-based and operator) use DPoP (RFC 9449). The proof is a signed JWT with these claims:

ClaimValue
htmHTTP method (GET, POST)
htuFull URI ({public_base_url}/v1/actions/{action_id}/execute)
iatCurrent timestamp
jtiUnique identifier (UUID v7, single-use)
athSHA-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.

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.