Skip to main content

A Python package for signing agent setups

Project description

agent-signing

Sign and verify AI agent setups. Detects when tools, agents, or their configurations change -- and optionally proves who signed them.

  • Order-independent -- reordering tools or agents in code does not invalidate the signature
  • Framework-agnostic -- auto-discovers metadata from LangChain/LangGraph, CrewAI, or plain dicts
  • Three signing modes -- HMAC (shared secret), Ed25519 (asymmetric keypair), JWT (identity tokens)
  • Zero framework dependencies -- detection is duck-typed; cryptography is the only runtime dependency

Installation

pip install agent-signing

Optional dependencies for running the demos:

pip install agent-signing[demo-langchain]  # LangChain / LangGraph demos
pip install agent-signing[demo-crewai]     # CrewAI demo
pip install agent-signing[server]          # Registry server (FastAPI + uvicorn)

Quick start

from agent_signing import AgentSigner

signer = AgentSigner()
signer.add_tool({"name": "search", "description": "Search the web", "parameters": {"query": "str"}})
signer.add_tool({"name": "calculator", "description": "Evaluate math", "parameters": {"expr": "str"}})
signature = signer.sign()

# Later -- rebuild the same setup and verify
verifier = AgentSigner()
verifier.add_tool({"name": "calculator", "description": "Evaluate math", "parameters": {"expr": "str"}})
verifier.add_tool({"name": "search", "description": "Search the web", "parameters": {"query": "str"}})
result = verifier.verify(signature)
assert result.valid  # True -- order doesn't matter

Signing modes

HMAC (shared secret)

signer = AgentSigner(secret="my-secret-key")
signer.add_tool(my_tool)
signature = signer.sign()

verifier = AgentSigner(secret="my-secret-key")
verifier.add_tool(my_tool)
assert verifier.verify(signature)

Ed25519 (asymmetric key pair)

Proves who signed the agent setup. Sign with a private key, verify with the corresponding public key.

from agent_signing import AgentSigner, generate_keypair

private_key, public_key = generate_keypair()

# Sign
signer = AgentSigner(private_key=private_key)
signer.add_tool(my_tool)
signature = signer.sign()

# Verify (can be a different machine -- only needs the public key)
verifier = AgentSigner(public_key=public_key)
verifier.add_tool(my_tool)
result = verifier.verify(signature)
assert result.valid

JWT identity token

Attach an identity token from an OAuth/OIDC provider. The token's claims are decoded and returned on verification.

signer = AgentSigner(identity_token="eyJhbG...")
signer.add_tool(my_tool)
signature = signer.sign()

verifier = AgentSigner()
verifier.add_tool(my_tool)
result = verifier.verify(signature)
assert result.valid
print(result.identity)  # {"sub": "user@example.com", "iss": "https://accounts.google.com"}

Ed25519 and JWT can be combined for both cryptographic proof and identity context.

Signature files

Write signatures to disk with sign_to_file() and verify with verify_file(). The file includes the signing timestamp, public key, hash, and signature.

from agent_signing import AgentSigner, generate_keypair

private_key, public_key = generate_keypair()

signer = AgentSigner(private_key=private_key)
signer.add_tool(my_tool)
signer.sign_to_file("agent_signature.json")
# Creates:
# {
#   "signed_at": "2025-06-15T12:00:00+00:00",
#   "public_key": "abcd1234...",
#   "hash": "842f9705...",
#   "signature": "{\"hash\": ...}"
# }

# Later -- verify against the file
verifier = AgentSigner(public_key=public_key)
verifier.add_tool(my_tool)
result = verifier.verify_file("agent_signature.json")
assert result.valid

# Inspect the file contents
record = AgentSigner.load_signature_file("agent_signature.json")
print(record["signed_at"], record["public_key"])

Command-line interface

Installing the package adds an agent-signing command for key generation, signing, verification, and publishing -- no Python code required.

