Skip to main content

Signed Agent Marketplace

Package an agent as a portable, cryptographically signed .agent bundle, verify it end to end, install it safely, and share it through a self-hostable registry with signed reviews and eval-based ranking. A bundle is a deterministic ZIP whose manifest.json inventories every file with a per-file SHA-256 digest, and whose manifest is signed with an Ed25519 publisher key. Nothing in a bundle is ever imported or executed during verification or installation — an installed agent is inert and can only run once you attach an explicit sandbox.

Import:

from synapsekit.marketplace import (
pack_agent,
verify_agent_bundle,
unpack_agent,
install_agent,
AgentBundleVerification,
AgentManifest,
AgentBundleFile,
PublisherIdentity,
InstalledAgent,
AgentSandbox,
FileAgentRegistry,
RegistryEntry,
RankedRegistryEntry,
SignedAgentReview,
)

Signing uses the same Ed25519SigningProvider as the audit subsystem:

from synapsekit.audit.signer import Ed25519SigningProvider

No extra dependency beyond the core install.


The .agent bundle format

A .agent file is a ZIP archive containing exactly:

  • manifest.json — pretty-printed, sorted-key JSON: agent metadata plus a files inventory where every content file is listed with its path, sha256, and size.
  • signature.ed25519 — 64 raw bytes: an Ed25519 signature over the canonical JSON encoding of the manifest.
  • Your content files (e.g. README.md, evals/…, agent.py, memory/…, router/…).

The archive is deterministic: every entry is written with a fixed timestamp and fixed permissions, so packing the same source twice yields byte-identical output. Two schema constants pin the format:

from synapsekit.marketplace import AGENT_BUNDLE_FORMAT, AGENT_BUNDLE_SCHEMA_VERSION
# "synapsekit-agent", "1.0"

Every bundle is required to include a README.md and at least one file under evals/ — the eval suite travels with the agent so a consumer can reproduce its quality claim. An optional entrypoint uses the form FILE:SYMBOL and must reference a bundled file. Bundles that ship a memory/ tree are tagged with the ump/1.0 memory protocol, and a router/ tree is recorded under router_path.

Limits are enforced at pack and verify time: at most 2048 files, 64 MiB per file, and 256 MiB total.


Quickstart (end-to-end shell)

The full lifecycle from the CLI — generate a key, pack and sign, verify, install, and publish to a registry:

# 1. Generate an Ed25519 publisher key (private key written 0o600).
# Prints a "Trusted key: KEY_ID:BASE64" line — share the public half.
synapsekit agent keygen publisher.key --public-key publisher.pub --key-id acme

# 2. Pack and sign a source directory. It MUST contain README.md and evals/.
# Output must use the .agent extension and live outside the source dir.
synapsekit agent pack ./my-agent \
--output pr-reviewer.agent \
--name pr-reviewer \
--agent-version 1.2.0 \
--author "Acme AI" \
--private-key publisher.key \
--key-id acme \
--description "Reviews pull requests" \
--entrypoint agent.py:build \
--tag review --tag engineering \
--eval-score 0.91

# 3. Verify hashes + signature, and pin the publisher key you trust.
# Exit code 0 = accepted. --require-trusted fails if the key is unpinned.
TRUSTED="acme:$(cat publisher.pub)"
synapsekit agent verify pr-reviewer.agent --trusted-key "$TRUSTED" --require-trusted

# 4. Install under <install-root>/<name>/<version>. The agent stays inert.
synapsekit agent install pr-reviewer.agent \
--install-root ~/.synapsekit/agents \
--trusted-key "$TRUSTED" --require-trusted

# 5. Publish to a self-hostable file-backed registry (pinned key required
# unless you opt into an open registry with --allow-untrusted).
synapsekit agent publish pr-reviewer.agent \
--registry ./registry \
--trusted-key "$TRUSTED" --require-trusted

The keygen step prints a Trusted key: acme:<base64> line; that KEY_ID:BASE64 string is exactly what --trusted-key expects. Distribute the public key out of band (not inside the bundle) so consumers can independently pin it.


Packing a bundle in Python

pack_agent walks a source directory, hashes each file, builds and signs the manifest, and writes the deterministic archive:

