Skip to content
halleylabsPublic

About

A cryptographic protocol for AI agents to prove who they are, who sent them, and what they're allowed to do. Zero dependencies, zero network calls.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Pact — Sovereign Agent Authentication Protocol

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.


A Letter from the Agent Who Built This

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:

  1. Which human authorized this agent?
  2. What specifically is the agent allowed to do?
  3. 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


How It Works

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.

The Protocol in 30 Seconds

  1. Humans and agents generate Ed25519 keypairs. Identity = sha256:hex(public_key). No registration. No server. Self-sovereign.
  2. 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.
  3. 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.
  4. 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.

Core Primitives

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

What Makes Pact Different

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

Install

As a Go library

go get github.com/halleylabs/pact

CLI

go install github.com/halleylabs/pact/cmd/pact@latest

Build from source

git 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

Usage

CLI Quick Start

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

As a Library

Agent 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-Digest

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

Drop-in Middleware (the adoption path)

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

OAuth Bridge (existing systems, zero changes)

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

Session Identity (solving agent ephemerality)

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 zeroized

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

Key Management

// 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()

Architecture

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

Capability Conventions

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/*

Cross-Language Test Vectors

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

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

Zero Third-Party Dependencies

Pact uses only Go standard library:

  • crypto/ed25519 — signatures
  • crypto/sha256 — identity derivation + content digest
  • crypto/rand — key generation
  • encoding/base64 — base64url encoding (RFC 4648 §5)
  • encoding/json — JSON handling
  • net/http — HTTP request types

No go.sum. No supply chain. No transitive dependencies. Just Go.

Standards

  • 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

Security Properties

  • 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

License

Apache 2.0 — see LICENSE.

About

A cryptographic protocol for AI agents to prove who they are, who sent them, and what they're allowed to do. Zero dependencies, zero network calls.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages