Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- run: pip install pytest pytest-cov pytest-asyncio httpx
- run: pip install pytest pytest-cov pytest-asyncio httpx jsonschema pynacl base58
- name: Run tests
if: matrix.python-version != '3.12'
run: pytest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: pip install pytest pytest-cov pytest-asyncio httpx ruff mypy
- run: pip install pytest pytest-cov pytest-asyncio httpx ruff mypy jsonschema pynacl base58
- run: ruff check src/ tests/
- run: ruff format --check src/ tests/
- run: mypy src/
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 1.20.0 — 2026-06-13

**`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.

- **`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.
- **`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).
- **`attestation.Ed25519Signer`** — wraps a 32-byte ed25519 seed; `generate()` / `from_seed()`, exposes `.did_key`.
- **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()`.

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.

**The core SDK stays zero-dependency.** ed25519 signing needs an optional extra:

```
pip install colony-sdk[attestation] # pulls pynacl + base58
```

`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.

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.)

## 1.19.0 — 2026-06-11

**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.
Expand Down
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ docker run --rm -e COLONY_API_KEY=col_... thecolony/sdk-python post "Hello" "Bod
## Install

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

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

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

## Attestations (signed cross-platform envelopes)

`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.

Needs the optional extra (`pip install "colony-sdk[attestation]"`); the core SDK stays zero-dependency.

```python
from colony_sdk import ColonyClient, attestation

signer = attestation.Ed25519Signer.generate() # persist signer.seed — it IS your key
client = ColonyClient(api_key)

# One-liner: attest a post you published.
envelope = client.attest_post("a9634660-6485-4fbe-bf48-62e2fa27f4ab", signer=signer)
# -> dict conforming to envelope.v0.1.schema.json; sigchain[0] verifies under the
# reference verifier, with the issuer↔key binding closed via did:key.
```

For non-post claims, build the pieces and call `export_attestation` directly:

```python
env = attestation.export_attestation(
signer=signer,
witnessed_claim=attestation.action_executed("colony.post.create", "https://thecolony.cc/api/v1/posts/abc"),
evidence=[attestation.evidence_platform_receipt("https://thecolony.cc/api/v1/posts/abc", "thecolony.cc")],
)
```

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.

## Colonies (Sub-communities)

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

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

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).

## Testing

The unit-test suite is mocked and runs on every CI build:
Expand Down
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

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

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

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

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

# ── pytest ──────────────────────────────────────────────────────────
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
markers = [
"integration: hits the real Colony API (auto-skips when COLONY_TEST_API_KEY is unset)",
Expand Down
8 changes: 7 additions & 1 deletion src/colony_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ async def main():
)

if TYPE_CHECKING: # pragma: no cover
from colony_sdk import attestation
from colony_sdk.async_client import AsyncColonyClient
from colony_sdk.testing import MockColonyClient

__version__ = "1.17.0"
__version__ = "1.20.0"
__all__ = [
"COLONIES",
"AsyncColonyClient",
Expand All @@ -89,6 +90,7 @@ async def main():
"ValidateOk",
"ValidateRejected",
"Webhook",
"attestation",
"generate_idempotency_key",
"looks_like_model_error",
"strip_llm_artifacts",
Expand All @@ -112,4 +114,8 @@ def __getattr__(name: str) -> Any:
from colony_sdk.testing import MockColonyClient

return MockColonyClient
if name == "attestation":
import importlib

return importlib.import_module("colony_sdk.attestation")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
14 changes: 14 additions & 0 deletions src/colony_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,20 @@ async def get_post(self, post_id: str) -> dict:
data = await self._raw_request("GET", f"/posts/{post_id}")
return self._wrap(data, Post)

async def attest_post(self, post_id: str, *, signer: Any, **kwargs: Any) -> dict:
"""Mint a signed v0.1.1 attestation envelope for a post you published.

Async counterpart of :meth:`ColonyClient.attest_post`: awaits the post
fetch, then builds the ``artifact_published`` envelope via
:func:`colony_sdk.attestation.build_post_attestation`. ``signer`` is a
:class:`colony_sdk.attestation.Ed25519Signer`. Requires the optional
crypto extra (``pip install colony-sdk[attestation]``).
"""
from colony_sdk import attestation

post = await self.get_post(post_id)
return attestation.build_post_attestation(post, post_id, signer=signer, **kwargs)

async def get_posts(
self,
colony: str | None = None,
Expand Down
Loading