Metadata-Version: 2.4
Name: atrib
Version: 0.1.0
Summary: atrib client SDK: verifiable agent actions. attest/recall verbs, Ed25519-signed chain-linked records, byte-identical to the TypeScript implementation.
Project-URL: Homepage, https://atrib.dev
Project-URL: Repository, https://github.com/creatornader/atrib
Author-email: atrib <hello@atrib.dev>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: attest,attribution,ed25519,mcp,merkle,recall
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42.0
Requires-Dist: rfc8785>=0.1.4
Requires-Dist: typing-extensions>=4.6
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pdoc>=15.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# atrib (Python SDK)

Python client SDK for [atrib](https://atrib.dev) — verifiable agent
actions. Every action becomes signed context for the next.

This is the first non-TypeScript implementation of the atrib record layer
(spec [§1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#1-attribution-record-format)) and SDK contract (spec [§5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#5-sdk-specification)). The guarantee that matters:
**byte-identity**. Identical inputs produce identical JCS canonical forms
(RFC 8785), Ed25519 signatures over the same bytes, identical record
hashes, propagation tokens, and chain roots as the TypeScript
implementation — verified against the shared conformance corpora in
`spec/conformance/` (1.4 signing + adversarial, 1.2.6 provenance_token,
1.2.3 multi-producer chain composition, 2.6.1 submission validation) and a
cross-implementation determinism harness.

## Install

```bash
pip install atrib          # once published; unpublished today — install from source:
pip install -e python/
```

Dependencies: `cryptography` (Ed25519), `rfc8785` (JCS), `typing_extensions`.

## Usage

```python
from atrib import AtribClient, AttestRef

client = AtribClient()  # key from ATRIB_PRIVATE_KEY / ATRIB_KEY_FILE

result = client.attest(
    {"what": "chose sqlite over postgres", "why_noted": "deployment constraint"},
)

client.attest(
    {"summary": "supersedes the sqlite decision", "reason": "scale changed"},
    ref=AttestRef(kind="revises", record_hash=result.record_hash),
)

history = client.recall(shape="history", limit=10)
client.flush()
```

The record layer is directly importable for hosts that own their pipeline:

```python
from atrib import (
    build_and_sign_emit_record, sign_record, verify_record,
    resolve_chain_root, genesis_chain_root, record_hash_ref,
    encode_token, decode_token, derive_provenance_token,
)
```

## API reference

Everything below is importable from the package root (`from atrib import …`).
The client layer (`AtribClient` and its result dataclasses) sits on top of a
fully exposed record layer; hosts can use either level.

### `AtribClient`

```python
AtribClient(
    *,
    key=...,                  # ResolvedKey | None | unset
    context_id=None,          # str | None (32 lowercase hex)
    anchors=None,             # list[AnchorSpec] | None
    allow_single_anchor=False,  # §2.11.12 rule 3 opt-in
    producer="atrib-sdk-py",  # _local.producer sidecar label (DEFAULT_PRODUCER)
    mirror_write_path=None,   # Path | str | None
    mirror_read_path=None,    # Path | str | None
    env=None,                 # Mapping[str, str] | None (default os.environ)
)
```

Constructor parameters (all keyword-only):

| Parameter | Default | Meaning |
| --- | --- | --- |
| `key` | unset → lazy `resolve_key(env)` ladder | Pre-resolved `ResolvedKey`. Passing the parameter at all (including `None`) skips the ladder; `None` disables signing (pass-through per [§5.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#58-degradation-contract) rule 5). |
| `context_id` | `None` → env discovery | Per-client default context. Env discovery is the [D078](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d078-mcp-servers-honor-atrib_context_id-env-as-context_id-default)/[D083](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d083-harness-session-id-discovery-extends-d078-for-cognitive-primitive-mcp-servers) subset: `ATRIB_CONTEXT_ID`, then `CLAUDE_CODE_SESSION_ID` / `CODEX_THREAD_ID` (UUID stripped of hyphens + lowercased to 32-hex). |
| `anchors` | `None` → `BUILT_IN_DEFAULT_ANCHOR_SET` (two anchors; the atrib-log member honors `$ATRIB_LOG_ENDPOINT`) | [D138](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d138-anchor-plurality-as-the-default-trust-posture) anchor set (`AnchorSpec = str \| Mapping` — bare [§2.6.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#261-submit-entry) endpoint shorthand for `{"url": ...}`, or an `AnchorDescriptor` mapping; `url` wins over `endpoint`). Skip rules mirror the TS `resolveAnchorSet` exactly: hostile shapes and `anchor_type` values outside the [§2.11.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#2118-anchor-type-registry) registry warn-and-skip (never raise) and are excluded from the plurality posture. Attests fan out to **every** usable atrib-log anchor through one non-blocking [§2.6.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#261-submit-entry) queue per endpoint; registered non-atrib-log types (`sigstore-rekor`, `rfc3161-tsa`, `opentimestamps`) count toward plurality but their legs are skipped with a warning — no Python transport yet (the TS reference stubs them too). |
| `allow_single_anchor` | `False` | [§2.11.12](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#21112-producer-side-anchor-posture) rule 3: states a < 2-anchor set is deliberate, silencing the sub-plurality warning and the `_local.anchor_config` sidecar degradation marker. |
| `producer` | `"atrib-sdk-py"` | `_local.producer` mirror-sidecar label ([§5.9](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#59-local-mirror-conventions)). |
| `mirror_write_path` | `default_mirror_write_path(env)` | Where new records are appended. |
| `mirror_read_path` | `default_mirror_read_path(env)` | Shared chain-inheritance read source. |
| `env` | `os.environ` | Injectable environment mapping (for tests and embedded hosts). |

The client is a context manager: `__exit__` flushes the submission queue
with a 5-second deadline and never raises.

#### `attest(content, *, ...) -> AttestResult`

```python
client.attest(
    content,                  # Mapping[str, object] — required, positional
    event_type=None,          # short name or absolute URI; default 'observation'
    ref=None,                 # AttestRef(kind='annotates'|'revises', record_hash=...)
    informed_by=None,         # list[str] of sha256:<64-hex> refs
    context_id=None,          # overrides the client default
    chain_root=None,          # explicit override (requires a context_id)
    provenance_token=None,    # §1.2.6 genesis-only anchor (see below)
    tool_name=None,
    args_hash=None,           # overrides the D099 default sha256(JCS(content))
    result_hash=None,
    timestamp_ms=None,        # clock injection for deterministic tests
)
```

Single write verb; signs in-process via the ported record layer and mirrors
+ submits per [§5.9](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#59-local-mirror-conventions)/[§5.3.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#535-log-submission).
The **only raise paths are contradictory inputs** (`ValueError`): an invalid
`event_type` URI, a `ref.kind` that contradicts an explicit `event_type`, an
unknown `ref.kind`, an `annotates`/`revises` ref that is not
`sha256:<64-hex>`, an annotation/revision `event_type` without the matching
`ref`, an explicit `chain_root` with no resolvable `context_id`, a
`context_id` that is not 32 lowercase hex, or a `provenance_token` on a
context that already has records on the local mirror (the
[§1.2.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#126-provenance_token)
genesis-only invariant). Everything operational degrades:

- No signing key → pass-through result (`via="none"`, `record_hash=None`)
  with a warning.
- No context_id anywhere → a fresh orphan context is synthesized
  ([§1.5.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#151-context_id-the-session-anchor);
  never inherited from the mirror tail) with a warning.
- Signing, mirror-write, or submission-enqueue failures append warnings and
  never raise.

The default `chain_root` comes from `resolve_chain_root` with the mirror
tail (write mirror preferred, then the read source — the `@atrib/emit`
inheritance ordering). The mirror sidecar carries
`{"producer": ..., "content": ...}`; the signed record commits to content
through the default `args_hash` per
[D099](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d099-explicit-emit-records-commit-local-content-through-default-args_hash).

`AttestRef` is a frozen dataclass: `AttestRef(kind, record_hash)` with
`kind` one of `'annotates'` / `'revises'` (deriving event types
`annotation` / `revision`).

`AttestResult` (frozen dataclass): `record_hash` (`sha256:<64-hex>` or
`None`), `context_id`, `via` (`'in-process' | 'none'`), `warnings`
(`atrib:`-prefixed strings), and `anchor_posture` — the resolved
[§2.11.12](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#21112-producer-side-anchor-posture)
posture dict `{"effective_anchor_count", "used_default_set", "warned"}`
(present even in pass-through mode). When a sub-plurality set lacks
`allow_single_anchor`, the mirror sidecar additionally carries the
`_local.anchor_config` degradation marker
`{"configured": <n>, "allow_single_anchor": false}`.

#### `recall(*, shape="history", ...) -> RecallOutcome`

```python
client.recall(
    shape="history",          # 'history' | 'session_chain' in v0
    context_id=None,          # session_chain defaults from the client
    event_type=None,          # short name or URI (normalized)
    creator_key=None,
    limit=10,
    since_ms=None,
    until_ms=None,
    verify_signatures=True,
)
```

Single read verb over the local mirror (read source + write mirror, newest
first). v0 serves the `history` and `session_chain` shapes; any other shape
degrades to `via="none"` with a warning pointing at the TypeScript SDK or
the primitives runtime — never a raise. With `verify_signatures=True`
(default), records failing
[§1.4.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#143-verification-procedure)
verification are dropped and each returned entry carries
`signature_verified`. Entries carry `record_hash`, `event_type`,
`context_id`, `creator_key`, `timestamp`, and `local_content` when the
mirror sidecar kept the content.

`RecallOutcome` (frozen dataclass): `shape`, `via`
(`'in-process' | 'none'`), `data` (`{"total", "returned", "records"}` or
`None`), `warnings`.

#### `flush(deadline_s=30.0) -> None`

Bounded wait for pending log submissions on **every** anchor leg (one
queue per effective atrib-log anchor). Never raises.

### Record layer: signing and verification ([§1.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#14-signing-and-verification))

- `sign_record(record, private_key) -> AtribRecord` —
  [§1.4.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#142-signing-procedure):
  remove `signature`, JCS-serialize, Ed25519-sign with a 32-byte seed,
  base64url. Optional fields keep their exact presence/absence (omission ≠
  null).
- `verify_record(record, *, now_ms=None) -> bool` —
  [§1.4.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#143-verification-procedure),
  all 8 steps (key decode, signature decode, JCS-minus-signature Ed25519
  verify — or the creator's `signers[]` entry over
  [§1.7.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#176-cross-attestation-requirement-for-transaction-records)
  cross-attestation bytes for transaction records — `spec_version`,
  `event_type` URI validity, timestamp not >5 min future, `context_id`
  shape). `now_ms` injects the clock for corpus tests; defaults to wall
  time. Returns `False` on any failure, never raises.
- `sign_transaction_record(record, private_key, counterparty_signers=None) -> AtribRecord` —
  signs the [§1.7.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#176-cross-attestation-requirement-for-transaction-records)
  cross-attestation bytes; the creator's signer entry comes first, followed
  by caller-supplied counterparty entries over the same canonical bytes.
- `sign_transaction_attestation(record, private_key) -> SignerEntry` — one
  counterparty signer entry over an existing transaction record's bytes;
  raises `ValueError` for non-transaction records.
- `get_public_key(private_key) -> bytes` — raw 32-byte Ed25519 public key
  for a 32-byte seed.

### Record construction

- `build_and_sign_emit_record(*, private_key, event_type, context_id, chain_root, content, informed_by=None, provenance_token=None, annotates=None, revises=None, tool_name=None, args_hash=None, result_hash=None, timestamp_ms=None) -> AtribRecord` —
  the port of `@atrib/emit`'s pipeline: synthetic `content_id` from
  `SYNTHETIC_SERVER_URL` + the event-type leaf, the
  [D099](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d099-explicit-emit-records-commit-local-content-through-default-args_hash)
  default `args_hash`, lexicographically sorted `informed_by`, and
  omission-not-null optional fields. Pure aside from signing; no I/O;
  `timestamp_ms` injects the clock.
- `SYNTHETIC_SERVER_URL` — `"mcp://atrib-emit"`, a frozen historical
  constant (embeds the original package name; MUST NOT change, or old
  records stop being content_id-compatible).
- `content_hash(content) -> str` — the
  [D099](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d099-explicit-emit-records-commit-local-content-through-default-args_hash)
  default args commitment: `sha256:<hex(SHA-256(JCS(content)))>`.
- `leaf_of_event_type_uri(uri) -> str` — trailing path segment for
  atrib-namespace URIs; the URI itself for slash-less or trailing-slash
  inputs.

### Chain composition ([§1.2.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#123-chain_root-for-genesis-records) / [§1.2.3.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#1231-multi-producer-chain-composition))

- `genesis_chain_root(context_id) -> str` —
  `"sha256:" + hex(SHA-256(UTF-8(context_id)))`.
- `chain_root(parent_record) -> str` — chain_root for a non-genesis record
  (hash of the parent's canonical signed form).
- `resolve_chain_root(*, context_id, inbound_record_hash_hex=None, auto_chain_tail_hex=None, mirror_tail_hex=None, env=None) -> str` —
  the bit-for-bit [D067](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d067-multi-producer-chain-composition-precedence-contract)
  port. Precedence, highest first: (1) inbound propagation token hex, (2)
  within-process auto-chain tail, (3) `ATRIB_CHAIN_TAIL_<context_id>` env
  var (accepted only as exact `sha256:<64-hex>`; malformed falls through),
  (4) mirror tail (bare 64-hex; caller pre-filters by context_id), (5)
  synthetic genesis. Pure and synchronous; pass a stub `env` in tests.

### Hashes and tokens

- `sha256(data) -> bytes`.
- `record_hash_bytes(record) -> bytes` / `record_hash_hex(record) -> str` /
  `record_hash_ref(record) -> str` — SHA-256 over the JCS-canonical
  COMPLETE signed record including `signature`, as raw bytes, bare 64-hex,
  and `sha256:<64-hex>` reference form respectively.
- `derive_provenance_token(upstream) -> str` —
  [§1.2.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#126-provenance_token):
  base64url (no padding) of the first 16 bytes of the upstream record
  hash; always 22 characters.
- `encode_token(record) -> str` / `decode_token(token) -> DecodedToken | None` —
  the [§1.5.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#152-http-transport-tracestate)
  propagation token (`base64url(record_hash) + "." + base64url(creator_key)`,
  max 87 chars). Decoding is lenient per
  [D018](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d018-w3c-trace-context-and-baggage-conformance-leftmost-atrib-lenient-parse-evict-from-end-on-overflow):
  malformed tokens return `None` (meaning genesis), never raise.
  `DecodedToken` is a frozen dataclass of two 32-byte values.

### Canonicalization ([§1.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#13-canonical-serialization))

- `jcs(value) -> bytes` — RFC 8785 via the `rfc8785` package; raises
  `ValueError` subclasses on non-I-JSON input (see the boundary section
  below).
- `canonical_signing_input(record)` — JCS with `signature` removed
  ([§1.4.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#142-signing-procedure)).
- `canonical_record(record)` — JCS of the complete signed record (the
  record-hash preimage).
- `canonical_cross_attestation_input(record)` — JCS with `signers: []` and
  no top-level `signature`
  ([§1.7.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#176-cross-attestation-requirement-for-transaction-records)).

### Content identity ([§1.2.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#122-content_id-derivation))

- `compute_content_id(server_url, tool_name) -> str` —
  `"sha256:" + hex(SHA-256(UTF-8(normalized + ":" + tool)))`.
- `normalize_server_url(url) -> str` — reproduces the WHATWG-parser
  behaviors the TS implementation relies on (special-scheme slash
  collapsing and required hosts, default-port dropping, opaque-host case
  preservation, trailing-slash trim, query/fragment drop), with the same
  lowercase fallback for unparsable input.

### Submission ([§2.6.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#261-submit-entry) / [§5.3.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#535-log-submission))

- `validate_submission(record, *, now_ms=None) -> ValidationResult` —
  client-side parity with the log's submission checks (spec_version,
  event_type URI, integral non-negative timestamp within the server's
  10-minute future-skew window, context_id shape, required string fields,
  `sha256:<64-hex>` shapes, `session_token`/`informed_by` types).
  `ValidationResult` is a frozen dataclass `(ok, status, error)`.
  `now_ms` injects the clock.
- `SubmissionQueue(log_endpoint=None, *, timeout_s=5.0)` — fire-and-forget
  submitter on a bounded daemon worker thread. `submit(record, priority="normal")`
  never raises; retry is exponential backoff, max 3 attempts inside a
  30-second window; 4xx responses are permanent rejects (dropped);
  transaction records POST with a high `X-atrib-Priority` header. The POST
  body is the bare signed record, never the mirror envelope
  ([§5.9.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#594-submission-path-invariant)).
  `get_proof(record_hash_bare_hex)` reads the proof-bundle cache (keyed by
  bare hex, no `sha256:` prefix). `flush(deadline_s=30.0)` waits, bounded,
  never raises.
- `DEFAULT_LOG_ENDPOINT` — `https://log.atrib.dev/v1/entries`.
- `normalize_log_endpoint(endpoint) -> str` — appends `/v1/entries` to a
  bare origin, mirroring the TS helper.

### Keys ([§5.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#56-key-management))

- `resolve_key(env=None) -> ResolvedKey | None` — the portable subset of
  `@atrib/emit`'s ladder: `ATRIB_PRIVATE_KEY` (base64url 32-byte seed),
  then `ATRIB_KEY_FILE`. The macOS-Keychain and 1Password rungs are
  platform/tool-coupled and intentionally not ported; hosts needing them
  resolve the seed themselves and pass it explicitly. Never raises:
  malformed material degrades to `None` with a stderr warning, and absence
  of a key is pass-through, not an error.
- `ResolvedKey` — frozen dataclass: `private_key` (32-byte seed), `source`
  (`'env' | 'file'`).

### Mirror ([§5.9](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#59-local-mirror-conventions))

- `MirrorLine` — frozen dataclass `(record, sidecar, proof, written_at)`;
  the normalized view of one mirror line.
- `parse_mirror_line(line) -> MirrorLine | None` — tolerates all three
  [§5.9.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#592-the-envelope-shape)
  line shapes; malformed lines yield `None` (skip).
- `read_mirror(path) -> list[MirrorLine]` — missing file → empty list; a
  malformed or non-UTF-8 line is skipped, never fatal.
- `read_mirror_tail(path, context_id=None) -> AtribRecord | None` — newest
  record, optionally filtered by context.
- `mirror_tail_hash_hex(path, context_id) -> str | None` — bare-hex hash of
  the newest same-context record, in the shape
  `resolve_chain_root(mirror_tail_hex=…)` expects.
- `append_mirror_line(path, record, *, sidecar=None, proof=None) -> None` —
  appends an envelope line; best-effort (failures warn on stderr). The
  `_local` sidecar lives at envelope level, never inside `record`, and
  never reaches the public log.
- `default_mirror_write_path(env=None)` — `$ATRIB_MIRROR_FILE`, else
  `~/.atrib/records/atrib-emit-<agent>.jsonl` with `<agent>` from
  `$ATRIB_AGENT` (default `claude-code`).
- `default_mirror_read_path(env=None)` — `$ATRIB_AUTOCHAIN_SOURCE`, then
  `$ATRIB_MIRROR_FILE`, then `~/.atrib/records/<agent>.jsonl`.

### Event types and record shape

- `SPEC_VERSION` — `"atrib/1.0"`.
- `AtribRecord` / `SignerEntry` — `TypedDict` shapes; optional fields use
  `NotRequired` because omission (never null) changes the JCS form and
  signature.
- `EVENT_TYPE_TOOL_CALL_URI`, `EVENT_TYPE_TRANSACTION_URI`,
  `EVENT_TYPE_OBSERVATION_URI`, `EVENT_TYPE_DIRECTORY_ANCHOR_URI`,
  `EVENT_TYPE_ANNOTATION_URI`, `EVENT_TYPE_REVISION_URI` — the six
  normative URIs.
- `normalize_event_type(value) -> str` — short alias / known typo path →
  canonical URI; unknown values pass through unchanged.
- `is_valid_event_type_uri(value) -> bool` —
  [§1.4.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#145-event_type-uri-validation)
  syntactic validation (extension URIs pass; the length limit counts UTF-16
  code units exactly like the JS reference).
- `is_normative_event_type_uri(uri) -> bool` — normative-set membership
  (informational only).
- `event_type_uri_to_byte(uri) -> int` — the
  [§2.3.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#231-entry-serialization)
  byte mapping; extensions map to `0xFF`.

### Anchor plurality ([D138](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d138-anchor-plurality-as-the-default-trust-posture), [§2.11.7-§2.11.13](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#2117-anchors-generalizing-the-replication-target))

`atrib.anchors` is the Python port of the producer-side anchor surface in
`packages/mcp/src/anchors.ts`. Anchoring never touches signed bytes and
never blocks a write; `AtribClient` uses this module for its per-anchor
fan-out (see the constructor table above).

- `ANCHOR_TYPES` — the [§2.11.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#2118-anchor-type-registry)
  v1 registry: `("atrib-log", "sigstore-rekor", "rfc3161-tsa", "opentimestamps")`.
- `BUILT_IN_DEFAULT_ANCHOR_SET` — the two-anchor zero-config default
  ([§2.11.12](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#21112-producer-side-anchor-posture) rule 1),
  value-identical to the TS constant.
- `AnchorDescriptor` / `AnchorSetConfig` — `TypedDict` config shapes
  (`url` wins over `endpoint`; absent `anchor_type` means `atrib-log`).
- `resolve_anchor_posture(config) -> AnchorPostureResolution` — the pure
  [§2.11.12](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#21112-producer-side-anchor-posture) precedence rules (default set / ≥ 2 as given /
  `allow_single_anchor` opt-in / warn + sidecar marker). Field names match
  the conformance corpus (`spec/conformance/2.11/anchors/`). Never raises.
- `resolve_effective_anchors(config) -> list[AnchorDescriptor]` — the
  effective set a producer submits to.
- `anchor_claim_artifact(record_hash) -> bytes` — the
  [§2.11.10](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#21110-the-anchoring-signature-claim-artifact)
  claim bytes `UTF-8("atrib-anchor/v1:" + record_hash)`, byte-identical to
  the TS `anchorClaimArtifact`; raises `ValueError` on a malformed hash
  (pure builder for programmer input).
- `ANCHOR_CLAIM_PREFIX` — `"atrib-anchor/v1:"`.

### Attribution receipts ([D141](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d141-devatribattribution-first-class-mcp-extension-sep-2133), `dev.atrib/attribution` v0.1)

Consumer-side receipt handling for the MCP extension (see
`docs/extensions/dev.atrib-attribution/`). Receipts are advisory — trust
derives from verifying signed records, never from the receipt.

- `ATTRIBUTION_EXTENSION_KEY` — `"dev.atrib/attribution"`, the `_meta` key.
- `ATTRIBUTION_LOG_SUBMISSION_STATUSES` — `("queued", "submitted",
  "disabled", "failed")`; a queue status, never an awaited proof
  ([§5.3.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#535-log-submission)).
- `parse_attribution_receipt_block(meta) -> AttributionReceiptBlock | None`
  — lenient extraction from a tool result's `_meta`: anything malformed
  yields `None`, never a raise; wrong-typed fields drop.
- `verify_attribution_receipt(block) -> AttributionReceiptVerification` —
  the extension-spec [§6.2](https://github.com/creatornader/atrib/blob/main/docs/extensions/dev.atrib-attribution/v0.1.md#62-receipt-block) consumer check over the RAW result block
  (structural well-formedness plus token ↔ receipt ↔ optional-record
  internal consistency), the exact port of `@atrib/mcp`'s
  `verifyAttributionReceipt`; hostile input yields
  `mismatched=["malformed"]`. Internal consistency only; Tier-3 assurance
  additionally verifies the attached record (`verify_record`) and log
  inclusion.
- `check_attribution_receipt_consistency(block, record=None) ->
  AttributionReceiptConsistency` — record-side consistency of a parsed
  block against the signed record it names; no record at all →
  `receipt_valid=False, mismatched_fields=["record"]` (conservative).

### Evidence envelopes ([D137](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d137-universal-evidence-envelope-as-the-single-protocol-level-attachment-model), [§5.5.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#557-universal-evidence-envelope))

`atrib.evidence` implements the universal envelope — the single
attachment model for external evidence (the legacy `protocol` string set
is frozen). Envelopes live outside signed bytes. Conformance:
`spec/conformance/evidence-envelope/` (all case families).

- `EVIDENCE_TIERS` — `("declared", "shape", "attested", "verified")`;
  `EVIDENCE_REF_KINDS`, `EVIDENCE_CONSTRAINT_STATUSES` — the other closed
  vocabularies.
- `EvidenceEnvelope`, `EvidencePayload`, `EvidencePayloadRef`,
  `EvidenceConstraint`, `EvidenceResult`, `EvidenceVerifier` — `TypedDict`
  shapes matching the TS `EvidenceEnvelope` family field-for-field.
- `evidence_envelope_key(envelope) -> str` — dedup identity
  `(profile, payload.hash)`; `evidence_tier_rank(tier) -> int` — `0`
  (declared) … `3` (verified), unknown tiers `-1`.
- `validate_envelope(envelope) -> EnvelopeValidation` /
  `is_valid_envelope(envelope) -> bool` — the normative [§5.5.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#557-universal-evidence-envelope) shape
  rules, reason-for-reason with the TS validator (closed reason-code set,
  e.g. `profile_uri`, `payload_hash`, `inline_without_inline_kind`).
  Rejecting an envelope never rejects the record it attaches to.
- `build_evidence_envelope(*, profile, tier, ...) -> (envelope | None,
  warnings)` — producer-side builder: computes `payload.hash` from
  `payload_material` (JCS rule, `jcs_sha256`) or `payload_material_utf8`
  (raw rule, `raw_sha256`), fills the default `result`, validates, and
  drops the envelope (never the record) on rejection. Raises `ValueError`
  only on contradictory input.
- `classify_profile(uri, registry=ATRIB_PROFILE_REGISTRY) ->
  ProfileClassification` — full-URI profile identity; a foreign domain
  reusing an atrib profile name is a valid third-party profile, never the
  atrib profile.
- `map_legacy_evidence_block(block) -> EvidenceEnvelope` (spec-named alias
  `from_legacy_evidence_block`) — the deterministic legacy [§5.5.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#556-generic-authorization-evidence-blocks) mapping
  through the frozen five-row table (`FROZEN_LEGACY_PROTOCOLS`,
  `LEGACY_PROTOCOL_TO_PROFILE`); unknown protocols raise `ValueError`
  (the one normative MUST-reject).
- `order_envelope_instances(instances)` — tier desc, `checked_at_ms`
  desc, verifier name asc; `is_relay_identity_swap(original, relayed)` —
  flags a verifier-block-only difference; `assess_reproducibility(envelope)`
  — `verified` + `ref.kind: "withheld"` is well-formed but
  claimed-not-reproducible; `render_envelope_opaque(envelope)` — the
  unknown-profile preservation surface (never drop, never affect record
  validity).
- `jcs_sha256(value)` / `raw_sha256(text)` — the two [§5.5.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#557-universal-evidence-envelope) payload hash
  rules.

### Encoding

- `base64url_encode(data)` / `base64url_decode(value)` — RFC 4648 §5, no
  padding; decode raises `ValueError` on malformed input.
- `hex_encode(data)` / `hex_decode(value)` — lowercase hex.

## Contracts honored

- **[§5.8](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#58-degradation-contract) degradation**: operational failures never raise and never block —
  missing key means pass-through, network failures are silent with bounded
  retry (3 attempts / 30s window), all warnings carry the `atrib:` prefix.
  The only raise paths are contradictory inputs.
- **[§1.2.3.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#1231-multi-producer-chain-composition) chain composition**: `resolve_chain_root` is a bit-for-bit
  port of the [D067](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d067-multi-producer-chain-composition-precedence-contract) reference (`packages/mcp/src/chain-root.ts`), tested
  against `spec/conformance/1.2.3/multi-producer/`.
- **[§5.9](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#59-local-mirror-conventions) mirror conventions**: JSONL envelopes under `~/.atrib/records/`,
  all three line shapes tolerated on read, `_local` sidecar never enters
  signed bytes or the public log.
- **[§2.6.1](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#261-submit-entry) submission**: bare signed record POST, priority header,
  idempotent-safe retry, proof bundles cached by record hash.

## Scope (v0)

The write verb (`attest`) signs in-process; `recall` covers the
history/session_chain shapes over the local mirror. Anchor plurality
([D138](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d138-anchor-plurality-as-the-default-trust-posture))
fans out to every usable atrib-log anchor; registered non-atrib-log
anchor types count toward the posture but have no Python transport yet
(their legs are skipped with a warning — the TS reference stubs them
too). `atrib.evidence` implements the
[D137](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d137-universal-evidence-envelope-as-the-single-protocol-level-attachment-model)
envelope surface (validation, builder, legacy mapping, tier semantics)
and `atrib.attribution` the
[D141](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d141-devatribattribution-first-class-mcp-extension-sep-2133)
receipt checks. Daemon-first transport
(Streamable HTTP to the local primitives runtime) lands with the
post-2026-07-28 stateless MCP transport rather than reimplementing the
current initialize-handshake session protocol. Summarize is not an SDK
verb — synthesis belongs to the calling harness.

## Known cross-implementation boundary (I-JSON)

Content outside I-JSON (RFC 7493) — integers beyond 2^53-1 or strings with
lone UTF-16 surrogates — cannot round-trip between JS and Python: the JS
runtime canonicalizes such content (lossily for big integers, because JS
numbers are already doubles) while this SDK's RFC 8785 implementation
rejects it with a `ValueError`. Rejection is deliberate: silently
reproducing JS precision loss would corrupt caller data, and a commitment
only one implementation can reproduce is worse than an error. Keep attest
`content` within I-JSON. The rejection contract is pinned in
`tests/test_port_parity.py`.

## Tests

```bash
pip install -e "python/[dev]"
pytest python/tests
mypy  # configured in pyproject.toml
```

Generated API documentation (from the inline docstrings) uses
[pdoc](https://pdoc.dev), included in the `[dev]` extras:

```bash
python -m pdoc atrib -o docs-build/   # static HTML
python -m pdoc atrib                  # live server
```

CI runs the same suites plus the cross-implementation determinism judge on
Python 3.10 and 3.12 via `.github/workflows/python-sdk.yml`, path-scoped to
the byte-identity surface (`python/`, `packages/sdk/`, `packages/mcp/`,
`services/atrib-emit/`, `spec/conformance/`). The judge step fails loudly
if it would silently skip.

The conformance corpora are consumed unmodified from `spec/conformance/`;
a port failure is a spec-bug discovery, not something to route around.