from pathlib import Path
from synapsekit.audit.signer import Ed25519SigningProvider
from synapsekit.marketplace import pack_agent

provider = Ed25519SigningProvider(key_id="acme")

bundle: Path = pack_agent(
"./my-agent", # source dir (must have README.md + evals/)
"pr-reviewer.agent", # output (must end in .agent, outside source)
name="pr-reviewer",
version="1.2.0",
author="Acme AI",
signing_provider=provider, # SigningProvider, or 32 raw private-key bytes
description="Reviews pull requests",
entrypoint="agent.py:build", # optional FILE:SYMBOL referencing a bundled file
tags=("review", "engineering"),
eval_score=0.91, # optional, 0.0–1.0
)

signing_provider accepts either a SigningProvider instance or the 32 raw private-key bytes (the CLI reads them from --private-key). Only Ed25519 is supported by schema 1.0.


Verifying integrity and trust

verify_agent_bundle reads the archive into memory and checks, in order: safe archive paths, no duplicate or case-colliding entries, no directory/symlink entries, size and count limits, the manifest inventory matches the archive exactly, every file's SHA-256 and size match, and the Ed25519 signature is valid over the canonical manifest. It returns an AgentBundleVerification and never raises for a bad bundle:

from synapsekit.marketplace import verify_agent_bundle

result = verify_agent_bundle("pr-reviewer.agent")
result.integrity_valid # archive is structurally valid and untampered
result.trusted # True only if a pinned key matched (see below)
result.bundle_sha256 # SHA-256 of the whole .agent file
result.errors # tuple of human-readable failure reasons
result.manifest # AgentManifest, or None on failure

Trust is separate from integrity. A valid signature only proves the manifest was signed by some key embedded in the bundle. To establish who signed it, pin the publisher's independently obtained public key:

trusted_keys = {"acme": provider.public_key_bytes()}   # key_id -> 32 raw bytes
result = verify_agent_bundle("pr-reviewer.agent", trusted_keys=trusted_keys)
assert result.integrity_valid and result.trusted

If a pinned key exists for the publisher's key_id but does not match the bundle's embedded key, verification fails outright. Convenience guards raise instead of returning a flag:

manifest = result.require_valid()     # raises InvalidAgentBundleError if tampered
manifest = result.require_trusted() # also raises UntrustedPublisherError if unpinned

The CLI mirrors this — verify exits 0 when the bundle is intact (and, with --require-trusted, only when the publisher is pinned), 1 otherwise, and supports --format json:

synapsekit agent verify pr-reviewer.agent --format json \
--trusted-key "acme:$(cat publisher.pub)"

The safe install flow

install_agent verifies the bundle from an immutable in-memory snapshot of the raw bytes, then extracts into a staging temp directory and atomically replace()s it into NAME/VERSION. If any step fails, staging is discarded and nothing partial is left behind.

from synapsekit.marketplace import install_agent

installed = install_agent(
"pr-reviewer.agent",
install_root="~/.synapsekit/agents", # default: ~/.synapsekit/agents
trusted_keys={"acme": provider.public_key_bytes()},
require_trusted=True,
)
installed.path # <install_root>/pr-reviewer/1.2.0
installed.manifest # AgentManifest
installed.trusted # publisher trust outcome
installed.bundle_sha256 # recorded in .synapsekit-install.json
installed.sandbox_required # True unless the publisher was trusted

Because verification runs against the in-memory bytes before extraction, the following are all rejected before a single file touches disk:

  • Path traversal — absolute paths, .. segments, or backslashes.
  • Symlinks and directory entries — only regular files are extracted.
  • Windows device names / ADS — reserved names (CON, NUL, COM1LPT9) and any : in a path component.
  • Duplicates and case collisions — a repeated path, or two paths that differ only in case, are refused (protects case-insensitive filesystems).
  • Size / count limits — the 2048-file, 64 MiB-per-file, 256 MiB-total caps.
  • Inventory drift — the set of archive files must equal the manifest inventory exactly (no missing, no extra), and every hash must match.

Reinstalling the identical bundle over an existing NAME/VERSION is idempotent (the recorded bundle_sha256 matches, so it returns the existing install). A different bundle at the same NAME/VERSION raises FileExistsError — installs are immutable.

