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

HeaderPurpose
Signature-Input / SignatureRFC 9421 signature material: covered components, key id, creation/expiry timestamps, nonce, and the web-bot-auth tag.
Signature-AgentThe agent's identity (e.g. "demo-agent.corri.dev"), per the Web Bot Auth drafts. Covered by the signature.
Corri-ActionThe declared action (e.g. read), bound into the signed material so it cannot be altered in flight.
Corri-PurposeThe declared purpose (e.g. summarization, training). Policy rules match on it; it is signed too.
Content-DigestRFC 9530 sha-256 digest covering any request body, so the body cannot be swapped under a valid signature.

Server configuration

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

ts
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:

json
{
  "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:

ts
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 });
Custom schemes: signature specifics live behind the RequestIdentityAdapter interface. Pass identity: { adapter } to createAgentAccess to plug in a different identity system without touching policy code.