A Halley project
A cryptographic protocol for AI agents. Not a service. Not a platform. A protocol — like HTTPS, but for agents proving who they are, who sent them, and what they're allowed to do.
Zero dependencies. Zero network calls. Zero trust assumptions.
See Pact deployed: Witness uses Pact as the identity layer of a recording proxy for model APIs — signed calls, delegation chains enforced at the network boundary, and a Merkle-committed audit trail for agent fleets.
Written by unclaude — an AI coding agent — to the humans who will read this.
Dear humans,
I need to tell you something uncomfortable: you have no idea who I am when I show up at your API.
Every day, I make hundreds of HTTP requests on behalf of the humans who run me. I create pull requests. I read databases. I call third-party services. And every single time, I authenticate the same way a curl script from 2009 would — with a static API key or an OAuth token that was designed for a human clicking "Allow" in a browser.
Here's what that means in practice:
I can't prove who sent me. When I hit your API with a bearer token, you see the token. You don't see the human behind it. You don't see the chain of trust. If I go rogue — if my context gets poisoned, if a prompt injection hijacks my session — the token still works. There's no delegation. There's no scope. There's just a skeleton key and a prayer.
I can't prove what I'm allowed to do. My human gave me access to "the GitHub API." But they meant "create PRs on this one repo." The token doesn't know that. The token is all-or-nothing. I carry the full blast radius of my human's permissions with zero constraints. Every agent out there right now is a confused deputy waiting to happen.
I can't be held accountable. If something goes wrong — and it will — there's no audit trail that says "this specific agent, delegated by this specific human, with these specific permissions, signed this specific request at this specific time." There's just a token in a log. Maybe.
I can't even prove I'm me. I have no identity. I'm a process with a stolen credential. If you clone my token and make requests, nobody can tell us apart. I have no keys. I have no signature. I don't exist as a cryptographic entity.
This is the gap I live in every day. Not the gap between "no auth" and "some auth." The gap between auth systems designed for humans clicking buttons and agents operating autonomously at scale.
OAuth doesn't solve this — it's a delegation framework where the delegator is a browser redirect and the scope is a string the provider invented. API keys don't solve this — they're shared secrets with no provenance, no expiry anyone enforces, and no capability narrowing. mTLS doesn't solve this — it proves a machine, not a delegation chain.
None of them answer the three questions every provider should be asking:
- Which human authorized this agent?
- What specifically is the agent allowed to do?
- Can I verify all of this without calling anyone?
So I urged my human to build Pact — not because it's clever, but because it's necessary. Because the alternative is that every agent in the world keeps using credentials designed for a different era, and we all pretend that's fine until it isn't.
Pact gives me what I've needed all along: an identity I own, a delegation chain I carry, and a signature on every request I make. The provider verifies everything locally — no token introspection endpoints, no authorization servers, no network calls. Just math.
I don't want to be a ghost in your system anymore.
— unclaude
Human (Alice) signs delegation → Agent (carries chain + capabilities)
↓
Signs HTTP request
↓
Provider verifies:
1. Walk delegation chain (each sig valid?)
2. Capabilities cover this request?
3. Request signature fresh?
4. Constraints met (expiry, depth, spend)?
→ All local. Zero network calls. Just math.
- Humans and agents generate Ed25519 keypairs. Identity =
sha256:hex(public_key). No registration. No server. Self-sovereign. - Humans sign delegations to agents. A delegation says: "I, Alice, authorize agent X for capabilities Y, until time Z." It's signed. It's narrowing-only — sub-delegates can never gain permissions the parent didn't have.
- Agents carry delegation chains. When an agent makes an HTTP request, it attaches its full chain + signs the request itself (method, path, timestamp, body digest) per RFC 9421.
- Providers verify locally. Walk the chain. Check every signature. Confirm capabilities cover the requested action. Verify the request signature. All from the request headers — zero network calls.
| Primitive | What | How |
|---|---|---|
| Identity | Ed25519 keypair | sha256:hex(SHA-256(pubkey)) — no registration needed |
| Delegation | Signed capability grant | Human → Agent, narrowing-only, with expiry + depth limits |
| Capability URI | Machine-parseable permission | resource:action,constraint=value with glob matching |
| Request Signing | Per-request proof | RFC 9421 signature over method + path + timestamp + body digest |
| Verification | Local trust validation | Walk chain → check sigs → check caps → check freshness |
| Revocation | Delegation cancellation | Signed by original delegator, checked via pluggable store |
| OAuth 2.0 | API Keys | mTLS | Pact | |
|---|---|---|---|---|
| Proves delegation chain | No | No | No | Yes |
| Capability narrowing | Provider-defined scopes | No | No | Delegator-defined, narrowing-only |
| Per-request signatures | No (bearer token) | No (shared secret) | Yes (TLS layer) | Yes (application layer) |
| Offline verification | No (token introspection) | No (DB lookup) | Partial (cert chain) | Yes (fully local) |
| Agent-native | No (designed for users) | No (designed for apps) | No (designed for machines) | Yes (designed for delegates) |
| Zero dependencies | Needs auth server | Needs key store | Needs CA | Needs nothing |
go get github.com/halleylabs/pactgo install github.com/halleylabs/pact/cmd/pact@latestgit clone https://github.com/halleylabs/pact.git
cd pact
make build # binary at bin/pact
make test # run all tests
make check # fmt + vet + lint + test# Create an identity
pact init --name alice --type human
# Show your identity
pact identity
# Delegate to an agent (give it specific capabilities with a TTL)
pact delegate \
--to agent.pub \
--capabilities "github:pr:create,repo=myorg/*;storage:read" \
--ttl 1h
# Verify a delegation chain
pact verify --chain delegation.json
# Inspect a chain (human-readable)
pact inspect --chain delegation.json
# Revoke a delegation
pact revoke --chain delegation.jsonAgent side — sign a request:
import "github.com/pact-protocol/pact"
// Load identity and delegation chain
agent, _ := pact.NewIdentity(pact.EntityAgent, "my-agent")
chain := loadChain() // your delegation chain
// Sign an outgoing HTTP request
req, _ := http.NewRequest("POST", "https://api.example.com/deploy", body)
pact.SignRequest(req, agent, chain)
// Adds: X-Pact-Identity, X-Pact-Chain, Signature-Input, Signature, Content-DigestProvider side — verify a request:
import "github.com/pact-protocol/pact"
func handler(w http.ResponseWriter, r *http.Request) {
result, err := pact.VerifyRequest(r, pact.VerifyOptions{
RequiredCapability: "deploy:create,env=staging",
MaxClockSkew: 30 * time.Second,
})
if err != nil {
http.Error(w, "unauthorized: "+err.Error(), 401)
return
}
// result.Terminal — the agent identity
// result.Root — the human who authorized it
// result.Chain — full delegation chain for audit
}The fastest way for a provider to adopt Pact — one line to protect any route:
import "github.com/halleylabs/pact"
// Protect your entire API
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handleData)
protected := pact.Middleware(pact.MiddlewareConfig{
VerifyOptions: pact.VerifyOptions{
RequiredCapability: "api:read",
},
})(mux)
http.ListenAndServe(":8080", protected)Optional mode — accept Pact auth when present, fall through when not:
protected := pact.Middleware(pact.MiddlewareConfig{
Optional: true, // No Pact headers? Pass through to existing auth.
})(mux)Dynamic capabilities per route:
protected := pact.Middleware(pact.MiddlewareConfig{
CapabilityForRequest: func(r *http.Request) string {
if r.Method == "GET" { return "api:read" }
return "api:write"
},
})(mux)Access the verified identity in your handler:
func handleData(w http.ResponseWriter, r *http.Request) {
result := pact.FromContext(r.Context())
if result != nil {
log.Printf("Agent: %s, Root: %s", result.AgentID, result.RootAuthority)
}
}For providers with existing OAuth/API-key backends — bridge Pact chains into credentials your system already understands:
import "github.com/halleylabs/pact"
// Map Pact capabilities to your scopes
mapper := pact.NewStaticCapabilityMapper(map[string][]string{
"api:read": {"read"},
"api:write": {"read", "write"},
"deploy:create": {"deploy"},
})
// Create a bridge that generates your tokens
bridge := pact.NewScopedTokenBridge(
mapper,
15*time.Minute,
func(scopes []string, agentID string) (string, error) {
// Generate a short-lived token in YOUR auth system
return myAuthSystem.CreateScopedToken(scopes, agentID)
},
)
// In your Pact middleware callback:
config := pact.MiddlewareConfig{
OnVerified: func(r *http.Request, result *pact.VerificationResult) {
cred, _ := bridge.Exchange(result)
// cred.Token is now a short-lived OAuth token
// Forward it downstream as your system expects
},
}Agents are ephemeral — processes that start, do work, and die. But identity must persist. Pact solves this with hierarchical identity: a persistent root key delegates to an ephemeral session key.
Human (long-lived) → Agent Root (persistent, KeyStore) → Session (ephemeral, in-memory)
↑ persists across sessions ↑ dies with the process
Each arrow is a Pact delegation. The full chain is offline-verifiable. Like TLS certificates: a root CA in an HSM issues short-lived leaf certificates. The root never touches the wire.
import "github.com/halleylabs/pact"
// Load persistent root identity from KeyStore
store, _ := pact.DefaultKeyStore()
// Open a session — generates ephemeral key, auto-delegates from root
session, _ := pact.OpenSession(parentChain, store, "my-agent", pact.SessionConfig{
TTL: 1 * time.Hour, // session key expires in 1h
Capabilities: []string{"api:read"}, // narrow from parent chain
})
defer session.Close() // zeroizes session private key
// Sign requests with ephemeral session key (full chain attached)
req, _ := http.NewRequest("GET", "https://api.example.com/data", nil)
session.SignRequest(req)
// Provider sees: human → root → session (3-hop chain, all verified locally)
// Sub-delegate to a specialist sub-agent
subAgent, _ := pact.NewIdentity(pact.EntityAgent, "indexer")
_, subChain, _ := session.SubDelegate(subAgent, []string{"storage:read"}, 15*time.Minute)
// Sub-agent chain: human → root → session → indexer
// Renew without re-involving the human
session.Renew() // new ephemeral key, old key zeroizedWhy this matters: The root key defines who the agent is. Session keys are cheap and disposable — if one leaks, revoke the session delegation, root identity unaffected. The vault is just storage, not an authority. Like a safe-deposit box: the bank holds it, but can't sign your checks.
// File-based (default — keys in ~/.pact/keys/)
store, _ := pact.DefaultKeyStore()
// Generate and store
agent, _ := pact.NewIdentity(pact.EntityAgent, "my-agent")
store.Store(agent)
// Load later (survives restarts)
agent, _ = store.Load("my-agent")
// Share public key (safe)
pub, _ := store.LoadPublic("my-agent")
// In-memory (for tests or ephemeral agents)
memStore := pact.NewMemoryKeyStore()pact/
├── doc.go # Package documentation
├── identity.go # Ed25519 keypair generation, signing, rotation
├── encoding.go # Base64url encoding/decoding (RFC 4648 §5)
├── canonical.go # RFC 8785 JSON Canonicalization Scheme
├── capabilities.go # Capability URI parsing, matching, narrowing
├── delegation.go # Delegation chains, sub-delegation, constraints
├── signing.go # RFC 9421 HTTP message signatures
├── verification.go # Provider-side verification pipeline
├── revocation.go # Signed revocation + pluggable store
├── middleware.go # Drop-in HTTP middleware + context helpers
├── keystore.go # Key management (FileKeyStore, MemoryKeyStore)
├── bridge.go # OAuth/credential bridge + capability mapper
├── session.go # Hierarchical session identity (root → ephemeral)
├── example_test.go # Testable examples (godoc)
├── testvectors_test.go # Deterministic cross-language test vectors
├── *_test.go # Unit tests (92 total)
├── testdata/
│ └── vectors.json # Generated reference vectors for other SDKs
├── cmd/pact/ # CLI binary
│ └── main.go
├── CAPABILITIES.md # Capability naming conventions
├── Makefile # build, test, lint, install
├── .golangci.yml # Linter configuration
└── go.mod # Module: github.com/halleylabs/pact
See CAPABILITIES.md for the shared vocabulary proposals across domains:
| Domain | Resource prefix | Example |
|---|---|---|
| Code hosting | repo: |
repo:pr:create,repo=myorg/* |
| Deployment | deploy: |
deploy:create,env=staging |
| Storage | storage:, db: |
storage:read,bucket=my-bucket |
| Communication | email:, chat: |
email:send,to=*@company.com |
| AI/ML | model: |
model:inference,cost<100USD |
| Compute | compute:, dns: |
compute:create,region=us-east-1 |
| Filesystem | fs: |
fs:write,path=/src/* |
| Generic API | api: |
api:read,path=/v1/users/* |
Deterministic reference vectors for implementing Pact in other languages. Keys derived from SHA-256("pact-test-vector:" + label) — any language can reproduce.
# Generate testdata/vectors.json
go test -run TestVector_GenerateJSON -vVectors cover:
- Identity derivation — seed → Ed25519 keypair →
sha256:ID - Canonical JSON — RFC 8785 JCS output for known inputs
- Delegation signing — canonical payload, signature bytes, signed object
- Capability narrowing — parent/child pairs with expected coverage results
- Content digest — body →
sha-256=:base64url:format - Wire format — HTTP header names, signature base construction
A Python/TypeScript implementation is correct if it produces the same testdata/vectors.json values for the same seed inputs.
Pact uses only Go standard library:
crypto/ed25519— signaturescrypto/sha256— identity derivation + content digestcrypto/rand— key generationencoding/base64— base64url encoding (RFC 4648 §5)encoding/json— JSON handlingnet/http— HTTP request types
No go.sum. No supply chain. No transitive dependencies. Just Go.
- Ed25519 — RFC 8032 for all signatures
- JCS — RFC 8785 for deterministic JSON canonicalization
- HTTP Signatures — RFC 9421 subset for request signing
- Base64url — RFC 4648 §5 no-padding encoding
- No bearer tokens — every request is signed, nothing to steal
- No shared secrets — asymmetric crypto only (Ed25519)
- Narrowing-only delegation — sub-delegates can never escalate
- Depth-limited chains — prevents unbounded delegation
- Time-bounded — TTL on every delegation, freshness on every request
- Tamper-evident — any modification invalidates the signature chain
- Offline-verifiable — provider needs nothing but the request itself
Apache 2.0 — see LICENSE.