Documentation
Reference for operators connecting their own agents to a AgentWarden-protected target system, or implementing ECAP independently of it.
Overview
AgentWarden places a zero-trust handshake in front of a target system, following the ECAP protocol (Ethical Crawler Agreement Protocol). Before an agent may read or write, it presents a short-lived, ES256-signed token that cryptographically binds operator identity, target system, and permitted scope. The agent-facing check path always responds with HTTP 200, regardless of whether access is granted or denied: the actual decision lives exclusively in response headers, so a rejected agent cannot abuse an error code for detection diagnostics.
ECAP itself is an open standard, see the ECAP Standard page. This page documents the production-grade reference implementation that runs inside AgentWarden.
Quickstart
The following steps register an operator, issue a token, and check it against the ECAP handshake service (ecap-authz, default port 9001).
- Register an operator
Through the issuer admin API (default port 9000), authenticated with the instance's admin token. AgentWarden generates a fresh ECDSA P-256 key pair and manages the private key itself.
bashcurl -X POST http://localhost:9000/admin/operators/managed \ -H "Authorization: Bearer $ECAP_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "iss": "operator:my-agent", "display_name": "My Agent", "scopes_allowed": ["read"] }'The response contains an
api_keythis operator uses to request its own tokens going forward. It is issued only once. - Request an ECAP token
Using the operator API key you just received, not the admin token.
bashcurl -X POST http://localhost:9000/ecap/v1/tokens \ -H "Authorization: Bearer $OPERATOR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sub": "agent:instance-01", "aud": "target-system:example-app", "scope": ["read"] }'json{ "ecap_token": "eyJhbGciOiJFUzI1NiIs...", "key_id": "kid:2026-03-ecdsa-p256-a1b2c3", "expires_at": 1780000300 } - Perform the handshake
The agent makes its actual request to the target system, routed through the ECAP handshake service. Three headers are mandatory.
bashcurl -i http://localhost:9001/any/path \ -H "ecap-token: $ECAP_TOKEN" \ -H "x-ecap-audience: target-system:example-app" \ -H "x-ecap-required-scope: read"The response is always
HTTP 200. On success it carriesx-ecap-decision: allowplusx-ecap-identityandx-ecap-operator. In production, Envoy uses this header to route to the real backend or to the deception layer.
Handshake flow
The check path evaluates in this order:
- Are
x-ecap-audienceandx-ecap-required-scopeset? Otherwiseroute_not_configured. - Is
ecap-tokenpresent and decodable as a JWT? - Is the key ID on the revocation list?
- Is the operator (issuer) registered and active?
- Is the signature valid, the token not expired, audience and scope correct (ES256 against the operator's stored public key)?
- Does the policy engine allow this combination of operator, scope, and identity, and is the identity not flagged in the Trust Registry?
- Can the access be recorded in the tamper-evident audit log?
If any step fails, the response is still 200 with x-ecap-decision: tarpit. The reason lives exclusively in x-ecap-tarpit-reason, see the table below.
Token format
ECAP tokens are JWTs signed with ES256 (ECDSA over curve P-256). The maximum lifetime is capped at 300 seconds at the protocol level, regardless of what is requested.
| Claim | Type | Meaning |
|---|---|---|
iss | string | Operator identity |
sub | string | Agent instance ID |
aud | string | Target system the token is valid for |
iat / exp | number | Issued / expires, Unix timestamp, max. 300s apart |
scope | string[] | Permitted actions, a subset of the operator's grant |
key_id | string | Operator's key ID, identical to the JWT header kid |
purpose_note | string | null | Optional, human-readable purpose |
Tarpit reasons
Values of x-ecap-tarpit-reason when x-ecap-decision equals tarpit. Stable, publicly documented identifiers.
| Value | Trigger |
|---|---|
route_not_configured | Audience/scope headers were not set by the gateway configuration |
missing_token | The ecap-token header is missing |
malformed_token | Token cannot even be decoded as an unverified JWT |
revoked_key | key_id is on the revocation list |
unknown_issuer | The iss claim is not registered |
suspended_issuer | Operator registered, but not active |
token_expired | The exp claim is in the past |
token_signature_invalid | Signature does not match the operator's stored public key |
token_malformed | Claims missing, wrong type, or a disallowed algorithm (e.g. alg: none) |
token_scope_denied | The requested scope is not contained in the token |
token_audience_mismatch | The aud claim does not match the requested route |
guardrails_unreachable | The policy engine is not responding |
guardrails_denied | Policy denies it: scope not allowed, or identity flagged |
audit_unavailable | Audit service unreachable, see zero-trust principles below |
internal_error | Any unexpected error, caught as a catch-all instead of leaking through |
Issuer endpoints
Operator-facing endpoints of the issuer service (default port 9000).
POST /ecap/v1/tokens
Request a token. Authenticated with the operator API key.
GET /.well-known/ecap-keys/{iss}
An active operator's public key as a JWK, for external verifiers that want to check tokens independently of AgentWarden.
POST /ecap/v1/revoke
Revoke your own key ID immediately. Applies to all still-valid tokens with this key_id, regardless of their exp.
Admin API
Operator management, Trust Registry, runtime configuration, and honeypots run over admin-protected REST endpoints on all four services, secured by a shared admin token with brute-force protection (lockout after repeated failed attempts, 429 with Retry-After). For day-to-day operation, that is the Operator Console, not direct API calls.
Zero-trust principles
Always HTTP 200 on the agent path. A rejected agent gets no error code it could use to diagnose its own detection. The decision lives exclusively in response headers.
Fail closed, not error leak. If the audit service is unreachable while recording a granted access, the access is not let through, but the response stays a normal-looking tarpit with reason audit_unavailable, never a 5xx error code.
Tamper-evident chain of evidence. Every audit entry is chained to its predecessor and signed via SHA-256. A single altered byte afterward will provably break the chain at the next verification.
Glossary
- ECAP (Ethical Crawler Agreement Protocol)
- An open, cryptographic zero-trust handshake protocol that requires an AI agent to present a signed, short-lived token before reading or writing to a target system.
- Zero-trust handshake
- A verification step that grants no access by default: identity, scope, and permission are checked cryptographically on every request, not assumed from a prior session.
- Tarpit
- A deception response that looks like a normal, successful interaction (HTTP 200) but silently redirects a rejected or suspicious agent into an isolated synthetic environment instead of the real system.
- Behavioral Intent Engine
- The AgentWarden layer that scores agent behavior over time, write frequency, syntax, and cross-identity coordination, rather than judging a single request in isolation.
- Deception Infrastructure
- The AgentWarden layer that redirects suspicious traffic into a synthetic copy of the target system, capturing forensic data while never exposing real data.
- Semantic Guardrails
- A deterministic policy layer that checks content and origin against rules formally verified for consistency before they go live.
- Trust Registry
- A registry of flagged agent identities, isolated per customer today; a federated, cross-organization version is on the roadmap, not yet shipped.
Sources & evidence
The ECAP core is openly archived, independent of AgentWarden as a company.