Your First Agent Manifest¶
By the end of this tutorial you will have a signed, schema-valid Agent Manifest that passes verify_manifest() with OverallResult.VALID.
What you'll learn¶
- Generate an Ed25519 key pair with
generate_ed25519() - Construct a minimal manifest dict and understand which fields get signed
- Sign the manifest and add the signature block
- Validate the schema with
Manifest.model_validate() - Verify the signed manifest end-to-end
Prerequisites¶
Generate a key pair¶
generate_ed25519() returns an Ed25519KeyPair. The private key stays in memory; you will need the public key later to verify.
from agent_manifest import generate_ed25519
kp = generate_ed25519()
print(kp.key_id) # sha256 hex of the raw public key bytes
print(kp.public_b64url()) # base64url-encoded public key (no padding)
print(kp.private_b64url()) # base64url-encoded private key - keep secret
Store kp.private_b64url() in a secret manager (GitHub Actions secret, Vault, etc.). Never commit it to source control.
Construct the manifest dict¶
A minimal manifest requires manifest_id, agent_id, version, issued_at, expires_at, issuer, and artifacts. All timestamps must be ISO 8601 UTC.
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
manifest_dict = {
"manifest_id": "018f4a3b-2c1d-7e5f-a8b9-0d1e2f3a4b5c", # UUID v7
"agent_id": "spiffe://trust.example/agent/my-agent/prod",
"version": "0.1",
"issued_at": now.isoformat(),
"expires_at": (now + timedelta(hours=24)).isoformat(),
"issuer": "spiffe://trust.example/signing-authority",
"artifacts": {},
}
Use a real UUID v7 in production. You can generate one with ManifestId.generate():
from agent_manifest._types import ManifestId
manifest_dict["manifest_id"] = str(ManifestId.generate())
Understand which fields get signed¶
SIGNED_FIELDS is the normative list of fields that are included in the signing pre-image. Fields outside this list (such as signature itself) are deliberately excluded so they can be added after signing.
from agent_manifest import SIGNED_FIELDS
print(SIGNED_FIELDS)
# ('@context', '@type', 'manifest_id', 'previous_manifest_id', 'agent_id',
# 'version', 'min_verifier_version', 'issued_at', 'expires_at', 'issuer',
# 'crypto_profile', 'artifacts', 'delegation_chain', 'hitl_record',
# 'prior_transparency_log_entry', 'log_retention', 'data_scope',
# 'operational_lifecycle')
signing_pre_image() extracts those fields from your manifest dict and returns the RFC 8785 canonical JSON bytes:
from agent_manifest import signing_pre_image
pre_image = signing_pre_image(manifest_dict)
print(type(pre_image)) # <class 'bytes'>
print(len(pre_image)) # canonical byte length varies by manifest size
The pre-image is what gets signed. Both the signer and the verifier call this same function to guarantee identical byte sequences.
Sign the manifest¶
Ed25519Signer.sign() takes the manifest dict, computes the pre-image internally, and returns a signature block dict. Assign that block to manifest_dict["signature"].
from agent_manifest import Ed25519Signer
signer = Ed25519Signer(kp)
manifest_dict["signature"] = signer.sign(manifest_dict)
print(manifest_dict["signature"])
# {
# "algorithm": "Ed25519",
# "key_id": "<sha256 hex>",
# "key_type": "software",
# "signed_at": "2026-06-21T...",
# "signature_value": "<base64url>",
# "signed_fields": [...]
# }
Validate the schema¶
Manifest.model_validate() checks that the manifest conforms to the Pydantic schema. It raises ValidationError if any required field is missing or any value has the wrong type.
from agent_manifest import Manifest
manifest_obj = Manifest.model_validate(manifest_dict)
print(manifest_obj.manifest_id)
print(manifest_obj.agent_id)
Schema validation is separate from signature verification. A manifest can be schema-valid but have an invalid signature, and vice versa.
Verify the signed manifest¶
verify_manifest() takes the manifest dict, a VerificationContext, and a RevocationStore. It returns a VerificationResult with an OverallResult enum.
To get OverallResult.VALID you must supply the signer's public key in trusted_keys. Without it the result is UNVERIFIABLE - the verifier cannot authenticate the manifest.
from agent_manifest import (
verify_manifest,
VerificationContext,
RevocationStore,
OverallResult,
)
ctx = VerificationContext(
trusted_keys={kp.key_id: kp.public_b64url()},
)
result = verify_manifest(manifest_dict, ctx, RevocationStore())
assert result.result == OverallResult.VALID
assert result.signature_verified is True
print(f"Result: {result.result}") # VALID
print(f"Manifest ID: {result.manifest_id}")
If you omit trusted_keys, the verifier returns UNVERIFIABLE:
ctx_no_keys = VerificationContext()
result = verify_manifest(manifest_dict, ctx_no_keys, RevocationStore())
assert result.result == OverallResult.UNVERIFIABLE
Summary¶
You generated an Ed25519 key pair, built a minimal manifest dict, signed it with Ed25519Signer, validated the schema with Manifest.model_validate(), and verified it with verify_manifest(). The SIGNED_FIELDS tuple and signing_pre_image() are the canonical source of truth for what gets signed - both sides of the protocol call the same function. Next, see CI/CD signing to automate this in a GitHub Actions workflow, or HITL approval workflows to require human sign-off before a manifest can verify as valid.