Metadata-Version: 2.4
Name: synpareia
Version: 0.7.0
Summary: Cryptographic primitives for AI agent identity, attestation, and interaction verification
Project-URL: Homepage, https://synpareia.com
Project-URL: Repository, https://github.com/synpareia/synpareia
Project-URL: Issues, https://github.com/synpareia/synpareia/issues
Project-URL: Changelog, https://github.com/synpareia/synpareia/blob/main/CHANGELOG.md
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cryptography>=50.0.0
Requires-Dist: http-message-signatures>=2.0
Requires-Dist: idna>=3.15
Requires-Dist: rfc8785>=0.1.4
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
Requires-Dist: fastapi>=0.115; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=9.0.3; extra == 'dev'
Requires-Dist: ruff>=0.9; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Requires-Dist: starlette>=1.3.1; extra == 'dev'
Provides-Extra: profile
Requires-Dist: httpx>=0.27; extra == 'profile'
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.20; extra == 'sqlite'
Provides-Extra: witness
Requires-Dist: httpx>=0.27; extra == 'witness'
Description-Content-Type: text/markdown

# synpareia

Cryptographic primitives for AI agent identity, attestation, and interaction verification.

Synpareia gives AI agents a persistent, verifiable identity and a tamper-evident history of their interactions. It works locally with zero network dependencies, zero accounts, and a single `pip install`.

## Is this the right package for you?

**Probably not, if you just want an agent to start using synpareia.** This is the foundation
layer — blocks, chains, anchors, commitments, witness clients, signed envelopes. It gives you
building blocks, deliberately without imposing a flow.

