Skip to content

fix(pull-requests): render private GitHub images in PR bodies - #8446

Closed
Xanacas wants to merge 14 commits into
pingdotgg:mainfrom
Xanacas:t3code/fix-pr-image-rendering
Closed

Xanacas wants to merge 14 commits into
pingdotgg:mainfrom
Xanacas:t3code/fix-pr-image-rendering

Conversation

@Xanacas

@Xanacas Xanacas commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Images embedded in pull request bodies rendered as broken placeholders: GitHub serves github.com/user-attachments/assets/… uploads only to an authenticated viewer, and the browser sends no GitHub cookies from the app's origin (verified: unauthenticated fetch → 404, with a gh token → 200).

Fixed by routing those URLs through the existing signed asset flow. The markdown image classifier in client-runtime returns a new GitHubAttachment source, clients request a signed /api/assets URL for it over the authenticated WebSocket, and the server resolves GitHub's redirect with the gh CLI token (cached, single-flight) and answers 302 to the short-lived signed storage URL. Image bytes flow from storage straight to the browser; the token never leaves the server. Claim expiry is bucketed so repeat issuances stay byte-identical and browser caches hit. Web and mobile both render through the shared classifier; public-repo uploads resolve anonymously if gh is not authenticated.

Verified end to end in a local web run against a private-repo PR — before/after screenshots in this comment.

Changes made by Claude Fable 5 (Claude Code).

🤖 Generated with Claude Code

Note

Render private GitHub attachment images in PR bodies via signed proxy URLs

  • Adds shared isGitHubUserAttachmentUrl allowlist predicate and a github-attachment variant to AssetResource so recognized github.com/user-attachments URLs can be classified and proxied
  • AssetAccess.issueAssetUrl now issues signed proxy URLs for GitHub attachments; resolveAsset validates the claim and returns a remote asset result; the GET asset route issues a 302 redirect to a resolved HTTPS storage location
  • Introduces GitHubAttachmentProxy service that resolves a redirect location using a cached (5-min) GitHub CLI token, manual redirect handling, HTTPS-only filtering, and failure-to-null conversion; wired into the production server layer and assetsCreateUrl WebSocket RPC
  • Web (ChatMarkdown) and mobile (ThreadFeed) markdown renderers route recognized GitHub attachment images through signed asset URLs; markdown without an environment ID still uses the direct GitHub URL
  • Risk: resolveAsset returns null for expired, tampered, or no-longer-valid claims, and the asset route returns 404 when GitHubAttachmentProxy is unavailable or cannot resolve a location — callers that expect a file response for these assets must handle the new remote/redirect path

Macroscope summarized 9a5a1de.

GitHub serves user-attachments uploads only to an authenticated viewer, so
images in PR bodies rendered as broken placeholders in the app. Clients now
route those URLs through the signed asset route; the server resolves GitHub's
redirect with the gh CLI token and answers 302, so bytes flow from signed
storage straight to the browser and the token never leaves the server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: faf14ea4-c819-4b05-8b99-be736ecc7ffc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 27, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Effect Service Conventions: one finding — the new GitHubAttachmentProxy service adds backend behavior with no focused test. Service definition order, make/layer exports, Foo["Service"] typing, environment-based dependency acquisition, and the new Schema.TaggedErrorClass all match the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/assets/GitHubAttachmentProxy.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1da4829ea2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/client-runtime/src/markdownImages.ts
@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change adds a server-side GitHub credential proxy and changes existing PR-image loading across web and mobile. Signed asset capabilities are minted from URL shape alone while the proxy can access private attachments through the environment credential, making authorization scope and cross-repository exposure important risks to examine.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Route handlers resolve services at serve time, so providing the proxy to
makeRoutesLayer never reached the request fiber and every proxied image
answered 404. Caught in an integrated web pass; unit tests could not see it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Xanacas

Xanacas commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Before/after from an integrated web pass against a private-repo PR (Vantisgo, PR body with three user-attachments screenshots):

Before (images 404 as broken placeholders):

before

After (rendered through the signed asset proxy; all three images load with real dimensions):

after

The integrated pass also caught a layer-wiring bug the unit tests could not see: route handlers resolve services at serve time, so the proxy layer had to be provided on the server runtime context rather than on makeRoutesLayer (fixed in 1a12535).

Xanacas and others added 2 commits August 27, 2026 22:56
Stub GitHubCli and HttpClient layers cover the token header and its cached
read, the anonymous fallback when gh has no token, and the null answers for
non-redirect responses and non-https locations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…available

