laboratory/docs/NATIVE_PR_DIFF_V1.md
Claudiu Schuster 0a99ff8125
All checks were successful
access-validation-contract / contract (push) Successful in 3m23s
ci / ci (push) Successful in 4m31s
Add complete native PR diff reads to authenticated MCP (#148)
2026-10-20 18:23:56 +00:00

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.