Skip to content

Deliberately published PR images ​

A screenshot has one permitted PR-renderable address; publishing it never needs an auxiliary git push. Ordinary run artifacts remain private under record 0033.

  • Code: src/artifacts/prImages.ts, src/artifacts/store.ts, src/artifacts/buildStore.ts, src/tools/attach.ts, deploy/cloudflare/prImages.ts, deploy/cloudflare/worker.ts
  • Tests: src/artifacts/prImages.test.ts, src/artifacts/store.test.ts, src/artifacts/buildStore.test.ts, src/tools/attach.test.ts, deploy/cloudflare/prImages.test.ts

Behavior ​

  1. attach_file { path, comment?, publishForPr: true } is deliberate public publication, not the default. Only a run with a resolved repository and the requester's existing repo:use permission (the GitHub capability's canWrite) may request it. The input supplies neither an artifact key nor a destination repository. The tool derives the outbound key from this run and this call. It completes the normal thread attachment first, then publishes that stored PNG. An attachment failure publishes nothing; a channel without an upload ticket still receives its private artifact link, but publication is refused because no inline upload completed. A publication failure says the attachment succeeded but no PR image was published. Both coding prompts instruct this path, embed the returned URL with Markdown image syntax, and forbid assets-branch workarounds. A thread-only request omits publication. If a publication preflight refuses attachment as well, the prompt still requires a private attach_file call without publication; a post-delivery publication error must not be mistaken for an attachment failure. No new git authority is granted.
  2. The bot sends the stored key and configured artifacts.retentionDays (default 30) to its Worker's internal POST /artifacts/publish-pr-image, authenticated with the existing internal artifact-copy bearer, never handed to an executor. Only outbound runs/<id>/out/<seq>-<name> keys qualify. The Worker requires stored image/png, a PNG signature and a nonempty object of at most 10 MiB. It bounds the streamed read, checks the actual length, and copies the validated bytes into published-pr/<random UUID>.png with publication, source and absolute expiry metadata. The Worker derives expiry from its publication clock plus the retention interval; missing or invalid retention, or an unrepresentable expiry, is refused before storage is read. It never publishes inbound objects or arbitrary URLs. PNG-only avoids SVG/HTML active content; this is a format check, not a secret scanner or malware detector. The agent must inspect/redact the screenshot before choosing public publication.
  3. GET /pr-images/<UUID>.png (and HEAD) is token-free and serves only a marked published copy, as image/png, inline, with nosniff, a restrictive sandbox CSP and Cache-Control: no-store. It accepts no arbitrary key, run id, token, filename or listing query. Missing, unmarked, malformed, expired and deleted copies answer 404. GET and HEAD check the stored expiry on every request and refuse at or after that instant, even while lifecycle deletion is pending; missing or invalid expiry metadata also fails closed. Other methods answer 405. The R2 bucket has no public domain; the ordinary /runs/:id/artifacts/<key> reader keeps its existing authentication and event-membership checks. This is a separate, narrow Worker route, not an exception to run-reader authorization.
  4. The URL has no expiring signature: it is stable across run completion and restarts for the copy's retention window. The reader enforces the artifacts.retentionDays interval (default 30) captured at publication. Later config changes do not extend that stored deadline. The existing bucket lifecycle deletes copies under the applied retention policy and aborts incomplete multipart uploads after one day; public reads do not wait for deletion. It is not a permanent archive. An operator can revoke it earlier by deleting the named published-pr/<UUID>.png object; ordinary artifacts are separate copies. Orphans after a lost response are cleaned by the same lifecycle. No application or edge cache is requested; GitHub/camo and anyone who fetched a public image may retain their own copy, so revocation cannot retract already downloaded bytes. No sweeping job or public-bucket setting is added.
  5. Deployments must expose /pr-images/* without an Access challenge (a path-specific edge bypass if necessary), while keeping dashboard/run paths protected. The publication endpoint still requires its internal bearer. No R2/public-domain change is allowed. A deployment without the artifact store cannot publish and must report that limit, not fall back to another push or expose an authenticated run URL.

Validation criteria ​

CriterionProof
Source keys, PNG MIME/signature, declared and actual size are checked; copies use an opaque fixed-shape URL[unit] src/artifacts/prImages.test.ts::PR image validation::*
Authenticated publication reads R2 host object fields without relying on enumeration and writes a separate marked copy; anonymous reads serve only published PNGs; refused requests expose no private object or storage error detail[unit] deploy/cloudflare/prImages.test.ts::PR image routes::*
R2 uses only the internal bearer and verifies the returned path; memory publication enforces the same content restrictions[unit] src/artifacts/store.test.ts::PR image publication::*
Store construction sends configured retention or its default to publication[unit] src/artifacts/buildStore.test.ts::buildArtifactStore (item 20)::publication receives configured retention or the artifact default
Explicit publication follows thread delivery, is repo-gated and off by default, and reports partial failures honestly[unit] src/tools/attach.test.ts::attach_file PR publication::*
Both coding prompts name the permitted path, not an assets push[unit] src/agents/registry.test.ts::…::a screenshot request with no destination named means both the thread and the pull request…
Every requested capture renders both in its thread and inline on the PR, with private artifacts still refused[agent] Run the acceptance procedure below after deployment; no live receipt is claimed by the unit tests.

Live acceptance — deployment gate ​

  1. After deploying the bot Worker and container, verify the artifacts bucket remains private with artifacts check, its lifecycle with artifacts lifecycle --dry-run and an operator read-back of the applied rules. Verify any Access bypass is only /pr-images/*, not /runs/* or the whole host.
  2. Start a bound-branch coding run requesting two harmless PNG captures (light and dark) on a test PR. Inspect/redact the captures; call attach_file with publishForPr: true for each. Record run id, thread, PR, exact head, each tool result and URL. Confirm both PNGs arrived inline in the requesting thread, not merely as links. Each URL must be absolute HTTPS, with no live token, signature or credentials.
  3. Put ![light](<returned URL>) and ![dark](<returned URL>) in the PR description's validation or a PR comment. In an ordinary GitHub browser session inspect the rendered PR: both images load through camo. Record the PR URL, head, image count and browser/network receipt. Repeat after the run finishes. No assets branch or extra pushed ref may appear.
  4. Fetch each public URL without cookies or authorization: 200, image/png, inline, nosniff; check decoded dimensions and image contents. Anonymous requests to the original /runs/:id/artifacts/<key>, an inbound key, an unpublished PNG and a guessed public id must not yield image bytes. Calling the internal publication endpoint without its bearer must be 401. An SVG/HTML file named .png and an over-cap PNG must be refused, with no public URL.
  5. Separately, in a disposable deployment with a short configured retention, verify both GET and HEAD become 404 at the stored expiry even if the R2 object is still present; neither may return image bytes. Record publication time, expiry and observed statuses. For revocation, delete an unexpired test publication using the operator's R2 controls. Its anonymous URL must become 404; the other image and private run artifacts must be unaffected. Record that previously cached external copies cannot be revoked. Remove the remaining test publication after recording the receipt, and keep the run/PR receipt rather than claiming permanent image retention.