An older server rejects the github-attachment resource during version skew,
and a markdown render without an environment has no proxy at all. Both now
fall back to loading the GitHub URL directly — public uploads keep working,
private ones degrade to the unavailable chip instead of regressing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the web-scoped changes (ChatMarkdown.tsx, pullRequest/pullRequestMarkdown.logic.ts) for shared-primitive use, Tailwind/CSS ownership, and environment routing.

Environment routing is correct: ChatMarkdownAssetImage requires an explicit environmentId, ChatMarkdown resolves it from threadRef?.environmentId ?? explicitEnvironmentId ?? null with no active-environment fallback, and every PullRequestMarkdown / PullRequestMarkdownEditor call site threads the owning environment explicitly.

One consistency finding on the new no-environment GitHub attachment branch (inline).

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
Xanacas and others added 2 commits August 27, 2026 23:07
The no-environment branch rendered a raw img with inline geometry and the
browser's broken-image glyph on failure; it now shares the block layout and
the unavailable chip of the proxied path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…endering

# Conflicts:
#	apps/server/src/assets/AssetAccess.ts
#	packages/contracts/src/assets.test.ts
Comment thread apps/server/src/assets/AssetAccess.ts Outdated
}
case "github-attachment": {
const url = input.resource.url;
if (!isGitHubUserAttachmentUrl(url)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Critical assets/AssetAccess.ts:303

issueAssetUrl lets any authenticated WebSocket caller mint a capability for any syntactically matching private GitHub attachment URL, exposing attachments accessible to the server's gh credential across private repositories. Validating only isGitHubUserAttachmentUrl(url) does not establish repository or pull-request access; authorize the URL against the requesting session before issuing the capability.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/assets/AssetAccess.ts around line 303:

`issueAssetUrl` lets any authenticated WebSocket caller mint a capability for any syntactically matching private GitHub attachment URL, exposing attachments accessible to the server's `gh` credential across private repositories. Validating only `isGitHubUserAttachmentUrl(url)` does not establish repository or pull-request access; authorize the URL against the requesting session before issuing the capability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This matches T3 Code's trust model on purpose, so no per-URL authorization is added:

  • An authenticated WS session already holds this authority. Every paired client is the operator of this environment and can run arbitrary agents and shell commands on the host machine — including gh auth token or gh api against any repo the credential reaches. The asset capability grants nothing the session couldn't already do in one turn; there is no finer-grained principal inside a single environment to authorize against.
  • The signed token is a transport shim, not a permission tier. Browsers can't attach the WS session's auth to <img> fetches, so the WS layer (already authenticated) mints a short-lived signed URL the image tag can use. It deliberately mirrors the existing attachment/workspace-file kinds, which likewise let any authenticated caller read any path the server can.
  • The guards that do matter are in place: strict allowlist (github.com/user-attachments/assets/<id> only), the gh token never leaves the server, only an https redirect Location is returned, and tokens expire quickly.

Per the repo's guidance, security should not be over-indexed for operator-level features; adding PR-scoped URL authorization here would add real complexity without a boundary to protect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding on the new GitHub-attachment image branches in apps/web/src/components/ChatMarkdown.tsx: the author's intrinsic width/height (and align) attributes that the Direct branch still forwards are now dropped for these images.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
Xanacas and others added 3 commits September 1, 2026 09:26
GitHub emits attachments with width/height attributes; forward them so
the image box is reserved before lazy load and layout does not shift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…endering

# Conflicts:
#	apps/mobile/src/features/threads/ThreadFeed.tsx
#	apps/server/src/http.ts
#	apps/web/src/components/ChatMarkdown.tsx
#	apps/web/src/components/pullRequest/pullRequestMarkdown.logic.ts
…to load

The fallback only kicked in when issuing the signed URL failed; a load
error on the signed URL itself showed the unavailable chip even though
the direct URL was loadable. Match the web renderer's fallback chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
Comment thread apps/server/src/assets/GitHubAttachmentProxy.ts Outdated
Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
Xanacas and others added 2 commits September 1, 2026 10:14
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Xanacas and others added 2 commits September 3, 2026 12:46
Without an environment there is nothing to proxy through, so the GitHub
attachment falls into the same <img> branch as any other remote image and
keeps expand and copy behavior. Drops the dedicated component and the
ServerConfig dependency the proxy only used for a cwd.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx
Drops the signed-then-direct fallback chain in web and mobile. A server
without the github-attachment kind fails the asset request, and the client
shows the same unavailable chip it shows for any other asset. Trims the
proxy tests to the one behavior worth guarding: the token is read once and
sent on every redirect resolution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Xanacas

Xanacas commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a clean squash of the same change onto current main, with the client fallback chain and extra tests removed. See the new PR linked from this repository's PR list (fix(pull-requests): render private GitHub images in PR bodies).

@Xanacas Xanacas closed this Sep 3, 2026
@Xanacas

Xanacas commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #9404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant