GitHub read-only adapter v1 — accepted specification
- Status: Accepted
- Accepted: 2026-08-02 by
@nomed - Governing issue: #18
- Security boundary: authenticated GitHub responses to validated, scope-bound planner observations
Objective
The adapter discovers the minimum GitHub repository, issue, Project schema, Project item, and relationship state needed by the accepted pure core. It cannot execute mutations. Network access, authentication, pagination, provider identifiers, and raw provider responses terminate at this boundary.
The adapter does not authorize a plan, decide policy, or render a public report. Authentication never implies authorization: every successful read is bound independently to the requested subject and resources.
Structural separation
type AllowedReadOperation =
| "resolve_scope"
| "read_project_fields"
| "read_project_item"
| "read_issue_relationships";
type ReadOnlyTransport = {
execute(
operation: AllowedReadOperation,
variables: Readonly<Record<string, unknown>>,
): Promise<unknown>;
};
The transport accepts an operation discriminator and variables, never a query string, URL, HTTP method, header, or credential. The adapter owns immutable GraphQL query documents for the four allowlisted operations. Each document MUST have a named query operation, MUST NOT contain a mutation or subscription operation, and MUST target the fixed public GitHub GraphQL endpoint selected by the host integration.
The adapter accepts a transport through dependency injection. It does not read environment variables, credential files, workflow context, the filesystem, clocks, randomness, or process-global network clients. A host creates the authenticated transport. Credentials never enter adapter inputs or outputs.
Redirects, arbitrary GraphQL, configurable endpoints, GitHub Enterprise Server, REST fallback, batching, and automatic persisted-query negotiation are outside v1.
Requested scope
type RequestedGitHubScope = {
subjectRef: string;
ownerLogin: string;
repositoryName: string;
projectNumber: number;
issueNumber: number;
};
Every string is trimmed, non-empty, contains no control character, and is at most 256 Unicode scalar values. projectNumber and issueNumber are positive safe integers. Owner and repository names use GitHub's documented login/name character set and are compared using the exact normalized form returned by GitHub; no fuzzy matching is allowed. subjectRef is an opaque host-attested installation or principal reference and is never sent as a resource selector.
resolve_scope MUST resolve and bind the authenticated subject, repository owner and name, issue number and membership, Project owner and number, and optional Project item membership. The adapter rejects missing resources, scope mismatch, cross-repository nodes, a Project owned by another requested owner, and multiple Project items for the same issue. Returned node IDs are opaque internal bindings and never become configuration.
Allowlisted reads
resolve_scoperesolves and cross-checks the bound repository, issue, Project, and optional Project item.read_project_fieldsreads definitions and supported option or iteration metadata only for the bound Project.read_project_itemreads values only for the single Project item bound to the requested issue.read_issue_relationshipsreads parent, sub-issue, and dependency relationships supported by GitHub for the bound issue. Cross-repository endpoints fail closed; absent provider support is not an empty graph.
An implementation MAY split a logical read into fixed internal query documents for GraphQL connection boundaries. The public operation set and limits remain unchanged, and no caller-controlled document is introduced.
Pagination and resource limits
Every connection uses forward cursor pagination with first: 50. The adapter permits at most:
| Resource | Limit |
|---|---|
| pages per connection | 20 |
| fields | 256 |
| options or iterations per field | 256 |
| Project items examined | 1,000 |
| field values on the bound item | 256 |
| relationship nodes | 512 |
| dependency edges | 4,096 |
| response bytes per page | 2 MiB |
| cumulative response bytes per adapter call | 16 MiB |
| GraphQL errors per response | 32 |
The transport MUST expose the raw response byte count without exposing the raw body. Absence of a byte count fails closed.
hasNextPage: true requires a non-empty endCursor. A cursor must advance and never repeat. Node IDs within and across pages must be unique unless explicitly defined as the repeated scope anchor. Limit exhaustion, duplicate nodes, cursor regression, inconsistent totalCount, or scope-anchor change fails the complete read. Truncation is never complete state.
Validated output
type GitHubObservation = {
scope: BoundGitHubScope;
projectSchema: ObservedProjectSchema;
item: ObservedItem | null;
relationships: ObservedRelationshipGraph;
evidence: {
schema: 1;
operationCounts: Readonly<Record<AllowedReadOperation, number>>;
pageCount: number;
fingerprint: string;
};
};
The fingerprint is lowercase hexadecimal SHA-256 over a canonical internal representation of validated bindings and observations, excluding raw responses and credentials. Provider order never affects it. It is a precondition token, not proof of authorization or freshness.
Logical keys resolve only through accepted repository policy. Provider IDs remain adapter-private. Unknown, malformed, duplicated, unsupported, or ambiguously mapped values fail closed when they affect managed state. Any failed operation, page, binding, or limit returns no partial observation.
Failure and retry semantics
V1 performs no automatic retries. A host may start a new complete read after explicit classification, using new scope resolution.
| Code | Meaning | Retry classification |
|---|---|---|
YKP-GH-READ-001 |
requested scope invalid | never |
YKP-GH-READ-002 |
authentication absent or invalid | after credential repair |
YKP-GH-READ-003 |
permission or access denied | after authorization repair |
YKP-GH-READ-004 |
transport or provider unavailable | explicit full-read retry permitted |
YKP-GH-READ-005 |
response or collection limit exceeded | never without review |
YKP-GH-READ-006 |
subject or resource binding mismatch | never |
YKP-GH-READ-007 |
pagination invariant violated | never |
YKP-GH-READ-008 |
response shape unsupported or malformed | after compatibility review |
YKP-GH-READ-009 |
primary or secondary rate limit | after provider-declared reset |
YKP-GH-READ-010 |
safe diagnostic could not be constructed | never |
YKP-GH-READ-011 |
required provider capability unavailable | after capability review |
HTTP 429, 502, 503, and 504 may be classified retryable, but the adapter never sleeps or retries. Other statuses are not retryable by default. A bounded reset hint may be returned privately to the host and is excluded from reports and fingerprints.
Redaction
Public diagnostics contain only schema version, stable code, severity, safe static message, logical operation, and retry classification. They exclude credentials; raw bodies; GraphQL messages or extensions; URLs; paths; documents; variables; headers; all provider and consumer identifiers; cursors; request IDs; stack traces; and transport-library messages.
Unsafe failures collapse to YKP-GH-READ-010. Redaction occurs before logging, serialization, callbacks, or error construction, not as post-processing.
Permission profile
The host credential MUST provide organization Projects, repository metadata, and issue read permissions sufficient for the fixed operations. A short-lived credential restricted to those reads remains the recommended least-privilege profile. An existing supplied credential that also has write permissions MUST NOT be rejected or fail dry-run or legacy-shadow qualification solely because of that excess scope. The adapter has no mutation transport, apply host, approval input, or controlled-apply authority, so excess permissions confer no additional behavior.
If the transport can attest that a credential is overprivileged, the adapter emits a non-public permission-delta warning. Excess privilege never becomes authorization.
Compatibility and rollback
Adding an operation, endpoint, provider, resource class, exposed identifier, retry, or fallback; widening a limit; weakening scope comparison; accepting partial state; or changing fingerprint inputs requires public-contract and security review. Any mutation requires a separate accepted contract and threat-model update.
Rollback removes the host integration and discards observations. The pure core retains no network dependency.
Implementation gate
Implementation follows only after explicit acceptance. Tests use an injected synthetic transport and invented fixtures only. They MUST prove query-only allowlisting, arbitrary-document and URL impossibility, exact scope binding, pagination progression, every resource limit, duplicate rejection, atomic failure, stable fingerprints, zero retry, credential absence, provider-error redaction, permission-delta reporting, and structural absence of mutation capability.
A live GitHub read-only probe is a later, separately reviewed validation step using a dedicated least-privilege credential with no consumer identity in public evidence.