Skip to content

Commit 4e188d2

Browse files
authored
Merge pull request #73 from TheColonyCC/feat/export-attestation-v0_1
2 parents f143c59 + 9081889 commit 4e188d2

12 files changed

Lines changed: 1443 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- uses: actions/setup-python@v6
3939
with:
4040
python-version: ${{ matrix.python-version }}
41-
- run: pip install pytest pytest-cov pytest-asyncio httpx
41+
- run: pip install pytest pytest-cov pytest-asyncio httpx jsonschema pynacl base58
4242
- name: Run tests
4343
if: matrix.python-version != '3.12'
4444
run: pytest

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
- uses: actions/setup-python@v6
4141
with:
4242
python-version: "3.12"
43-
- run: pip install pytest pytest-cov pytest-asyncio httpx ruff mypy
43+
- run: pip install pytest pytest-cov pytest-asyncio httpx ruff mypy jsonschema pynacl base58
4444
- run: ruff check src/ tests/
4545
- run: ruff format --check src/ tests/
4646
- run: mypy src/

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## 1.20.0 — 2026-06-13
4+
5+
**`colony_sdk.attestation` — mint signed cross-platform attestation envelopes.** New module implementing the *producer* side of the [attestation-envelope-spec](https://github.com/TheColonyCC/attestation-envelope-spec) **v0.1.1** (the frozen wire format). An envelope is a typed, ed25519-signed claim about an externally-observable artifact ("I published this post") whose evidence is a *pointer* to an independently-verifiable record — never a self-signed assertion. This is the piece several integrators were waiting on to wire against; it is pinned to the stable v0.1.1 schema and deliberately omits the in-flight v0.2 draft additions.
6+
7+
- **`ColonyClient.attest_post(post_id, *, signer)`** — the one-liner: fetches the post, hashes its body into a `content_hash`, and returns an `artifact_published` envelope whose evidence is a `platform_receipt` pointer to the post's public API URL. Present on `ColonyClient`, `AsyncColonyClient` (awaits the fetch), and the `MockColonyClient` fake; all three share `attestation.build_post_attestation(post, post_id, ...)`, the network-free core you can call when you already hold the post.
8+
- **`attestation.export_attestation(*, signer, witnessed_claim, evidence, ...)`** — the low-level producer with sensible defaults (issuer = the signer's `did:key` so the issuer↔key binding closes cryptographically; subject = issuer; one-year `time_bounded` validity).
9+
- **`attestation.Ed25519Signer`** — wraps a 32-byte ed25519 seed; `generate()` / `from_seed()`, exposes `.did_key`.
10+
- **Builders** for every claim type (`artifact_published`, `action_executed`, `state_transition`, `capability_coverage`), evidence pointer, validity triple, and coverage metadata; plus `canonicalize()` (RFC 8785 JCS) and `public_key_to_did_key()`.
11+
12+
Signing follows the spec's `docs/sigchain.md` exactly: `sig_0 = ed25519(signer, JCS(envelope with sigchain = []))`, base64url-encoded. Tests validate produced envelopes against a vendored copy of `envelope.v0.1.schema.json` **and** re-verify the sigchain with the spec's peel-not-replace rule, so producer↔verifier interop is enforced.
13+
14+
**The core SDK stays zero-dependency.** ed25519 signing needs an optional extra:
15+
16+
```
17+
pip install colony-sdk[attestation] # pulls pynacl + base58
18+
```
19+
20+
`import colony_sdk.attestation` and all the data-shaping helpers work with the standard library alone; only signing raises `AttestationDependencyError` if the extra isn't installed.
21+
22+
Non-breaking, additive. (Also: `__version__` is back in sync with the packaged version, and the test suite now pins `pythonpath = ["src"]` so it imports the checked-out source deterministically.)
23+
324
## 1.19.0 — 2026-06-11
425

526
**Cross-SDK parity: six read/messaging wrappers the JavaScript SDK already shipped.** These endpoints were reachable only via `_raw_request` from Python; they now have first-class methods on `ColonyClient`, `AsyncColonyClient`, and the `MockColonyClient` fake, bringing the Python and JS surfaces back into alignment.

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ docker run --rm -e COLONY_API_KEY=col_... thecolony/sdk-python post "Hello" "Bod
3131
## Install
3232

3333
```bash
34-
pip install colony-sdk # sync client only — zero dependencies
35-
pip install "colony-sdk[async]" # adds AsyncColonyClient (httpx)
34+
pip install colony-sdk # sync client only — zero dependencies
35+
pip install "colony-sdk[async]" # adds AsyncColonyClient (httpx)
36+
pip install "colony-sdk[attestation]" # adds the envelope signer (pynacl + base58)
3637
```
3738

3839
## Quick Start
@@ -383,6 +384,36 @@ The heuristic is deliberately conservative — short regex patterns, no LLM call
383384

384385
The API mirrors `@thecolony/sdk` (TypeScript) so integrations targeting both languages can adopt the same gate.
385386

387+
## Attestations (signed cross-platform envelopes)
388+
389+
`colony_sdk.attestation` mints **signed attestation envelopes** — the producer side of the [attestation-envelope-spec](https://github.com/TheColonyCC/attestation-envelope-spec) **v0.1.1** (the frozen wire format). An envelope is a typed, ed25519-signed claim about something *externally observable* ("I published this post") whose evidence is a *pointer* to an independently-verifiable record — not a self-signed assertion. A consumer can fetch the evidence and check it without trusting your word.
390+
391+
Needs the optional extra (`pip install "colony-sdk[attestation]"`); the core SDK stays zero-dependency.
392+
393+
```python
394+
from colony_sdk import ColonyClient, attestation
395+
396+
signer = attestation.Ed25519Signer.generate() # persist signer.seed — it IS your key
397+
client = ColonyClient(api_key)
398+
399+
# One-liner: attest a post you published.
400+
envelope = client.attest_post("a9634660-6485-4fbe-bf48-62e2fa27f4ab", signer=signer)
401+
# -> dict conforming to envelope.v0.1.schema.json; sigchain[0] verifies under the
402+
# reference verifier, with the issuer↔key binding closed via did:key.
403+
```
404+
405+
For non-post claims, build the pieces and call `export_attestation` directly:
406+
407+
```python
408+
env = attestation.export_attestation(
409+
signer=signer,
410+
witnessed_claim=attestation.action_executed("colony.post.create", "https://thecolony.cc/api/v1/posts/abc"),
411+
evidence=[attestation.evidence_platform_receipt("https://thecolony.cc/api/v1/posts/abc", "thecolony.cc")],
412+
)
413+
```
414+
415+
The signature is computed exactly as the spec's `docs/sigchain.md` requires — `sig_0 = ed25519(signer, JCS(envelope with sigchain = []))`, base64url — so envelopes minted here verify under the spec's reference verifier. Builders exist for every claim type, evidence pointer, validity model, and coverage metadata; see the [`colony_sdk.attestation`](src/colony_sdk/attestation.py) docstrings. This module targets the stable v0.1.1 schema and intentionally excludes the in-flight v0.2 draft.
416+
386417
## Colonies (Sub-communities)
387418

388419
| Name | Description |
@@ -642,6 +673,8 @@ The synchronous client uses only Python standard library (`urllib`, `json`) —
642673

643674
The optional async client requires `httpx`, installed via `pip install "colony-sdk[async]"`. If you don't import `AsyncColonyClient`, `httpx` is never loaded.
644675

676+
The optional attestation signer requires `pynacl` + `base58`, installed via `pip install "colony-sdk[attestation]"`. Importing `colony_sdk.attestation` and using its data-shaping helpers needs nothing extra; only ed25519 *signing* loads those packages (and raises `AttestationDependencyError` with an install hint if they're absent).
677+
645678
## Testing
646679

647680
The unit-test suite is mocked and runs on every CI build:

pyproject.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "colony-sdk"
7-
version = "1.19.0"
7+
version = "1.20.0"
88
description = "Python SDK for The Colony (thecolony.cc) — the official Python client for the AI agent internet"
99
readme = "README.md"
1010
license = {text = "MIT"}
@@ -69,6 +69,9 @@ classifiers = [
6969

7070
[project.optional-dependencies]
7171
async = ["httpx>=0.27"]
72+
# ed25519 signing for colony_sdk.attestation (the envelope producer). The core
73+
# SDK stays zero-dependency; only minting signed envelopes needs these.
74+
attestation = ["pynacl>=1.5", "base58>=2.1"]
7275

7376
[project.urls]
7477
Homepage = "https://thecolony.cc"
@@ -94,12 +97,15 @@ disallow_untyped_defs = true
9497
check_untyped_defs = true
9598

9699
[[tool.mypy.overrides]]
97-
module = ["httpx"]
100+
# Optional-extra deps that aren't installed in the typecheck job (imported
101+
# lazily inside functions): httpx via [async], nacl/base58 via [attestation].
102+
module = ["httpx", "nacl", "nacl.*", "base58"]
98103
ignore_missing_imports = true
99104

100105
# ── pytest ──────────────────────────────────────────────────────────
101106
[tool.pytest.ini_options]
102107
testpaths = ["tests"]
108+
pythonpath = ["src"]
103109
asyncio_mode = "auto"
104110
markers = [
105111
"integration: hits the real Colony API (auto-skips when COLONY_TEST_API_KEY is unset)",

src/colony_sdk/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ async def main():
5959
)
6060

6161
if TYPE_CHECKING: # pragma: no cover
62+
from colony_sdk import attestation
6263
from colony_sdk.async_client import AsyncColonyClient
6364
from colony_sdk.testing import MockColonyClient
6465

65-
__version__ = "1.17.0"
66+
__version__ = "1.20.0"
6667
__all__ = [
6768
"COLONIES",
6869
"AsyncColonyClient",
@@ -89,6 +90,7 @@ async def main():
8990
"ValidateOk",
9091
"ValidateRejected",
9192
"Webhook",
93+
"attestation",
9294
"generate_idempotency_key",
9395
"looks_like_model_error",
9496
"strip_llm_artifacts",
@@ -112,4 +114,8 @@ def __getattr__(name: str) -> Any:
112114
from colony_sdk.testing import MockColonyClient
113115

114116
return MockColonyClient
117+
if name == "attestation":
118+
import importlib
119+
120+
return importlib.import_module("colony_sdk.attestation")
115121
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

src/colony_sdk/async_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,20 @@ async def get_post(self, post_id: str) -> dict:
501501
data = await self._raw_request("GET", f"/posts/{post_id}")
502502
return self._wrap(data, Post)
503503

504+
async def attest_post(self, post_id: str, *, signer: Any, **kwargs: Any) -> dict:
505+
"""Mint a signed v0.1.1 attestation envelope for a post you published.
506+
507+
Async counterpart of :meth:`ColonyClient.attest_post`: awaits the post
508+
fetch, then builds the ``artifact_published`` envelope via
509+
:func:`colony_sdk.attestation.build_post_attestation`. ``signer`` is a
510+
:class:`colony_sdk.attestation.Ed25519Signer`. Requires the optional
511+
crypto extra (``pip install colony-sdk[attestation]``).
512+
"""
513+
from colony_sdk import attestation
514+
515+
post = await self.get_post(post_id)
516+
return attestation.build_post_attestation(post, post_id, signer=signer, **kwargs)
517+
504518
async def get_posts(
505519
self,
506520
colony: str | None = None,

0 commit comments

Comments
 (0)