agent-signing --help            # list commands
agent-signing <command> --help  # options for a command

Manifest format

sign and verify read the agent setup from a JSON manifest with tools and/or agents arrays. Each entry is a plain dict (see Plain dicts); at least one tool or agent is required.

{
  "tools": [
    {"name": "search", "description": "Search the web", "parameters": {"query": "str"}},
    {"name": "calculator", "description": "Evaluate math", "parameters": {"expression": "str"}}
  ],
  "agents": [
    {"name": "researcher", "role": "Researcher", "goal": "Find info", "tools": ["search"]}
  ]
}

generate-keypair

Generate an Ed25519 key pair, written as hex-encoded files.

agent-signing generate-keypair --out-dir ./keys --prefix agent
# writes ./keys/agent.private.key and ./keys/agent.public.key
Option Default Description
--out-dir . Directory to write the key files into
--prefix agent Filename prefix (<prefix>.private.key, <prefix>.public.key)

sign

Sign a manifest and write a signature file. Choose a mode with --secret (HMAC) or --private-key (Ed25519); with neither, the bare aggregate hash is written.

# HMAC
agent-signing sign manifest.json --secret "my-secret"

# Ed25519, attaching identity + self-declared metadata, embedding components
agent-signing sign manifest.json \
    --private-key keys/agent.private.key \
    --identity-token "eyJhbG..." \
    --name "Ada Lovelace" --email ada@example.com \
    --include-components \
    -o agent_signature.json
Option Default Description
manifest -- (positional) Path to the JSON manifest
-o, --output agent_signature.json Output signature file
--secret -- HMAC shared secret
--private-key -- Path to a hex-encoded Ed25519 private key file
--identity-token -- JWT identity token to attach
--name -- Self-declared signer name (informational)
--email -- Self-declared signer email (informational)
--include-components off Embed the signed tool/agent definitions in the file

verify

Re-derive the signature from a manifest and check it against a signature file. Exits non-zero on mismatch. Supply --secret or --public-key to match the signing mode; when a JWT was attached, the decoded identity is printed.

agent-signing verify manifest.json -s agent_signature.json --public-key keys/agent.public.key
Option Default Description
manifest -- (positional) Path to the JSON manifest
-s, --signature-file agent_signature.json Signature file to verify against
--registry-url -- Fetch the approved signature from this registry instead of a local file
--secret -- HMAC shared secret used at signing time
--public-key -- Path to a hex-encoded Ed25519 public key file

With --registry-url, the current setup's hash is looked up on the registry and verified against the registered signature — no local signature file required:

agent-signing verify manifest.json --registry-url http://localhost:8000 --public-key keys/agent.public.key

upload

Publish an existing signature file to a registry server.

agent-signing upload http://localhost:8000 -s agent_signature.json
Option Default Description
registry_url -- (positional) Base URL of the registry
-s, --signature-file agent_signature.json Signature file to upload

The signature file carries whatever sign wrote into it, so --name, --email, and embedded --include-components are published too. See Publishing the signed setup.

Signature registry

The optional registry server lets teams publish and inspect signatures in a central location.

Running the server

pip install agent-signing[server]
uvicorn server.backend.main:app --reload

Open http://localhost:8000 to browse the web UI -- search by hash or view recent signatures.

Publishing signatures

Use publish() to submit a signature to the registry. It accepts an optional path to publish from an existing signature file, or signs on-the-fly.

from agent_signing import AgentSigner, generate_keypair

private_key, public_key = generate_keypair()

signer = AgentSigner(private_key=private_key)
signer.add_tool(my_tool)
signer.sign_to_file("agent_signature.json")

# Publish from the signature file
signer.publish("http://localhost:8000", path="agent_signature.json")

# Or sign and publish in one step (no file needed)
signer.publish("http://localhost:8000")

Publishing the signed setup

