GitHub Read Relay
The relay is the core of Octopool: a single Worker endpoint that performs read-only GitHub requests on behalf of a caller. It serves shared cache hits first, then equivalent token-free reads, and selects a pooled GitHub identity only when needed.
Source: src/relay.ts, src/router.ts, src/policy.ts, src/route-manifest.ts, src/github.ts, src/github-web.ts.
#POST /v1/github/request
Authenticated with a caller bearer token scoped to the target pool (see Auth).
Request body:
{
"pool": "maintainers",
"method": "GET",
"path": "/repos/openclaw/openclaw/pulls/123",
"query": { "per_page": "100" },
"headers": { "accept": "application/vnd.github+json" },
"route_hint": {
"pr_head_sha": "0123456789abcdef0123456789abcdef01234567"
}
}
pool,method,pathare required, non-empty strings.- Only
GETis enabled. Any other method is rejected with403 method_denied. queryvalues are strings or string arrays. Keys are rejected if they look secret-bearing (token,secret,password,api_key, …).headersare filtered down toaccept,x-github-api-version,if-none-match,if-modified-since. Everything else is dropped.route_hint.pr_head_shaand closed/mergedroute_hint.pr_stateare validated cache discriminators for PR file lists.- Legacy
route_hint.owner,route_hint.repo,route_hint.kind,cache_key, andidempotency_keyinput remains accepted for wire compatibility but is discarded during validation. It does not enter the trusted request model or affect routing, caching, or policy.
#Path validation
path must be an absolute GitHub API path. It is rejected (400 invalid_path) if it contains ://, \, ?, #, .., a bare dot segment, or percent-encoded path traversal (%2e, %5c). The relay only talks to approved GitHub API, web, raw-content, and patch hosts.
#Response envelope
{
"status": 200,
"headers": {
"content-type": "application/json",
"etag": "...",
"x-ratelimit-remaining": "4998",
"x-ratelimit-reset": "1780000000"
},
"body": {},
"body_encoding": "json",
"identity": { "id": "ghapp_openclaw_openclaw", "kind": "github_app" },
"relay": {
"pool": "maintainers",
"request_id": "...",
"cacheable": true,
"cache": "miss",
"stale_ok": false,
"route_kind": "pr_view",
"lease_reason": "highest_remaining"
}
}
headersare filtered to a safe allowlist (content negotiation, caching, rate-limit, request id). Authorization and cookies never leave the Worker.body_encodingisjson,text, orbase64. Binary responses are base64-encoded.repo_viewreturns a fixed public metadata subset before caching so token-specific repository fields such as identity permissions are not shared.- Release list/latest/tag/id reads use unauthenticated public GitHub API reads; supported top-level
gh release viewsummaries prefer public GitHub release HTML once anonymous quota falls below 50%. Raw API requests retain exact REST response semantics. Octopool does not use pooled credentials for releases, so draft/private release visibility is not shared. - Supported top-level
gh run view --json jobsreads can compose job and step metadata from public GitHub pages below 50% anonymous API quota. Raw/actions/runs/{id}/jobsrequests retain exact REST response semantics, and log bodies still require authenticated API access. - Public org repository/member/event reads, user/gist collection reads, global metadata reads, and public repository metadata collections can be served from unauthenticated GitHub API responses before spending pooled identity quota.
GET /orgs/:orgis intentionally not relayed because authenticated GitHub responses can include additional org fields that are not present in unauthenticated public API responses.GET /users/:login/starredand/subscriptionsare intentionally not relayed because authenticated responses can include private repositories visible to the caller.cacheishit,stale,miss, orbypass(conditional, log, large-payload, or otherwise non-cacheable request).stale_ok: truemeans an expired public cache entry was served because all eligible identities were depleted, cooling down, missing, or rate-limited.stale_reasonandcache_expires_atare included on those responses.backendis present asweborgithub_publicwhen a cache miss or identity-less cache hit was served without a pooled API identity.lease_reasonissticky,highest_remaining, orfallback— see Identities & routing.
#Supported routes
Routes are defined in src/route-manifest.ts and enforced by src/policy.ts. Only the following read-only shapes are enabled. A safe CLI-shaped request outside this set gets 424 fallback_local with reason route_denied, so the shim can delegate to real gh:
<!-- supported-route-kinds:start -->
user_viewuser_repo_listuser_org_listuser_gist_listuser_follower_listuser_following_listuser_event_listuser_received_event_listuser_key_listuser_gpg_key_listorg_repo_listorg_event_listorg_public_member_listorg_public_member_viewgist_viewemoji_listgithub_metalicense_listlicense_viewgitignore_template_listgitignore_template_viewrepo_viewcommit_listcommit_viewcommit_commentscommit_pullscommit_branches_where_headcommit_statusesrepo_commentcomparecontentsrepo_readmepr_viewpr_listpr_filespr_commitspr_review_commentspr_review_comment_listpr_review_comment_viewpr_review_comment_reactionspr_reviewspr_review_viewpr_review_comments_for_reviewpr_requested_reviewerscommit_check_runscommit_check_suitescommit_statusref_statusesrun_listrun_viewrun_jobsrun_artifactsjob_viewjob_logscheck_run_annotationsissue_viewissue_listissue_commentsissue_comment_listissue_comment_viewissue_comment_reactionsissue_eventsissue_event_listissue_event_viewissue_labelsissue_reactionsissue_timelineassignee_listassignee_viewlabel_listlabel_viewmilestone_listmilestone_viewbranch_listbranch_viewtag_listrepo_languagesrepo_contributorsrepo_licenserepo_topicscommunity_profilefork_liststargazer_listsubscriber_listdeployment_listrepo_event_listnetwork_event_listrepo_stats_contributorsrepo_stats_commit_activityrepo_stats_code_frequencyrepo_stats_participationrepo_stats_punch_cardgit_blobgit_commitgit_treegit_refgit_matching_refsworkflow_listworkflow_viewworkflow_run_listrelease_listrelease_latestrelease_viewrelease_assetsrelease_assetsearch_issuessearch_codesearch_commitssearch_repositoriesrate_limit
<!-- supported-route-kinds:end -->
job_logs is a large-payload, log-class route: it follows GitHub's signed redirect to *.actions.githubusercontent.com / *.blob.core.windows.net, is not cached, and is gated by the pool's allow_logs policy.
#Policy gates
classifyRoute enforces, per pool:
allowed_owners— owners with scoped identity routing. Defaults toDEFAULT_ALLOWED_OWNERS(openclaw).allow_public_repos— public repositories from other owners are allowed after the public-repo guard provesprivate: false(defaulttrue). These routes use broad PAT identities from the pool rather than repo-scoped GitHub App installation tokens.allow_logs— log routes require it (defaulttrue), else424 fallback_localwith reasonlogs_denied.allow_search— search routes require it (defaultfalse). Issue, code, and commit searches require exactly onerepo:owner/namequalifier plus plain terms and optionaltype:issue|pr/state:open|closed; repository search accepts plain terms only. Invalid queries return424 fallback_localwith reasonsearch_denied.
Every repo route additionally passes a public-visibility check before a pooled identity or cache entry is used — see Cache & public-repo guard. The complete list of relay paths eligible for anonymous API or public web/raw/Git transport is in Token-Free GitHub Endpoints.
route_hint.pr_head_sha and route_hint.pr_state are validated, optional cache discriminators for PR file lists. They do not bypass policy or visibility checks; they only let clients that already know current PR state keep /files cache entries separate across head SHAs or closed/merged state.
#Safety limits
- Redirects from
api.github.comare denied (502 github_redirect_denied) except the log-download flow above. - Response bodies are capped: 1 MiB default, up to
MAX_RESPONSE_BYTES(2 MiB) for large-payload routes and Actions run lists. Run lists remain cacheable; over-cap responses fail with502 github_response_too_large. - Requests time out after
REQUEST_TIMEOUT_MS(15s default).
#Audit
Every validated request from an authenticated caller to an existing pool writes an audit_events row with request id, caller, pool, route key, route kind, identity id, status, error code, and duration. Parse, authentication, and pool-lookup failures occur before the audit boundary. Audit writes happen via ctx.waitUntil and never block the response. The hourly maintenance task deletes audit rows older than 30 days in bounded batches, matching the maximum stats query window.