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 keygencreates a profile-separated owner-only Ed25519 private key and can enrol its public key into a separate trust bundle;synapse capability-card signsigns strict canonical card JSON and refuses duplicate JSON keys or output-file replacement;synapse capability-card verifyperforms 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-dbopts 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:
- Stop the hub before making a simple filesystem backup, then copy the history database and trust bundle together and preserve owner-only permissions.
- 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. - 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.
- 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.