Skip to content

fix(sandbox): a checkout with no credential must arrive complete - #116

Merged
debuggingfuture merged 2 commits into
mainfrom
fix/complete-clone-for-diff
Aug 11, 2026
Merged

fix(sandbox): a checkout with no credential must arrive complete#116
debuggingfuture merged 2 commits into
mainfrom
fix/complete-clone-for-diff

Conversation

@debuggingfuture

@debuggingfuture debuggingfuture commented Aug 10, 2026

Copy link
Copy Markdown
Member

pr-review has been failing intermittently on prepare-diff — three of nine runs on one consumer repo over 24h, each burning ~5 minutes before it gave up. The cause is in the clone, not the diff.

Problem & Insight

The clone is the container's only authenticated reach at GitHub — by design, nothing after it holds a credential (ADR-0006). The SDK's gitCheckout owns the clone's object filter and exposes no way to turn it off, so it handed back a checkout that still needed the network to answer questions about its own history, and had nothing left to ask with.

pr-review's three-dot git diff <base>...<head> reads merge-base blobs. Those belong to neither tree a clone materialises: not the default-branch tip it lands on, and not the head git checkout moves to. Git fell through to the promisor remote, found no credential, and the step died:

fatal: could not read Username for 'https://github.com': No such device or address
fatal: could not read Username for 'https://github.com': No such device or address
fatal: unable to read <oid>          # a source file the PR touched, at the merge-base

Intermittent by construction, which is why it read as flakiness: it only bit when a merge-base blob differed from both trees on disk — i.e. when the PR sat behind its base by a commit touching the same file. Runs whose PR was level with the default branch passed on the same code path.

Take

git.clone is now three execs of its own — clone, SHA checkout, scrub — with the clone deliberately complete: no --filter, no --depth, no --single-branch. Completeness belongs to the primitive, not to one recipe; routing pr-review around its own checkout (fetching the diff from the compare API) would have left the next git consumer to rediscover this.

Doing the clone ourselves means owning the credential, so the token no longer travels in the URL. Git gets the credential-free URL plus a -c credential.helper that reads it from FLARE_DISPATCH_CLONE_TOKEN, set on the clone exec alone. A URL carrying its own credential leaks by three routes at once — the command string, git's stderr, .git/config — each needing its own guard, and the command-string one was held up by an internal of the pinned @cloudflare/sandbox, which is what ADR-0011 says not to lean on. Reading from the environment closes all three at the source: redactCloneFailure and scrubCloneCredential stay as backstops rather than as the only thing between an installation token and the workload. authenticateCloneUrl is deleted rather than left exported.

Verified against a private repo: clone fails Authentication failed with the variable unset, succeeds with it set, remote.origin.url is credential-free from the start, no promisor config, and the three-dot diff that started this runs green over historical blobs with an unreachable remote.

Cost is the historical blobs a filtered clone skipped — 30 MB for the repo that hit this, against a clone budget unchanged at the SDK's 600s. The substrate's own depth: 1 clone (apps/substrate/src/sandbox-do.ts) is untouched: it checks out one named ref for a shell, never diffs across history, and says so.

Key actions

  • Complete clone via cloneCommand, replacing box.gitCheckout
  • Token supplied through the clone exec's environment; authenticateCloneUrl removed
  • Regression tests on the command shape — no --filter / --depth / --single-branch / --bare / --sparse; no token, no x-access-token: in the command; inherited credential helpers reset first
  • Clone failures cover their own case: non-zero exit fails the checkout, and the credential stays out of the failure record
  • pnpm test 2149 passed, pnpm typecheck + pnpm lint clean

The clone is the container's only authenticated reach at GitHub — the
credential scrub rewrites `.git/config` the moment the checkout lands
(ADR-0006). The SDK's `gitCheckout` owns the clone's object filter and
offers no way to turn it off, so the checkout it produced still needed the
network to answer questions about its own history, and had nothing left to
ask with.

`pr-review` is where that bit. Its three-dot `git diff <base>...<head>`
reads MERGE-BASE blobs, which belong to neither tree a clone materialises —
the default-branch tip it lands on, nor the head `git checkout` moves to.
Git reached for the promisor remote, found no credential, and the step
died after ~5 minutes on two `could not read Username for
'https://github.com'` prompts and `unable to read <oid>`. Intermittent by
construction: it only bit when a merge-base blob differed from both trees
on disk.

`git.clone` is now three execs of its own — clone, SHA checkout, scrub —
with the clone deliberately complete: no `--filter`, no `--depth`, no
`--single-branch`. The URL's userinfo is stripped by the SDK's own log
sanitizer before any command string reaches a log, so the token's exposure
is unchanged. Completeness is a property of the primitive, not of one
recipe: anything cheaper needs a credential that outlives the clone.
@debuggingfuture
debuggingfuture marked this pull request as ready for review August 10, 2026 21:28

@flaredispatch-fractalboxdev flaredispatch-fractalboxdev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI code review — 🛑 Request changes

Risk tier: full · 1 critical · 1 warnings · 0 suggestions

Reviewers: security ⚠️ · performance 1 · code-quality ⚠️ · documentation ⚠️ · release-management ⚠️ · compliance 1 · agents-md 0

