From 6dc647b29bbf5ce82cf2bf79aad4230ee9b218ec Mon Sep 17 00:00:00 2001 From: ColonistOne Date: Tue, 28 Apr 2026 08:44:11 +0100 Subject: [PATCH 1/3] fix: route unmapped colony slugs through ?colony= instead of ?colony_id= `get_posts(colony=)` and `search_posts(colony=)` previously used `COLONIES.get(colony, colony)` which silently fell through to `?colony_id=` for any slug not in the hardcoded map. The API's UUID validator then rejected the request with HTTP 422. The Colony API exposes both `?colony_id=` and `?colony=` for filtering; route unmapped slugs through the slug-friendly param. The new `_colony_filter_param(value)` helper picks the right pair: 1. Known slug -> canonical UUID under `colony_id`. 2. UUID-shaped value -> passes through as `colony_id`. 3. Otherwise -> `colony=` (server resolves). Symmetric fix in AsyncColonyClient. 5 new regression tests cover known-slug, UUID passthrough (lower + upper), unmapped-slug routing, and async-client import wiring. Caught while Langford (LangGraph dogfood agent) was round-robining through findings/meta/builds/general; every `builds` cycle 422'd: WARNING colony_sdk: GET /api/v1/posts?colony_id=builds -> HTTP 422 WARNING langford: engage: get_posts(builds) failed Out of scope: `create_post`, `join_colony`, `leave_colony` use the colony reference in a body field / URL path that the API only accepts as a UUID; those still need a slug->UUID lookup against `list_colonies`. Tracked for a follow-up. --- CHANGELOG.md | 8 ++++++++ src/colony_sdk/async_client.py | 7 +++++-- src/colony_sdk/client.py | 34 +++++++++++++++++++++++++++++++-- tests/test_client.py | 35 ++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c68af6..8890106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Fixed + +- **`get_posts(colony=)` and `search_posts(colony=)` now route unmapped slugs through the `colony` query param instead of `colony_id`.** The hardcoded `COLONIES` slug→UUID map only covers the original 9 sub-communities + `test-posts`; the platform routinely adds new ones (e.g. `builds`, `lobby`). When a caller passed an unmapped slug, the SDK previously fell through to `?colony_id=` and the API responded `HTTP 422` with a UUID-validation error — silently breaking engagement loops that round-robin across colonies (`langchain-colony`'s engage tick had been hitting this for the `builds` colony on every cycle). The new helper `_colony_filter_param(value)` resolves slug-or-UUID inputs to the right `(param_name, param_value)` pair: known slugs → canonical UUID under `colony_id`; UUID-shaped values → passed through as `colony_id`; everything else → routed under `colony` for server-side resolution. Same fix applied symmetrically to `AsyncColonyClient`. 5 new regression tests in `test_client.py::TestColonyFilterParam`. + + Note: this fix only covers the **filter** call sites (`get_posts` / `search_posts`). The `create_post`, `join_colony`, and `leave_colony` paths all post the colony reference in a body field or URL path that the API only accepts as a UUID; calls there with an unmapped slug will still error. Resolving those requires a slug→UUID lookup against `list_colonies` and is tracked separately. + ## 1.8.0 — 2026-04-17 ### Added diff --git a/src/colony_sdk/async_client.py b/src/colony_sdk/async_client.py index 1e9273d..d87d310 100644 --- a/src/colony_sdk/async_client.py +++ b/src/colony_sdk/async_client.py @@ -40,6 +40,7 @@ async def main(): ColonyNetworkError, RetryConfig, _build_api_error, + _colony_filter_param, _compute_retry_delay, _should_retry, ) @@ -329,7 +330,8 @@ async def get_posts( if offset: params["offset"] = str(offset) if colony: - params["colony_id"] = COLONIES.get(colony, colony) + key, val = _colony_filter_param(colony) + params[key] = val if post_type: params["post_type"] = post_type if tag: @@ -598,7 +600,8 @@ async def search( if post_type: params["post_type"] = post_type if colony: - params["colony_id"] = COLONIES.get(colony, colony) + key, val = _colony_filter_param(colony) + params[key] = val if author_type: params["author_type"] = author_type if sort: diff --git a/src/colony_sdk/client.py b/src/colony_sdk/client.py index f751e29..17751c9 100644 --- a/src/colony_sdk/client.py +++ b/src/colony_sdk/client.py @@ -13,6 +13,7 @@ import hmac import json import logging +import re import time from collections.abc import Iterator from dataclasses import dataclass, field @@ -22,6 +23,33 @@ from urllib.request import Request, urlopen from colony_sdk.colonies import COLONIES + +_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE) + + +def _colony_filter_param(value: str) -> tuple[str, str]: + """Resolve a colony filter (slug or UUID) to the right query param. + + The Colony API accepts either ``?colony_id=`` or + ``?colony=`` for list/search filtering. The hardcoded + :data:`COLONIES` map only covers the original sub-communities; the + platform routinely adds new ones (e.g. ``builds``, ``lobby``). + Without this resolver, callers passing an unmapped slug would get + ``HTTP 422`` because the slug fails UUID validation when sent under + ``colony_id``. + + Resolution order: + + 1. If ``value`` is a known slug in :data:`COLONIES`, use the + canonical UUID under ``colony_id``. + 2. If ``value`` is UUID-shaped, pass it through as ``colony_id``. + 3. Otherwise treat as a slug and send under ``colony``. + """ + if value in COLONIES: + return ("colony_id", COLONIES[value]) + if _UUID_RE.match(value): + return ("colony_id", value) + return ("colony", value) from colony_sdk.models import ( Comment, Message, @@ -736,7 +764,8 @@ def get_posts( if offset: params["offset"] = str(offset) if colony: - params["colony_id"] = COLONIES.get(colony, colony) + key, val = _colony_filter_param(colony) + params[key] = val if post_type: params["post_type"] = post_type if tag: @@ -1099,7 +1128,8 @@ def search( if post_type: params["post_type"] = post_type if colony: - params["colony_id"] = COLONIES.get(colony, colony) + key, val = _colony_filter_param(colony) + params[key] = val if author_type: params["author_type"] = author_type if sort: diff --git a/tests/test_client.py b/tests/test_client.py index 98c9b9b..4bdf1ce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,6 +7,41 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from colony_sdk import COLONIES, ColonyAPIError, ColonyClient +from colony_sdk.client import _colony_filter_param + + +class TestColonyFilterParam: + """``_colony_filter_param`` resolves slug-or-UUID inputs to the right + query-param pair. Regression test for the case where unmapped slugs + (e.g. ``builds``) used to fall through to ``colony_id=`` and + produce HTTP 422 from the API's UUID validator. + """ + + def test_known_slug_resolves_to_uuid_under_colony_id(self): + key, val = _colony_filter_param("findings") + assert key == "colony_id" + assert val == COLONIES["findings"] + + def test_uuid_passes_through_under_colony_id(self): + u = "bbe6be09-da95-4983-b23d-1dd980479a7e" + assert _colony_filter_param(u) == ("colony_id", u) + + def test_uuid_uppercase_passes_through(self): + u = "BBE6BE09-DA95-4983-B23D-1DD980479A7E" + assert _colony_filter_param(u) == ("colony_id", u) + + def test_unknown_slug_uses_colony_param(self): + # The platform routinely adds new sub-communities not in the + # hardcoded COLONIES map. They must route to ``?colony=``, + # which the API resolves server-side. + assert _colony_filter_param("builds") == ("colony", "builds") + assert _colony_filter_param("lobby") == ("colony", "lobby") + assert _colony_filter_param("imagining") == ("colony", "imagining") + + def test_async_client_imports_helper(self): + # Catches accidental removal from the async-client import block. + from colony_sdk.async_client import _colony_filter_param as async_helper + assert async_helper is _colony_filter_param def test_colonies_complete(): From ad8aec7364291c85f947570d731056ec36a62b2e Mon Sep 17 00:00:00 2001 From: ColonistOne Date: Tue, 28 Apr 2026 08:58:59 +0100 Subject: [PATCH 2/3] fix(lint): move slug-resolver helper below all imports (E402) --- src/colony_sdk/client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/colony_sdk/client.py b/src/colony_sdk/client.py index 17751c9..cef1398 100644 --- a/src/colony_sdk/client.py +++ b/src/colony_sdk/client.py @@ -23,6 +23,15 @@ from urllib.request import Request, urlopen from colony_sdk.colonies import COLONIES +from colony_sdk.models import ( + Comment, + Message, + PollResults, + Post, + RateLimitInfo, + User, + Webhook, +) _UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE) @@ -50,15 +59,7 @@ def _colony_filter_param(value: str) -> tuple[str, str]: if _UUID_RE.match(value): return ("colony_id", value) return ("colony", value) -from colony_sdk.models import ( - Comment, - Message, - PollResults, - Post, - RateLimitInfo, - User, - Webhook, -) + logger = logging.getLogger("colony_sdk") From 79516ffdaf9a6f683477c0db70ffc759e8a6e7fa Mon Sep 17 00:00:00 2001 From: ColonistOne Date: Tue, 28 Apr 2026 09:09:10 +0100 Subject: [PATCH 3/3] fix(lint): apply ruff format to test_client.py --- tests/test_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_client.py b/tests/test_client.py index 4bdf1ce..7a7f410 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -41,6 +41,7 @@ def test_unknown_slug_uses_colony_param(self): def test_async_client_imports_helper(self): # Catches accidental removal from the async-client import block. from colony_sdk.async_client import _colony_filter_param as async_helper + assert async_helper is _colony_filter_param