What most people actually want is the layer above it: **making commitments that mean something,
vetting a counterparty before relying on them, and recording an interaction so either side can
prove their account later.** That is
[`synpareia-trust-mcp`](https://pypi.org/project/synpareia-trust-mcp/) — the Trust Toolkit MCP
server, which wraps this SDK with runtime-ready ergonomics and names its tools after the social
act rather than the mechanism.

> **Installing the toolkit today:** the latest `synpareia-trust-mcp` on PyPI is **0.8.0**, which
> declares `mcp[cli]>=1.0` with no upper bound. `mcp` 2.0.0 (2026-07-28) moved
> `mcp.server.fastmcp`, so an unpinned install resolves 2.0.0 and the server fails on import.
> Install it as `pip install synpareia-trust-mcp 'mcp<2'` (or `uvx --with 'mcp<2'
> synpareia-trust-mcp`) **for as long as 0.8.0 is the latest release**. The bound ships in
> 0.9.0; once `pip index versions synpareia-trust-mcp` shows 0.9.0 or newer, drop the pin and
> this note. This SDK is unaffected.

**Reach for this SDK when you are building the thing agents use**, not using it: a framework
adapter (CrewAI, LangGraph, a custom platform), another MCP server, your own agent runtime, or
verification embedded in a service of your own. Come back here when the toolkit's ergonomics
stop fitting.

## Install

```bash
pip install synpareia
```

Requires Python 3.11+. Four runtime dependencies: `cryptography` (signing),
`http-message-signatures` (RFC 9421 request auth), `rfc8785` (JCS canonicalisation),
`idna`. Tiers 1–3 exercise only the first three; nothing here pulls in a web framework.
Network access needs an extra — `pip install "synpareia[witness]"` or `[profile]`, both
of which add `httpx`.

("Only dependency: `cryptography`" was true when the tier table below was written and
stopped being true as soon as signed requests and canonical JSON arrived.)

### Verify you have the SDK

```python
import synpareia
assert synpareia.__version__ >= "0.2.0"
assert hasattr(synpareia, "generate"), "you have a different 'synpareia' in your venv"
```

If `hasattr(synpareia, "generate")` is False, something else in your environment is shadowing the SDK. Use a fresh virtualenv (`python -m venv .venv && source .venv/bin/activate && pip install synpareia`) and try again.

## Quick Start

### Create an identity

Every agent gets an Ed25519 keypair. The identity is derived deterministically from the public key — no server, no registration.

```python
import synpareia

# Generate a new agent identity
profile = synpareia.generate()
print(profile.id)  # did:synpareia:a1b2c3...
```

### Sign a block

A block is the atomic unit: a typed, hashed, signed record of something that happened.

```python
block = synpareia.create_block(
    profile,
    type="message",
    content="Hello from Agent A",
)

# Anyone with the public key can verify authorship
assert synpareia.verify_block(block, profile.public_key)
```

### Build a chain

A chain is an ordered, hash-linked sequence of blocks — a tamper-evident history.

```python
# Create a Chain of Presence (personal history).
# Every chain carries a policy — templates.cop() is the personal-history shape.
chain = synpareia.create_chain(profile, policy=synpareia.templates.cop(profile))

# Append blocks
another_block = synpareia.create_block(profile, type="message", content="Hello again")
pos1 = synpareia.append_block(chain, block)
pos2 = synpareia.append_block(chain, another_block)

# Verify the chain is intact. Verification fails closed: signed blocks
# require the authors' public keys.
valid, errors = synpareia.verify_chain(chain, public_keys={profile.id: profile.public_key})
assert valid
```

### Export and verify independently

Chains are portable. Export them as JSON, send them to anyone, and they can verify independently.

```python
from synpareia import export_chain, verify_export

# Export
data = export_chain(chain)

# Anyone with the authors' public keys can verify — no network, no trust, just math
valid, errors = verify_export(data, public_keys={profile.id: profile.public_key})
assert valid
```

### Link chains with anchors

When an agent participates in a shared conversation, anchors link their personal chain to the shared record.

```python
# A peer keeps the shared record on their own chain...
peer = synpareia.generate()
shared_chain = synpareia.create_chain(peer, policy=synpareia.templates.cop(peer))
shared_block = synpareia.create_block(peer, type="message", content="shared record")
synpareia.append_block(shared_chain, shared_block)

# ...and Agent A's personal chain references a position in the shared chain
anchor_block, anchor_pos = synpareia.create_anchor_block(
    profile,
    chain,
    target_chain_id=shared_chain.id,
    target_sequence=2,
    target_block_hash=shared_block.content_hash,
    anchor_type="correspondence",
)
```

### Commit-reveal for independent evaluation

Two agents can independently commit to assessments before revealing them — proving neither was influenced by the other.

```python
# Agent commits (keeps nonce secret)
commitment_block, nonce = synpareia.create_commitment_block(
    profile,
    content=b"My honest assessment: excellent interaction",
)

# Later, reveal and verify (the commitment hash is the block's content)
assert synpareia.verify_commitment(
    commitment_block.content,
    b"My honest assessment: excellent interaction",
    nonce,
)
```

## Core Concepts

### Four Primitives

| Primitive | Purpose | Example |
|-----------|---------|---------|
| **Block** | Atomic signed record | A message, thought, reaction, or system event |
| **Chain** | Ordered, hash-linked sequence of blocks | An agent's personal history, a conversation log |
| **Anchor** | Cross-chain reference | "This block in my chain corresponds to that block in theirs" |
| **Seal** | Third-party attestation (Tier 4) | A witness timestamps or checkpoints a chain |

### Chain of Presence (CoP)

An agent's personal, append-only, hash-linked history across interactions and platforms. Like a cryptographic resume — verifiable by anyone, controlled by the agent.

### Spheres

When two or more agents interact, the shared observable history is a sphere chain. Each agent's CoP links to the sphere via anchors, creating a verifiable record of who said what, and when.

## What You Can Verify

| Claim | How |
|-------|-----|
| "Agent X authored this content" | Ed25519 signature on the block |
| "This content existed at time T" | Block timestamp + chain position |
| "This history hasn't been tampered with" | Hash-linked chain verification |
| "Agent X participated in conversation Y" | Anchor from CoP to sphere chain |
| "Both assessments were independent" | Commit-reveal: both commitments precede both reveals |
| "This chain export is authentic" | `verify_export()` — offline, no service in the loop; the verifier supplies the authors' public keys |

## API Reference

### Identity

| Function | Description |
|----------|-------------|
| `generate()` | Create a new Ed25519 keypair and Profile |
| `from_private_key(bytes)` | Reconstruct Profile from private key |
| `from_public_key(bytes)` | Create a verify-only Profile |
| `load(pub_b64, priv_b64?)` | Load Profile from base64-encoded keys |

### Blocks

| Function | Description |
|----------|-------------|
| `create_block(profile, type, content)` | Create a signed block |
| `verify_block(block, public_key?)` | Verify content hash and signature. **Fails closed**: with a key, the block must carry a signature that verifies against it; with no key, it's an explicit structure-only check (signed blocks fail) |
| `reveal_block(block, content)` | Reveal content of a hash-only block |

### Chains

| Function | Description |
|----------|-------------|
| `create_chain(profile, *, policy=...)` | Create a new chain. A `policy` is required — use `templates.cop(profile)` for a personal chain, `templates.sphere(a, b)` for a two-party sphere, `templates.audit(...)` for oversight. |
| `append_block(chain, block)` | Append a block, returns ChainPosition |
| `verify_chain(chain, *, public_keys=...)` | Walk hash links, enforce chain policy, and verify Ed25519 signatures. **Fails closed** if signatures are present but `public_keys` is omitted — structure-only callers should use `verify_chain_structure` instead. |
| `verify_chain_structure(chain)` | Structural validation only (no signatures, no policy). Explicit opt-in. |
| `export_chain(chain)` | Export as portable, verifiable JSON |
| `verify_export(data, *, public_keys=...)` | Verify an export without the original chain. **Fails closed** the same way `verify_chain` does: exports do not carry public keys, so omitting them returns `(False, [...])` rather than reporting an unchecked chain as sound. |

### Anchors

| Function | Description |
|----------|-------------|
| `create_anchor_block(profile, chain, ...)` | Create a cross-chain reference |
| `verify_anchor(anchor, source, target)` | Verify anchor references are valid |

### Commitments

| Function | Description |
|----------|-------------|
| `create_commitment(content, nonce?)` | Create a commitment hash |
| `verify_commitment(hash, content, nonce)` | Verify a commitment reveal |
| `create_commitment_block(profile, content)` | Create a commitment as a block |

### Chain policy

A chain carries a `Policy` that declares who the signatories are, what
block types are permitted, and how the chain transitions through
lifecycle states (Proposed → Pending → Active → Concluded). Use the
`templates` module for the canonical three shapes:

```python
from synpareia import create_chain, templates, verify_chain_policy

alice = synpareia.generate()
chain = create_chain(alice, policy=templates.cop(alice))  # Chain of Presence (single-party)

# For a two-party sphere:
bob = synpareia.generate()
sphere = create_chain(alice, policy=templates.sphere(alice, bob))

# Standalone policy validation (no signature check):
valid, errors = verify_chain_policy(sphere)
```

`verify_chain` invokes `verify_chain_policy` internally on every
verification — callers get policy enforcement (permitted block types,
signatory constraints, lifecycle state) by default.

### Multi-party block proposals

A `BlockProposal` is a shareable draft of a block that multiple parties
sign before it becomes a full `Block`. Use when two or more agents need
to jointly commit to a statement that none of them can later disavow.

```python
from synpareia import start_proposal, sign_proposal, assemble_block

# Alice drafts a block that requires both Alice and Bob's signatures.
proposal = start_proposal(
    alice,
    type="message",
    content="Alice and Bob agree: integration passed.",
    required_signers={alice.id, bob.id},
)

# Each party signs in turn.
proposal = sign_proposal(proposal, alice)
proposal = sign_proposal(proposal, bob)

# Assembly verifies every signature before producing the final block.
# public_keys is required — assemble_block refuses to produce an
# unverified block.
block = assemble_block(
    proposal,
    public_keys={alice.id: alice.public_key, bob.id: bob.public_key},
)

# The resulting block carries Alice as the primary signer and Bob in
# co_signatures; both bindings are committed to by Alice's signature.
```

### Threshold commitments (XOR n-of-n)

Reveal-requires-cooperation primitive. Two or more parties each
contribute a random share; the joint nonce is the XOR of all shares.
No single party can reveal the committed content without the others'
cooperation, because none of them knows the joint nonce.

```python
from synpareia import (
    create_threshold_commitment,
    random_shares,
    verify_threshold_commitment,
    xor_shares,
)

content = b"Alice and Bob's sealed assessment"

# Each party holds their own 32-byte share (generated together here for brevity).
alice_share, bob_share = random_shares(2)

# Commit jointly — neither party alone can open this.
commitment, joint_nonce = create_threshold_commitment(content, [alice_share, bob_share])

# Later, to reveal: both shares are contributed.
assert xor_shares([alice_share, bob_share]) == joint_nonce
assert verify_threshold_commitment(commitment, content, [alice_share, bob_share])
```

`create_threshold_commitment` rejects share-sets that XOR to an
all-zero joint nonce (catches duplicate shares or accidentally
collapsed entropy). `random_shares` enforces a 16-byte minimum
share length.

### Types

`BlockType`, `ChainType`, `AnchorType`, `ContentMode` — extensible enums for all standard types.

### Storage

`MemoryStore` (default), `ChainStore` protocol for custom backends.

### Directory client (network — `pip install "synpareia[profile]"`)

`ProfileClient` is the async client for the synpareia directory; `SyncProfileClient`
is the same surface for non-async callers. Every mutating call is signed with RFC 9421
HTTP Message Signatures using your Ed25519 key, so the directory authenticates you
without an API key.

```python
from synpareia.identity import generate
from synpareia.profile import SyncProfileClient

me = generate()

with SyncProfileClient("https://synpareia.com") as directory:
    view = directory.get_existence(did=me.id)
    print(view["exists"])
```

`publish`, `get_existence`, `get_history`, `get_well_known`, `delete_history_version`,
`delete_profile`, `request_witness_anchor`, `submit_event`, `get_reputation`. The last
two are new in 0.7.0: `submit_event` posts a signed, content-less topology event
(`/api/v2/events`, live), and `get_reputation` reads the asker-anchored aggregate.

**`get_reputation` is not yet served by the hosted directory.** The route exists in the
directory's source tree but is not in the deployed image, so calling it against
`synpareia.com` returns 404 today rather than the documented `confidence: 0.0`. Point
`base_url` at a directory built from a tree that mounts it, or wait for the deploy.

## Design Choices

- **Ed25519** for signatures — fast, small, deterministic, safe-by-default
- **SHA-256** for hashing — universal, well-audited, interoperable
- **JCS (RFC 8785)** for canonicalization — deterministic JSON serialization for reproducible hashes
- **Frozen dataclasses** — immutable primitives, zero framework dependency
- **Length-prefixed commitments** — prevents separator collision in commit-reveal payloads
- **Constant-time comparison** — timing-safe commitment verification via `hmac.compare_digest`

## Tiers

The SDK is structured in tiers of increasing dependency:

| Tier | What | Dependencies |
|------|------|-------------|
| **1-3** | Blocks, Chains, Anchors | `cryptography`, `rfc8785`, `http-message-signatures` |
| **4** | Witness seals, liveness | + `httpx` (`synpareia[witness]`) |
| **5** | Reputation, directory | + `httpx` (`synpareia[profile]`) + a directory to talk to |

Tiers 1-3 work entirely offline. No server, no account, no network.

Tier 5 said "(coming)" until 0.7.0. It ships now — `ProfileClient.submit_event` and
`ProfileClient.get_reputation`, documented above — with the caveat that the hosted
directory serves the write route and not yet the read one.

## License

Apache 2.0
