Agent identity
Requests are attributed to agents cryptographically, HTTP Message Signatures (RFC 9421) with the web-bot-auth tag from Cloudflare's Web Bot Auth proposal, over Ed25519 keys.
There is no user-agent sniffing and no IP allow-listing. An agent that wants verified access signs each request with its private key; you resolve the matching public key by (agentId, keyId). If the signature verifies, the request gets a verified_agent principal; otherwise it is unknown and your policy decides what unknowns may do (typically challenge).
The wire profile
| Header | Purpose |
|---|---|
Signature-Input / Signature | RFC 9421 signature material: covered components, key id, creation/expiry timestamps, nonce, and the web-bot-auth tag. |
Signature-Agent | The agent's identity (e.g. "demo-agent.corri.dev"), per the Web Bot Auth drafts. Covered by the signature. |
Corri-Action | The declared action (e.g. read), bound into the signed material so it cannot be altered in flight. |
Corri-Purpose | The declared purpose (e.g. summarization, training). Policy rules match on it; it is signed too. |
Content-Digest | RFC 9530 sha-256 digest covering any request body, so the body cannot be swapped under a valid signature. |
Server configuration
import { createAgentAccess, memoryReplayStore } from "@corri/sdk/server";
const access = createAgentAccess({
issuer: "https://your-site.example",
identity: {
// JWK object, JWK JSON string, SPKI PEM, or CryptoKey, or null when unknown.
// May also return { key, revoked, operatorId, attributes }.
resolvePublicKey: async ({ agentId, keyId }) => keyDirectory.lookup(agentId, keyId),
replayStore: memoryReplayStore(),
maxClockSkewSeconds: 120, // default 120
maxRequestAgeSeconds: 300 // default 300
},
// …policy, entitlements, receipts
});
Verification enforces four things before a principal is trusted: the signature is valid for the resolved key, the covered components include the required material, the timestamps are inside the clock-skew and max-age windows, and the nonce has never been seen before.
Replay protection
Every signature carries a nonce; the replay store consumes it once. Entries expire with the signature they protect.
import { memoryReplayStore, redisReplayStore } from "@corri/sdk/server";
// Local development / single instance:
const store = memoryReplayStore();
// Production (multi-instance), works with ioredis and node-redis v4:
import Redis from "ioredis";
const shared = redisReplayStore(new Redis(process.env.REDIS_URL!), {
style: "ioredis" // or "node-redis"
});
The 401 challenge
When policy returns challenge (usually because the principal is unknown), the SDK answers with a 401 that tells the agent exactly how to authenticate:
{
"error": "agent_identity_required",
"effect": "challenge",
"challenge": {
"scheme": "http-message-signatures",
"tag": "web-bot-auth"
}
}
Failure reasons
Identity verification failures are precise (the IdentityFailureReason type): signature_missing, signature_malformed, signature_invalid, signature_expired, signature_future_dated, signature_replayed, unknown_key, revoked_key, wrong_tag, content_digest_mismatch, component_mismatch.
Signing requests yourself
createAgentClient handles signing for you, but the primitive is exported for custom stacks:
import { signRequest } from "@corri/sdk/client";
const { headers } = await signRequest({
agentId: "reader.example-agent.com",
keyId: "agent-2026",
privateKey: AGENT_PRIVATE_JWK,
method: "GET",
url: "https://your-site.example/api/report",
action: "read",
purpose: "summarization"
});
const res = await fetch("https://your-site.example/api/report", { headers });
RequestIdentityAdapter interface. Pass identity: { adapter } to createAgentAccess to plug in a different identity system without touching policy code.