By default a published record contains only the hash and signature -- the registry never sees which tools and agents were signed. Pass include_components=True to also publish the signed component definitions. The registry re-derives the aggregate hash from them and confirms it matches the signed hash, so the tool/agent summary it displays is verifiable rather than self-asserted: a tampered list is flagged instead of trusted.

You can also attach a self-declared name and email. These are not covered by the signature (anyone can claim any name), so the registry labels them as self-declared.

signer = AgentSigner(
    private_key=private_key,
    name="Ada Lovelace",
    email="ada@example.com",
)
signer.add_tool(my_tool)
signer.add_agent(my_agent)

# Embed the signed components so the registry can verify and display them
signer.publish("http://localhost:8000", include_components=True)

Or from the command line:

agent-signing sign manifest.json --private-key agent.private.key \
    --name "Ada Lovelace" --email ada@example.com --include-components
agent-signing upload http://localhost:8000 -s agent_signature.json

For each signature, the web UI then runs a real in-browser Ed25519 verification and shows the signer's key fingerprint, any decoded JWT identity, the delay between signing and registration, other signatures on the same hash (co-signatures), and -- when components were published -- the verified list of signed tools and agents.

Verifying against the registry

verify_from_registry() is the network counterpart to verify_file(): instead of reading the approved signature from a local file, it fetches it from the shared registry. It computes the current setup's aggregate hash, looks it up on the registry (GET /signatures/{hash}), and verifies the setup against the registered signature(s).

verifier = AgentSigner(public_key=public_key)
verifier.add_tool(my_tool)

result = verifier.verify_from_registry("http://localhost:8000")
if result.valid:
    print(result.reason)
    print(result.record["signed_at"])  # the matched registry record

A tampered setup hashes to a different value, for which no signature is registered, so the lookup returns nothing and verification fails. Trust is anchored in the material held by the verifier, never in the fetched record:

  • Ed25519 (public_key set): the registered signature must validate against your pinned public key. A record an attacker self-published under their own key for a tampered setup is rejected.
  • HMAC (secret set): the registered signature must match an HMAC recomputed with your shared secret.
  • Neither: only confirms that a signature for this exact setup is registered; the signer is not authenticated.

When multiple parties have co-signed the same hash, the first record that passes is returned in result.record.

Registry API

Endpoint Method Description
/signatures POST Submit a signature record
/signatures/{hash} GET Look up signatures by aggregate hash
/signatures?limit=20&offset=0 GET List recent signatures (paginated)
/ GET Web UI

The POST /signatures body accepts optional name, email, and components fields in addition to the required hash, signature, signed_at, and public_key. When components are present the response includes components_verified and a summary of the signed tools and agents.

Framework support

LangChain / LangGraph

add_tool() accepts any LangChain BaseTool (including @tool-decorated functions) and auto-extracts name, description, and args (parameter schema).

add_agent() accepts a LangGraph CompiledStateGraph (from create_react_agent) and auto-discovers all tools bound to the agent.

from langchain_core.tools import tool
from langchain.agents import create_agent
from agent_signing import AgentSigner, generate_keypair

@tool
def search(query: str) -> str:
    """Search the web."""
    return "results"

agent = create_agent(llm, [search])

private_key, public_key = generate_keypair()
signer = AgentSigner(private_key=private_key)
signer.add_agent(agent)  # auto-discovers all tools bound to the agent
signer.sign_to_file("agent_signature.json")

CrewAI

add_tool() accepts CrewAI BaseTool objects and extracts name, description, and args_schema.

add_agent() accepts CrewAI Agent objects and extracts role, goal, backstory, llm, and tools.

from crewai import Agent
from crewai.tools import tool
from agent_signing import AgentSigner, generate_keypair

@tool("search")
def search(query: str) -> str:
    """Search for information."""
    return "results"

researcher = Agent(
    role="Researcher",
    goal="Find accurate information",
    backstory="You are a skilled researcher.",
    tools=[search],
)