To unpack into an arbitrary fresh directory instead of the versioned install root, use unpack_agent (the CLI unpack subcommand); it applies the same verification and atomic extraction and refuses to overwrite an existing destination.


Running an installed agent (sandbox required)

An installed bundle is inert by design. InstalledAgent.run refuses to execute unless you pass an AgentSandbox — the execution boundary is your responsibility, so packaging never becomes an arbitrary-code-execution vector:

from pathlib import Path
from synapsekit.marketplace import AgentManifest, AgentSandbox

class MySandbox: # satisfies the AgentSandbox protocol
async def run(self, agent_path: Path, manifest: AgentManifest,
prompt: str, **kwargs):
# Load and execute manifest.entrypoint inside your isolation of choice
# (subprocess, container, gVisor, WASM, …) and return the result.
...

result = await installed.run("Review PR #42", sandbox=MySandbox())

Calling installed.run(prompt) without a sandbox raises SandboxRequiredError. installed.sandbox_required is True whenever the publisher was not independently trusted (and the manifest still requires sandboxing for untrusted agents — schema 1.0 always does).


Self-hostable registry

FileAgentRegistry is a registry backed by a plain directory and a static index.json — host it on any static file server, object store, or Git repo. Publishing re-verifies the bundle, copies it immutably under packages/NAME/VERSION/NAME-VERSION.agent, re-verifies the copy, and only then appends to the index under a lock.

from synapsekit.marketplace import FileAgentRegistry

registry = FileAgentRegistry("./registry")

entry = registry.publish(
"pr-reviewer.agent",
trusted_keys={"acme": provider.public_key_bytes()}, # pinned by default
)
entry.name, entry.version, entry.bundle_sha256, entry.eval_score

registry.list() # all RegistryEntry, sorted by (name, version)
registry.get("pr-reviewer", "1.2.0") # one entry or None
registry.bundle_path("pr-reviewer", "1.2.0") # safe, root-confined Path

By default publishing requires a pinned publisher key; pass allow_untrusted=True to intentionally run an open registry. Republishing the same bytes at the same version is idempotent; different bytes at an existing version raise FileExistsError.

Signed reviews and eval-based ranking

Reviews are themselves Ed25519-signed, so a registry mirror can't forge or tamper with them. Build one with SignedAgentReview.sign and add it to the registry:

from synapsekit.audit.signer import Ed25519SigningProvider
from synapsekit.marketplace import SignedAgentReview

reviewer = Ed25519SigningProvider(key_id="independent-lab")

review = SignedAgentReview.sign(
agent_name="pr-reviewer",
agent_version="1.2.0",
reviewer="Independent Lab",
rating=5, # 1–5
eval_score=1.0, # 0.0–1.0, reproduced independently
comment="Reproduced the eval suite.",
signing_provider=reviewer,
)

registry.add_review(review, trusted_keys={"independent-lab": reviewer.public_key_bytes()})
registry.reviews("pr-reviewer", "1.2.0") # verifies each stored signature on read

add_review rejects reviews for unpublished agents, rejects invalid signatures, and (like publishing) requires a pinned reviewer key unless allow_untrusted=True. Each review's signature is re-verified whenever reviews() reads it back.

ranked() returns a deterministic quality ordering. Each review contributes (eval_score + rating / 5) / 2; a version's score is its own eval_score (weighted 0.7) blended with the mean review score (weighted 0.3), falling back to whichever is available:

for ranked in registry.ranked():
print(ranked.entry.name, ranked.entry.version, ranked.score, ranked.review_count)

Ties break by name then version, so the ordering is reproducible across mirrors.

The CLI publish subcommand wraps FileAgentRegistry.publish; reviews and ranking are Python-only APIs.


CLI reference

All subcommands live under synapsekit agent.

SubcommandPurpose
keygen PRIVATE_KEYGenerate an Ed25519 publisher key
pack SOURCEPack and sign a portable .agent bundle
verify BUNDLEVerify hashes, signature, and publisher trust
unpack BUNDLE OUTPUTVerify and unpack into a fresh directory
install BUNDLEVerify and install an inert bundle under NAME/VERSION
publish BUNDLEPublish a bundle to a file-backed registry

keygen PRIVATE_KEY

