Catalog PyPI
PACKAGE EXPLORERView in registry

agent-signing

PyPI

A Python package for signing agent setups

Versions collected1
Direct dependencies14From release 0.2.0
Last observed20 Oct, 23:23 UTC

README

Version 0.2.0 documentation

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
OptionDefaultDescription
--out-dir.Directory to write the key files into
--prefixagentFilename 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
OptionDefaultDescription
manifest--(positional) Path to the JSON manifest
-o, --outputagent_signature.jsonOutput 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-componentsoffEmbed 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
OptionDefaultDescription
manifest--(positional) Path to the JSON manifest
-s, --signature-fileagent_signature.jsonSignature 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
OptionDefaultDescription
registry_url--(positional) Base URL of the registry
-s, --signature-fileagent_signature.jsonSignature 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

EndpointMethodDescription
/signaturesPOSTSubmit a signature record
/signatures/{hash}GETLook up signatures by aggregate hash
/signatures?limit=20&offset=0GETList recent signatures (paginated)
/GETWeb 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:

ComponentFields extracted
LangChain toolname, description, args (JSON schema)
CrewAI toolname, description, args_schema (JSON schema)
LangGraph agenttools discovered via nodes["tools"]
CrewAI agentrole, goal, backstory, llm, tools
Dictall 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)

ParameterTypeDescription
secretstr | NoneHMAC shared secret
private_keybytes | NoneEd25519 private key (32 bytes) for signing
public_keybytes | NoneEd25519 public key (32 bytes) for verification
identity_tokenstr | NoneJWT string to attach to the signature
namestr | NoneSelf-declared signer name (informational; not signed)
emailstr | NoneSelf-declared signer email (informational; not signed)

Methods

MethodDescription
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() -> strCompute and return the signature
sign_to_file(path, include_components=False) -> strSign and write a JSON signature file (timestamp, public key, hash, signature; plus name/email and components when set)
verify(signature) -> VerificationResultVerify against a previous signature
verify_file(path) -> VerificationResultVerify 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) -> dictPublish a signature to a registry server

VerificationResult

FieldTypeDescription
validboolWhether verification passed
reasonstrHuman-readable explanation
identitydict | NoneDecoded 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

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.2.04 Jun 202614

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
hanneshapke/agent-signing

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 23:23 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:c4932176f9f58a41312847f7464536fe347f12d417644a6dd502d5111a38695a