GitHubCapability¶
Lets agents read and write GitHub issues, pull requests, and project boards via a GitHub App installation. The coordination primitive claim_unassigned_issue turns a Project board into an external work-queue that survives Colony restarts and interleaves cleanly with human contributors.
Code: polymathera.colony.agents.patterns.capabilities.GitHubCapability. Subpackage: _github/{auth,client}.py. Event protocol: polymathera.colony.agents.blackboard.protocol.GitHubEventProtocol.
When to use¶
Many of the work products Colony agents produce — bug analyses, contract specifications, slicing reports, change-impact summaries — naturally land as GitHub issues, comments, or PR reviews. A coordinator that picks up unassigned issues from a Project board, runs an analysis, posts a comment with the result, and moves the issue to "Done" needs exactly this surface.
Action surface (22 actions)¶
| Group | Actions |
|---|---|
| Repo + content | list_repos, get_repo, list_branches, get_file_contents, search_code |
| Issues | list_issues, get_issue, create_issue, comment_on_issue, close_issue, reopen_issue, add_labels |
| Pull requests | list_pull_requests, get_pull_request, get_pr_diff, create_pull_request, comment_on_pr, review_pr, get_pr_checks |
| Projects v2 | list_project_items |
| Coordination | claim_unassigned_issue, release_claim |
Every action returns a uniform {"ok": bool, "message": str, …} shape. Errors degrade rather than raise: NotFoundError → {ok: false, status_code: 404}, RateLimitError → {ok: false, status_code: 403}, etc. Mutations write an audit record to audit:github:{ts}:{uuid}.
Authentication¶
GitHub Apps, not personal access tokens. The capability needs three pieces of configuration:
| What | Where it comes from |
|---|---|
| App ID | Constructor app_id= → typed GitHubAuthConfig.app_id → env GITHUB_APP_ID |
| Private key (PEM) | Constructor private_key_pem= / private_key_path= → typed GitHubAuthConfig.private_key_pem → env GITHUB_PRIVATE_KEY_PEM |
| Installation ID | Constructor installation_id= → typed GitHubAuthConfig.installation_id → env GITHUB_INSTALLATION_ID |
Each value falls through the chain in that order. See Configuration System for how operator YAML, env vars, and tier overlays compose.
The flow:
GitHubAppAuth.mint_jwtsigns a 9-minute JWT with the RSA key, backdated 60 s to tolerate clock skew.TokenCache.getexchanges the JWT for an installation-scoped access token (1 h TTL) at/app/installations/{id}/access_tokens.- The token is cached and refreshed 5 min before expiry under a lock.
Missing credentials are non-fatal: the capability captures an _init_error and every action returns a clean error dict. Operators turn it on by setting env vars (or — eventually — through the Settings UI).
Client behaviour¶
GitHubClient wraps httpx.AsyncClient with:
- Auto token injection on every request.
- One 401-retry with forced token refresh (handles a stale-cache window).
- Primary rate-limit detection (
x-ratelimit-remaining: 0) →RateLimitErrorraised immediately. - Secondary rate-limit detection (403/429 +
Retry-Afteror "secondary"/"abuse" in body) → exponential backoff with jitter, honouringRetry-After, up tomax_retries. - Pagination iterator (
iter_paginated) that stops when a page comes back short. - GraphQL helper for Projects v2 (the only API GitHub no longer offers via REST).
claim_unassigned_issue — the coordination primitive¶
GitHub Projects v2 doesn't support optimistic concurrency on field updates, so we lock with labels instead:
- List candidate open issues, filtering out anything already carrying a
claimed-by:*label. - For each candidate (in order):
- POST
claimed-by:<agent_id>to the issue's labels. - Re-fetch the issue.
- If our label is the only
claimed-by:*label → claim succeeded; return the issue. - Otherwise we lost a race — DELETE our label and try the next candidate.
- If no candidate succeeds, return
{"claimed": false, "issue": None}.
release_claim(issue_number) removes the label when work completes.
This pattern races safely against any number of concurrent agents and against humans who manually label issues. The audit log records every claim.
Event protocol¶
GitHubEventProtocol defines the blackboard key shapes a future POST /api/v1/github/webhook endpoint will write:
github:issue_opened:{owner}/{repo}:{number}
github:issue_commented:{owner}/{repo}:{number}
github:issue_closed:{owner}/{repo}:{number}
github:pr_opened:{owner}/{repo}:{number}
github:pr_review_requested:{owner}/{repo}:{number}
github:pr_merged:{owner}/{repo}:{number}
github:project_item_changed:{project_id}:{item_id}
Other capabilities can subscribe today via input_patterns=[GitHubEventProtocol.issue_opened_pattern(), …]. The webhook endpoint itself is deferred — see the design doc for the FastAPI route plan.
Configuration¶
GitHubCapability.bind(
scope=BlackboardScope.SESSION,
default_repo="acme/proj", # used when actions accept repo=None
default_project_id="PVT_kwDO…", # GraphQL node id
max_requests_per_minute=120,
audit_enabled=True,
)
Wired into the session agent. With no credentials configured the capability stays disabled; the action surface returns clean error dicts so the LLM can suggest the user configure it.
Test surface¶
tests/test_github_capability.py (26 tests). Every HTTP call goes through httpx.MockTransport. Covers:
- JWT claim shape, 60 s backdating.
- Token-cache refresh-before-expiry, exchange-error surfacing.
- Client 401-retry (one shot), primary rate-limit raising, paginated iterator short-page exit.
- Action surface via a
_StubClient: list-with-PR-exclusion, ownership defaults, audit emission, project field flattening. claim_unassigned_issue: happy path, skip-already-claimed, race-rollback (concurrent agent applied its label first → we delete ours and move on), no-candidates.GitHubEventProtocolround-trips for repo/number and project/item keys.- Missing-credentials init-error reporting.
Open follow-ups¶
- Webhook endpoint at
POST /api/v1/github/webhook(signature verified, normalised toGitHubEventProtocolkeys). - Settings UI for App credentials + project field mapping.
- Per-tenant multi-installation support.
GitHubReadOnlyCapabilitysubclass that omits mutations viabind(exclude_actions=[...]).GitHubActionsCapabilitysibling fortrigger_workflow/wait_for_workflow_run.