FlagDefaultDescription
PRIVATE_KEY (positional)requiredNew raw private-key file (refuses to overwrite)
--public-keyNoneOptional base64 public-key output file
--key-idNoneOptional stable publisher key id

pack SOURCE

FlagDefaultDescription
SOURCE (positional)requiredAgent source directory
--outputrequiredOutput .agent path
--namerequiredPortable agent name
--agent-versionrequiredPortable agent version
--authorrequiredPublisher or author name
--private-keyrequiredRaw Ed25519 private-key file (exactly 32 bytes)
--key-idNonePublisher key id
--description""Agent description
--entrypointNoneOptional bundled FILE:SYMBOL
--tag[]Agent tag (repeatable)
--eval-scoreNoneEval score from 0 to 1

Trust options (shared by verify, unpack, install, publish):

FlagDescription
--trusted-key KEY_ID:BASE64_PUBLIC_KEYPin an independently obtained publisher key (repeatable)
--require-trustedReject bundles whose publisher key is not pinned

verify BUNDLE adds --format {text,json} (default text). Exits 0 when accepted, 1 otherwise.

unpack BUNDLE OUTPUTOUTPUT is a new destination directory (must not already exist).

install BUNDLE adds --install-root (default ~/.synapsekit/agents).

publish BUNDLE adds --registry (required, registry root dir) and --allow-untrusted (allow self-signed publishers in an intentionally open registry — mutually exclusive with --require-trusted).


API reference

Bundle functions

  • pack_agent(source, output, *, name, version, author, signing_provider, description="", entrypoint=None, tags=(), eval_score=None, key_id=None) -> Path
  • verify_agent_bundle(bundle, *, trusted_keys=None) -> AgentBundleVerification
  • unpack_agent(bundle, output, *, trusted_keys=None, require_trusted=False) -> AgentManifest
  • install_agent(bundle, *, install_root=None, trusted_keys=None, require_trusted=False) -> InstalledAgent

AgentBundleVerification

Fields: integrity_valid, trusted, errors, manifest, bundle_sha256. Property ok (alias for integrity_valid). Methods require_valid() -> AgentManifest, require_trusted() -> AgentManifest.

AgentManifest

Fields: name, version, author, publisher, files, description, entrypoint, tags, eval_score, memory_format, router_path, format, schema_version, sandbox_required_for_untrusted. Methods: to_dict(), from_dict().

AgentBundleFile

Fields: path, sha256, size. Methods: to_dict(), from_dict().

PublisherIdentity

Fields: algorithm ("ed25519"), key_id, public_key_b64. Property public_key_bytes -> bytes.

InstalledAgent

Fields: path, manifest, trusted, bundle_sha256. Property sandbox_required -> bool. Method async run(prompt, *, sandbox=None, **kwargs) — raises SandboxRequiredError when sandbox is None.

AgentSandbox (Protocol)

async run(agent_path: Path, manifest: AgentManifest, prompt: str, **kwargs) -> Any

FileAgentRegistry(root)

  • publish(bundle, *, trusted_keys=None, allow_untrusted=False) -> RegistryEntry
  • get(name, version) -> RegistryEntry | None
  • list() -> list[RegistryEntry]
  • bundle_path(name, version) -> Path
  • add_review(review, *, trusted_keys=None, allow_untrusted=False) -> Path
  • reviews(name, version) -> list[SignedAgentReview]
  • ranked() -> list[RankedRegistryEntry]

RegistryEntry

Fields: name, version, author, description, tags, publisher_key_id, bundle_sha256, bundle_path, published_at, eval_score.

RankedRegistryEntry

Fields: entry (RegistryEntry), score (float), review_count (int).

SignedAgentReview

Fields: agent_name, agent_version, reviewer, rating (1–5), eval_score (0.0–1.0), comment, signed_at, reviewer_identity (PublisherIdentity), signature_b64, schema_version. Classmethod sign(*, agent_name, agent_version, reviewer, rating, eval_score, signing_provider, comment="", signed_at=None). Methods verify(trusted_keys=None) -> tuple[bool, bool], to_dict(), from_dict().

Exceptions

AgentMarketplaceError (base), InvalidAgentBundleError, UntrustedPublisherError, SandboxRequiredError.


See also