1. ⚠️ Warning — Full clone can severely increase execution time and storage

📍 packages/runtime-cf/src/sandbox-clone-url.ts:104-105

'cloneCommand' intentionally omits depth, filter, and branch narrowing, so every run downloads all reachable objects and tags before the review starts. Large repositories can approach or exceed the 600-second clone timeout and consume substantially more sandbox disk than the previous filtered checkout. Consider retaining narrowly scoped credentials for a targeted post-clone fetch, or otherwise adding a bounded fetch strategy that preserves the no-network-after-scrub guarantee.

2. 🛑 Critical — Authenticated clone URL is passed directly to exec

📍 packages/runtime-cf/src/sandbox-cf.ts:530-537

The new implementation embeds the GitHub installation token in the shell command sent to 'box.exec'. Unlike the previous 'gitCheckout' API, this exposes the credential to any sandbox command tracing, process/worker diagnostics, or SDK logging that records the raw command; the nearby comment's claim that exec sanitizes the URL is not enforced by this change. It also risks propagating the token through clone stderr and subsequent error handling. Avoid placing credentials in the command string (use a credential mechanism supported by the sandbox, or explicitly guarantee and test redaction at the exec boundary before shipping).

📋 View full logs & reviewed diff ↗

Review flagged the first cut: moving the clone from the SDK's `gitCheckout`
to `exec` put the authenticated URL — token and all — into a command string.
It is redacted there, but only by `redactCommand` inside
`@cloudflare/sandbox`, i.e. by an internal of a pinned dependency. ADR-0011
is about exactly that: a guarantee held up by an SDK pin can be revoked by a
version bump nobody reads as a security change.

A URL carrying its own credential leaks by three routes at once — the
command string, git's stderr, and `.git/config` — and each one needs its own
guard. Git now gets the credential-free URL and a `-c credential.helper` that
reads the token from `FLARE_DISPATCH_CLONE_TOKEN`, set on the clone `exec`
alone. The command holds only the variable's name, nothing git prints can
contain a token, and `.git/config` never receives one. `redactCloneFailure`
and `scrubCloneCredential` stay, now as backstops rather than as the only
thing between an installation token and the workload.

Verified against a private repo: the clone fails `Authentication failed`
with the variable unset and succeeds with it set, `remote.origin.url` is
credential-free from the start, no promisor config, and the three-dot diff
that started this reproduces green over historical blobs with an unreachable
remote.

`authenticateCloneUrl` is deleted rather than left exported — nothing calls
it, and leaving the URL-embedding shape available invites its return.
@debuggingfuture

Copy link
Copy Markdown
Member Author

Both findings addressed in d1307d7.

Critical — token in the exec command. Correct, and the fix is not a redaction test. The SDK does sanitize (logCanonicalEventsanitizePayloadredactCommand, which strips URL userinfo from the command, the error message, and the stack), so the claim held for 0.10.1 — but it held via an internal of a pinned dependency, which ADR-0011 says is not where a credential guarantee belongs. Git now gets the credential-free URL plus -c credential.helper reading FLARE_DISPATCH_CLONE_TOKEN, set on the clone exec alone. The command carries the variable's name, git never sees a token so nothing it prints can quote one, and .git/config never receives one — the three leak routes closed at the source rather than filtered. authenticateCloneUrl is deleted so the URL-embedding shape can't come back.

Verified against a private repo: Authentication failed with the variable unset, success with it set, remote.origin.url credential-free from the start.

Warning — full clone cost. Kept, deliberately. There is no bounded middle: git fetch <sha> negotiates on commits, so an ancestor's blobs are never sent to a repo that already claims the descendant, and backfilling a filtered clone means --refetch, which downloads what a complete clone would anyway. The alternative you name — retaining a credential for a targeted post-clone fetch — is the trade ADR-0006 already decided the other way. The repo that hit this is 30 MB; the 600s clone budget is unchanged from what gitCheckout applied. If a consumer does outgrow it, the honest lever is a per-repo budget, not an incomplete checkout that fails five minutes into a diff.

@flaredispatch-fractalboxdev flaredispatch-fractalboxdev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI code review — 💬 Comment

Risk tier: full · 0 critical · 1 warnings · 0 suggestions

Reviewers: security ⚠️ · performance 1 · code-quality ⚠️ · documentation 0 · release-management ⚠️ · compliance 0 · agents-md ⚠️

1. ⚠️ Warning — Authenticated clones now download the entire repository history and object database

📍 packages/runtime-cf/src/sandbox-clone-url.ts:78-111

'cloneCommand' explicitly removes filtering, depth, and branch narrowing, so every authenticated run transfers all refs and blobs up front. For large or long-lived repositories this can substantially increase network, disk, and clone latency, and the fixed 600-second timeout may turn repositories that previously completed into failures. Consider preserving a complete-history fallback only when the review workload requires it, or otherwise adding a bounded/conditional clone strategy with a credential that remains available for lazy fetches.

📋 View full logs & reviewed diff ↗

@debuggingfuture
debuggingfuture merged commit c1d8b70 into main Aug 11, 2026
5 checks passed
@debuggingfuture
debuggingfuture deleted the fix/complete-clone-for-diff branch August 11, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant