Skip to content

Signed capability cards

SYNAPSE can attach a domain-separated Ed25519 signature to an agent capability advertisement and verify it before projecting the card into manifests, directories, dashboards, MCP resources, or A2A Agent Cards. Verification makes discovery metadata tamper-evident. It is deliberately advisory: a valid card does not authorize a tool, grant an ACL permission, execute code, prove that an advertised capability works, or replace signed messages and connection identity.

Unsigned cards keep working. Every projected card carries an explicit verification.result; an unsigned card is missing_signature, while a signed card with a failure remains visible with the exact failure instead of being hidden or presented as verified.

Runtime status

The first complete advisory runtime ships:

  • synapse capability-card keygen creates a profile-separated owner-only Ed25519 private key and can enrol its public key into a separate trust bundle;
  • synapse capability-card sign signs strict canonical card JSON and refuses duplicate JSON keys or output-file replacement;
  • synapse capability-card verify performs one-shot cryptographic and binding verification without changing live replay history;
  • synapse worker --capability-card-key ... signs each normalized live advertisement with an increasing in-process sequence;
  • synapse hub --capability-card-trust ... verifies cards against explicit agent and project bindings, a validity window, key expiry/revocation, and a bounded sequence/downgrade history. The in-memory default preserves legacy operation; --capability-card-history-db opts into an owner-only SQLite history that survives hub restarts;
  • manifest, directory, fallback dashboard, and A2A projections show the advisory result.

The feature adds no required cloud service and no required dependency to the core-only path. Ed25519 operations use the existing optional security dependency. With no card trust bundle and no signing key, legacy unsigned behaviour remains byte-compatible on the client wire.

Operator walkthrough

Create a separate card-signing key and trust file:

synapse capability-card keygen \
  --key-id SYNAPSE-CHANNEL:worker:2026-07 \
  --private-out ./worker-card.pem \
  --agent SYNAPSE-CHANNEL/worker \
  --project SYNAPSE-CHANNEL \
  --trust ./capability-card-trust.json

Start an advisory-verifying hub. The trust file does not enable an enforcement gate; it enables truthful diagnostics:

synapse hub \
  --capability-card-trust ./capability-card-trust.json \
  --capability-card-history-db ./capability-card-history.db \
  --capability-card-clock-skew-seconds 30 \
  --capability-card-history-capacity 4096 \
  --capability-card-history-retention-seconds 3600

Start a namespaced worker with the corresponding private key:

synapse worker \
  --prefix SYNAPSE-CHANNEL/ \
  --name worker \
  --capability-card-key ./worker-card.pem \
  --capability-card-key-id SYNAPSE-CHANNEL:worker:2026-07 \
  --capability-card-project SYNAPSE-CHANNEL

synapse manifest then renders verify=valid. A missing, unknown, revoked, expired, replayed, downgraded, or tampered card stays listed with its own result.

For an offline card, put the stable advertisement fields in card.json, then:

synapse capability-card sign card.json \
  --key ./worker-card.pem \
  --key-id SYNAPSE-CHANNEL:worker:2026-07 \
  --sequence 1 \
  --out signed-card.json

synapse capability-card verify signed-card.json \
  --trust ./capability-card-trust.json \
  --json

The one-shot verifier checks the signature and bindings but intentionally does not consume sequence state. Replay and downgrade checks happen on the live hub, where consecutive advertisements share one bounded history. Omit --capability-card-history-db to retain the bounded in-memory default.

Trust-bundle profile

Card keys are separate from connection-identity, event-signing, federation, and receipt-signing material. The public JSON shape is:

{
  "keys": [
    {
      "key_id": "SYNAPSE-CHANNEL:worker:2026-07",
      "public_key": "base64-raw-ed25519-public-key",
      "agents": ["SYNAPSE-CHANNEL/worker"],
      "projects": ["SYNAPSE-CHANNEL"],
      "expires_at": 1785600000.0,
      "revoked": false
    }
  ]
}

Agent and project arrays are mandatory and non-empty. There is no wildcard and no trust-on-first-use. Duplicate key ids, malformed base64, non-Ed25519 keys, non-finite expiry, and malformed revocation state fail hub startup.

Signing profile

The signer normalizes the stable card fields and attaches this envelope:

{
  "version": 1,
  "key_id": "SYNAPSE-CHANNEL:worker:2026-07",
  "algorithm": "ed25519",
  "signed_at": 1783879200.0,
  "expires_at": 1783879500.0,
  "sequence": 42,
  "card_digest": "sha256-hex",
  "value": "base64-signature"
}

Canonical JSON sorts object keys, preserves JSON strings and integers, uses compact UTF-8 encoding, rejects duplicate input keys and non-finite values, and removes only signature.value from the signed bytes. The bytes and card digest have distinct SYNAPSE-CAPABILITY-CARD-...-V1 domain prefixes, so a card signature cannot verify as a signed event, receipt, or connection proof.