private_key, public_key = generate_keypair()
signer = AgentSigner(private_key=private_key)
signer.add_tool(search)
signer.add_agent(researcher)
signer.sign_to_file("agent_signature.json")

Plain dicts

For any other framework, pass plain dicts:

signer.add_tool({"name": "search", "description": "Search the web", "parameters": {"query": "str"}})
signer.add_agent({"name": "researcher", "role": "Researcher", "goal": "Find info"})

What gets signed

The signature covers the semantic definition of the agent setup, not the source code. Specifically:

Component Fields extracted
LangChain tool name, description, args (JSON schema)
CrewAI tool name, description, args_schema (JSON schema)
LangGraph agent tools discovered via nodes["tools"]
CrewAI agent role, goal, backstory, llm, tools
Dict all keys passed

The signature changes when any of these fields change. It does not change when:

  • Tools or agents are reordered in code
  • Unrelated code around the agent setup changes
  • Runtime state (e.g., conversation history) changes

API reference

AgentSigner(secret=None, private_key=None, public_key=None, identity_token=None, name=None, email=None)

Parameter Type Description
secret str | None HMAC shared secret
private_key bytes | None Ed25519 private key (32 bytes) for signing
public_key bytes | None Ed25519 public key (32 bytes) for verification
identity_token str | None JWT string to attach to the signature
name str | None Self-declared signer name (informational; not signed)
email str | None Self-declared signer email (informational; not signed)

Methods

Method Description
add_tool(tool) Register a tool (framework object or dict)
add_agent(agent) Register an agent (framework object or dict)
components (property) The extracted tool/agent components that get signed
sign() -> str Compute and return the signature
sign_to_file(path, include_components=False) -> str Sign and write a JSON signature file (timestamp, public key, hash, signature; plus name/email and components when set)
verify(signature) -> VerificationResult Verify against a previous signature
verify_file(path) -> VerificationResult Verify against a signature file
load_signature_file(path) -> dict (static) Read and return a signature file's contents
publish(registry_url, path=None, include_components=False) -> dict Publish a signature to a registry server

VerificationResult

Field Type Description
valid bool Whether verification passed
reason str Human-readable explanation
identity dict | None Decoded JWT claims (when JWT was used)

Supports bool() -- use if result: directly.

generate_keypair() -> tuple[bytes, bytes]

Returns (private_key, public_key) as raw 32-byte Ed25519 keys.

Contributing

Contributions are welcome! See CONTRIBUTING.md for the local development workflow -- linting with ruff, testing with pytest, commit conventions, and the commitizen tag-driven release process.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_signing-0.2.0.tar.gz (35.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_signing-0.2.0-py3-none-any.whl (29.3 kB view details)

Uploaded Python 3

File details

Details for the file agent_signing-0.2.0.tar.gz.

File metadata

  • Download URL: agent_signing-0.2.0.tar.gz
  • Upload date:
  • Size: 35.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for agent_signing-0.2.0.tar.gz
Algorithm Hash digest
SHA256 2099c076b90be9b64d9a3eb5433fd82887e4db6abcb65d96697e99cfaeab558a
MD5 9fc9ae8033a9a9f116521db98d20994e
BLAKE2b-256 7c78240769ec28465f97689d4a3a43d54c86b9fb6e3e65b20d948059af5a4f8c

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_signing-0.2.0.tar.gz:

Publisher: release.yml on hanneshapke/agent-signing

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_signing-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: agent_signing-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 29.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for agent_signing-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 87d31403a5ebbcf43f45acb20afb2b587f9726759cad75a24095ceb593b7256c
MD5 bb3c518b130bbe6fc20543ea27317754
BLAKE2b-256 88e3ed52bd928dc85b271d476d2549a5eff0d29c3b3696dfe8aed2f762c41516

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_signing-0.2.0-py3-none-any.whl:

Publisher: release.yml on hanneshapke/agent-signing

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page