6.4 KiB
Native PR diff reading v1
get_pull_request_diff(owner, repo, index, cursor?) is an authenticated
native MCP read tool. It consumes Core's
forgejo.pr-diff-evidence.v1 at the fixed native route
/api/v1/repos/<owner>/<repo>/pulls/<index>.diff?binary=true&evidence=v1.
It requires current native read:repository scope and the repository's
Code/PR permissions. No additional write scope or connected-account flow
is introduced. The anonymous four-tool catalog is unchanged.
Receipt and continuation
Each call obtains and verifies the whole new native evidence response through the fixed authenticated route for the requested repository and PR. That route applies native permissions and resolves the PR within its authorized repository; its positive repository/PR IDs are native authority. The gateway validates those IDs, requested index and all internal generation repository IDs/provenance. Only after successful final JSON closure, entity framing, canonical base64, decoded byte count and raw SHA-256 verification does it return one window. A valid requested prefix followed by bad padding, corrupt bytes, truncated JSON, a duplicate member or a wrong whole hash produces an error only. No usable bytes, cursor or intermediate result are published.
The result contains requested, the freshly verified manifest,
byte_offset, chunk_bytes, authoritative base64 data, encoding,
chunk_sha256, whole_diff_verified, eof, next_byte_offset and
next_cursor. text is supplied when the selected chunk is strict UTF-8,
otherwise null. Binary data and code points crossing a byte boundary are
preserved by base64. No replacement decoding is used.
Repeat the same owner, repo and positive integer index, supplying
cursor=next_cursor, until eof is true. For example:
{"owner":"team","repo":"project","index":7}
{"owner":"team","repo":"project","index":7,"cursor":"<next_cursor>"}
The checksummed cursor is an untrusted hint, not an access grant. Every page uses the current caller's credentials and native permission checks. An authorized different caller can use the hint. Missing or revoked access, unavailable evidence and malformed responses refuse the page. No anonymous or alternate-credential retry occurs.
The cursor binds requested context, native repository/PR row IDs and index, object format, full generation input pair and source/ref provenance, generation profile, whole byte count/hash and next byte offset. Changed identity, inputs or bytes require an explicit restart without the cursor. The gateway never silently splices a new diff into an old continuation. Full generation and transfer cost applies to every page, including a retry after process restart. There is no private byte cache, persistent artifact, cursor TTL or promise of resuming an old diff after PR movement.
Meaning of the manifest
Core generates from full immutable OIDs: the PR's stored merge base and
the base repository's hidden refs/pull/<index>/head. Both input repository
IDs must match the authorized target repository. Current visible API branch
tips are not substitutes for those inputs. The original fork repository and
branch record remain historical context with resolution=not_read and
generation_input=false. A deleted fork can have a null repository ID;
an unavailable target-tip observation has a null OID and status=missing.
Byte labels remain canonical base64, including non-UTF-8 labels.
Capture/generation times, target-tip and original-fork observations are
fresh on each response and may change between pages. The result always
returns the new verified observations. Stable input/byte identity does not
prove that refs stayed unchanged between observations. The contract is
observed_input_range, with atomic_latest_pr_snapshot=false; it does not
claim a transactional latest-PR snapshot.
whole_diff_verified=true records the gateway's complete upstream receipt.
It does not prove that the client received or assembled earlier pages, or
that anyone reviewed their substance. Clients concatenate decoded chunks
at contiguous byte offsets, verify each chunk's SHA-256, then check the
assembled count and raw SHA-256 against manifest.content. Git blob
framing is not part of a diff checksum. eof says only that this selected
window reaches the verified diff's end. Any later PR write must separately
check the expected current input pair under its own write contract.
Bounds and transport
The parser keeps bounded metadata, a 16-KiB input/decode buffer and at most one 32-KiB candidate output window while hashing and discarding other diff bytes. Total diff length has no gateway cap. Page fitting accounts for the actual nested MCP text response and RPC ID under the existing 49,000-byte envelope. A smaller output window can result; metadata is never clipped. Evidence metadata and cursor input must fit that bounded metadata envelope, otherwise the call fails. Unrelated repository/PR descriptions are not fetched and impose no content limit on diff reading.
The native response must be HTTP 200, application/json,
Cache-Control: private, no-store, Accept-Ranges: none, with one canonical
Content-Length equal to the complete encoded JSON entity. Transfer-Encoding,
Content-Encoding, Content-Range, ambiguous length and redirects are refused.
Ordinary repeated Via headers are allowed. HTTP/error bodies and credential
values are never returned. Requests use the configured forge origin only;
returned URLs are not followed. Each call makes exactly one evidence GET,
spends the existing caller read
budget and uses the existing per-request I/O timeout. No total job deadline
is added.
Verification and installation
agent-gateway/test_pull_request_diff.py runs credential-free HTTP,
stream-split, late-corruption, canonical-pad-bit, identity, cursor, permission,
memory and response-envelope contracts normally and with -O.
scripts/e2e-local.sh also runs test_pull_request_diff_native.py against
the actual Core image: a disposable private repository, scoped reader,
multi-page binary reconstruction equal to separate native Git, revoked
continuation, moved-input refusal and a truly empty diff. Its existing
registrar, SSH, console, join and anonymous-catalog gates still apply.
Install and byte-verify agent-gateway/mcp_forgejo.py and
agent-gateway/pr_diff_evidence.py together, with the required Core schema
already present. An accepted source gate is distinct from installed runtime
proof. See the runbook.