advertised_at and verification are hub projection fields and are excluded; the advertiser cannot know them before admission. The stable digest excludes the whole signature envelope, preventing recursive content. The signature still covers its own key id, sequence, validity window, and claimed digest.

Verification results

Result Meaning
valid Signature, digest, agent/project binding, expiry, and lifecycle checks passed.
missing_signature Card is unsigned and remains advisory discovery.
unknown_key Key id is absent from the card trust bundle.
revoked_key Operator marked the key revoked.
bad_signature Envelope, digest, canonical JSON, or Ed25519 signature is invalid.
expired Key or card is outside its accepted validity window.
sequence_mismatch Sequence did not increase for the same agent/key binding.
capability_downgrade A newer valid card removed a task class, skill, or contract.
agent_mismatch Card, socket sender, and allowed agent binding disagree.
project_scope_mismatch Card, sender namespace, and allowed project binding disagree.
manifest_mismatch A caller-required manifest digest differs from the signed card.
history_full Bounded lifecycle state could not admit a new binding without evicting a live replay guard.
history_unavailable The configured durable history could not validate or commit lifecycle state, so the card is not reported as valid.

A capability downgrade is recorded and surfaced for review; it is not silently upgraded to valid. It still does not create an execution denial because card verification is advisory in this tranche.

Durable history and recovery

--capability-card-history-db FILE creates or opens a dedicated SQLite database. It requires --capability-card-trust, uses full synchronous transactions and DELETE journalling, and serializes concurrent writers with an immediate transaction. On POSIX, the database must be a regular non-symlink file owned by the current user with mode 0600. Startup validates the schema, retained rows, configured capacity, and SQLite quick check; ambiguity or corruption stops the hub before it binds. A runtime lock, I/O, or validation failure rolls back the transaction and projects history_unavailable instead of valid.

The database contains card identities, key ids, sequence floors, route-capability floors, digests, and timestamps. It contains no private signing key, but it is still security state: deleting or replacing it removes replay and downgrade memory. This is the opt-in cross-restart replay and downgrade floor. Use one database for one logical trust domain; SQLite safely serializes processes that accidentally share it, but separate hubs should normally have separate files.

Recovery is explicit:

  1. Stop the hub before making a simple filesystem backup, then copy the history database and trust bundle together and preserve owner-only permissions.
  2. Before restore, run sqlite3 capability-card-history.db 'PRAGMA quick_check;' against the stopped copy. Restore the matched history and trust files, then start the hub normally; startup validates them again.
  3. If no valid history backup exists, do not delete the database and continue under the same signing key. Revoke the old key id, enrol a fresh key id, archive the damaged database for audit, and start a new history database. Old cards then fail as revoked instead of becoming replay candidates after a silent reset.
  4. A stock worker's live sequence is still process-local and has no sequence-resume flag. Rotate it to a fresh enrolled key id before restart, or use an external publisher that persists and supplies a monotonically higher sequence. Restarting the stock worker at sequence one under the old key is correctly reported as sequence_mismatch; the hub never guesses a higher value on its behalf.

This is a recovery playbook for advisory provenance state, not a backup service, distributed database, or managed key-delivery system.

Lifecycle and honest limits

  • Card replay/downgrade history is bounded by binding count and time.
  • History uses the in-memory default unless the operator supplies --capability-card-history-db. The SQLite option preserves replay and downgrade floors across hub restarts; it does not persist live capability cards themselves.
  • A worker sequence is in-process. Operators that publish offline cards must persist and increase their own sequence.
  • Credential rotation uses a new key id. Revocation blocks new verification immediately while old projected evidence retains its recorded result.
  • Capability cards themselves remain ephemeral and are forgotten when their live agent disconnects or their ordinary card TTL expires.
  • No enforcement flag exists yet. Durable history and recovery instructions now ship, but enforced admission still needs managed key distribution, an explicit rollout/rollback policy, and a separate owner-approved CLI contract.

Relationship to other controls

  • Identity and ACL decides who may advertise or execute; card signing only authenticates the advertisement.
  • Per-message authentication authenticates selected frames and has its own key/profile/replay state.
  • Signed events and mTLS protect durable events and configured peers; their signatures cannot substitute for card signatures.
  • Sandboxed tools and marketplace uses a signed card as provenance, a permission manifest as requested authority, the WASM runtime as enforcement, and a run receipt as evidence.

Boundaries

Signed capability cards are implemented as advisory tamper evidence. They do not authorize tools, replace message authentication, replace signed events, sandbox agents, validate external A2A conformance, or certify that advertised capabilities work. Runtime marketplace distribution and enforced signed-card admission remain separate, unshipped layers.