Skip to content

fix(producer): local renders bring in only media, font, CSS and JS from outside the project - #7647

Merged
jrusso1020 merged 4 commits into
mainfrom
fix/local-render-outside-file-policy
Nov 13, 2026
Merged

jrusso1020 merged 4 commits into
mainfrom
fix/local-render-outside-file-policy

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Local renders now apply publish's outside-file policy. Files outside the project are still brought in, but only media, font, CSS and JS files, and never from private folders such as hidden folders, env files, Git data, system folders or per-user app data. Each file that is left out is named in a warning that uses publish's wording, ending "left out of the render".

Claude Opus 5.5

What

hyperframes render, range renders, hyperframes benchmark, the Studio server's render and the Vite Studio adapter all render with allowOutsideProjectAssets. Until now, that copied every existing file that markup named outside the project into compiled/hf-ext/, whatever its type or folder, and served project links that lead outside the project without checking where they lead. This PR makes local renders follow the policy publish already uses:

  • References outside the project are kept.
  • Files in private locations are left out. That covers hidden folders, env files, Git data, system folders and per-user app data.
  • Only image, video, audio, font, CSS and JS files are brought in.

Why

Publish and local render should agree on what a project may pull in from around it. Until now a local render brought in any outside file a project named, of any type and from any folder, while publish already limited both.

Related work

Applies the publish policy from packages/cli/src/utils/privateProjectFile.ts and outsideFileType.ts to local renders.

How

Shared policy. The private-location predicate, the copied-type list and the left-out wording move from the CLI to a new @hyperframes/studio-server/outside-file subpath. The producer cannot import the CLI, and it already depends on studio-server. The media extension sets move with them, unchanged (100% rename), because the copied-type list includes them. The CLI imports or re-exports everything from the new subpath, so publish behaves exactly as before. cli/src/utils/outsideFileType.ts stays as a two-name re-export, so publish's imports are unchanged. leftOutWarning gained a from argument ("upload" by default).

One enforcement point in the producer. outsideAssetRefusal(projectDir, path) in producer/src/utils/paths.ts returns blocklisted, file_type or null for a path whose real location is outside the project. Every place where allowOutsideProjectAssets lets an outside file in now calls it. Callers only pass allowOutsideProjectAssets: true, so no entry point can skip the check.

Where What it covers
collectExternalAssets (htmlCompiler) every src/href/url() reference, including <a href> and <link href>, spelled as ../ or an absolute or drive path
dropMediaOutsideProject (now also called in trusted mode, from compile and probe) video, audio and image sources in any spelling the media resolver accepts (file: URLs, percent-encoding), so nothing extracts frames from them
render file server project links that lead outside the project. The server returns 404 and logs a warning
local @font-face embedding project font files that link outside the project. The font is not inlined into the compiled HTML, and a warning names it
distributed plan's project copy the same links, for range render (renderSpan → plan)
GIF/HEIC prep, sub-composition links the private-location rule only, because these inputs are converted or compiled, not copied as they are. An outside sibling HEIC still converts as before. A sub-composition that links to a private location fails with a clear message

Entry points: render, benchmark, the Studio server and the Vite adapter all go through executeRenderJob, which runs compileStage, probeStage and the file server. Range render goes through renderSpan, which runs plan and then compileStage.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

New tests in packages/producer/src/services/outsideProjectAssets.test.ts:

  • Leave out private and non-media files. The fixture project references a .ssh folder two levels up, using ../, an absolute path, a file: URL, a percent-encoded path, <link href>, <a href>, CSS url() and inline-style url(). It also has a video, an animated GIF, a project link into .ssh, @font-face files that link to a .ssh file and to notes.txt, ../shared/notes.txt, ../shared/logo.png and a project link to that logo. The test checks that compiled/ holds only index.html and the logo, that the composition has no videos, that no ffmpeg extraction runs on the .ssh files, that each skip is a named warning, that the file server returns 404 for the private copies and the link and 200 for the logo and its link, and that the plan copy for range render contains nothing from .ssh, no notes.txt and no private link.
  • Refuse a linked composition in a private location.

Results:

  • Before and after. Both new tests fail against main's producer sources and pass with this change.
  • Mutation checks. I reverted each enforcement point on its own (collect, media drop, file server, plan copy, font embedding, GIF prep, sub-composition, type check), and each revert makes a test fail.
  • Existing tests. "trusted local renders still import outside files" and the other outside-asset tests stay green, as do the related producer suites (htmlCompiler, fileServer, plan, assetMediaType, GIF/HEIC prep, compile and probe stage, paths) and the CLI publish and offload suites.
  • render-symlinked-assets. No left-out warnings and no 404s. Locally, its pixel comparison fails by the same 100 frames on main and on this branch, which points to my box rather than this change.
  • Manual. I ran the built CLI's hyperframes render --debug on a project like the test fixture. The render succeeded, every left-out file was named in a warning, compiled/hf-ext/ held only shared/logo.png, and the private file's contents appeared nowhere in the work directory.
  • Checks. typecheck (studio-server, producer, cli, studio), oxlint, oxfmt, the comment ratchet, package-cycles, package-subpaths, workspace-contracts and cli-lazy-deps all pass.

Size budgets. scripts/package-size-budgets.json raises the unpacked budgets: producer goes from 3,210,000 to 3,245,000, and studio-server from 646,000 to 654,000. Both were already at the 1% edge on main. Measured locally with npm pack --dry-run, main is 3,237,162 for producer and 652,502 for studio-server. This branch adds about 7.9 KB to producer, because the checks sit in code that every producer entry bundles. It adds about 5.5 KB to studio-server for the new outside-file entry and its declarations. No source maps, tests or assets were added. #7579 raised producer's budget the same way.

No visible change

This change is in the producer, studio-server and CLI only. No UI changes.

— Rames Jusso

🤖 Generated with Claude Code

…om outside the project

Local renders (`render`, range render, `benchmark`, the Studio server and the
Vite Studio adapter) run with allowOutsideProjectAssets, which copied every
existing file that markup named outside the project into compiled/hf-ext, of
any type and from any folder, and served project links that lead outside
without looking at where they lead. Publish already applies a policy here:
outside references are kept, private locations are left out, and only image,
video, audio, font, CSS and JS files are copied. Local renders now follow the
same policy.

The publish predicates (private location, copied file type, left-out wording)
move from the CLI to a new `@hyperframes/studio-server/outside-file` subpath,
since the producer cannot import the CLI and already depends on studio-server.
The CLI re-exports them, so publish behaves as before. The media extension
sets move with them because the copied-type list includes them.

The producer decides with one predicate, `outsideAssetRefusal` in
utils/paths.ts, and applies it wherever allowOutsideProjectAssets lets an
outside file in, so no entry point can skip it:
- collectExternalAssets: src/href/url() references, which covers <a href> and
  <link href>
- dropMediaOutsideProject: video/audio/image sources in any spelling the media
  resolver accepts (file: URLs, percent-encoding), so nothing probes or
  extracts them
- the render file server: project links that lead outside
- the distributed plan's project copy: the same links (range render)
- GIF/HEIC prep and sub-composition links: private locations only, since
  those inputs are converted or compiled rather than copied as they are

Each left-out file logs the publish wording with "left out of the render".

To reproduce on main: a project whose index.html references
../../.ssh/id_key through <link href>, <a href> and url(), with a project
link to a file in .ssh, rendered locally, gets those files copied into
compiled/hf-ext and served.

Comment lines: the four in studio-server's outsideFile.ts moved verbatim from
the CLI files they replace. Two are new, in utils/paths.ts. The one on
outsideAssetRefusal states the policy once, so its six call sites carry no
comment. The one on refusesOutsideLink says its boolean means "refused, and
already warned", which the name alone does not say. Comment ratchet is clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 83 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

jrusso1020 and others added 2 commits November 13, 2026 09:39
The base repository's deletion guard rejects removing a file that exists on
main unless git pairs it as a rename. outsideFileType.ts moved into
studio-server/outside-file with enough added around it that git no longer
pairs the two, so CI's "Reject accidental file deletions" step failed.

The file stays as a two-name re-export and publish keeps importing from it,
so publishProject.ts and its file-type test are back to main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… policy

CI's packed-manifest check measured @hyperframes/producer at 3,244,665 bytes
unpacked (budget 3,210,000) and @hyperframes/studio-server at 653,506
(budget 646,000), both just past the 1% tolerance.

Both packages were already at the edge on main. Measured locally with
npm pack --dry-run, main is 3,237,162 for producer and 652,502 for
studio-server, so studio-server on main is already at its ceiling. This
branch adds about 7.9 KB unpacked to producer, because the new checks sit in
the compiler, render stages and file server, which each producer entry
bundles. It adds about 5.5 KB to studio-server for the new outside-file
entry and its declarations. No source maps, tests or assets were added to
either tarball.

The budgets go to 3,245,000 and 654,000, just above CI's measurements, the
same way #7579 raised producer's budget for the project-folder checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@heygengenesis heygengenesis 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.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head 09324191

  • blocker — packages/producer/src/services/htmlCompiler.ts:2001 (embedLocalFontFaces, not in the diff): a trusted render still reads a project file that is a link to a private location and puts its bytes into compiled/index.html, with no warning. In trusted mode assetRoot is undefined, so the only check is the lexical isPathInside. Repro, run with vitest at this head: project/brand.ttf → <root>/.ssh/id_key and <style>@font-face { src: url("brand.ttf") }</style>, then runCompileStage with allowOutsideProjectAssets: true. Result: compiled/index.html contains url("data:font/ttf;base64,UFJJVkFURS1LRVktQ09OVEVOVFM="), which is the key's contents, and warnings is []. The file server and plan copy refuse that same link, but this path reads the file before either one runs, and on a range render the compiled HTML goes into the plan. This contradicts the PR's "every place where allowOutsideProjectAssets lets an outside file in now calls it" and the manual check that the private file's contents "appeared nowhere in the work directory". It also embeds a file_type refusal, such as a .ttf link to notes.txt, because the extension falls back to ttf. Fix: after the isPathInside check, if (outsideAssetRefusal(projectDir, absPath)) continue;, warned via leftOutOfRender, and add a font case to the private-outside fixture.
Scope

Read the full diff. Audited every allowOutsideProjectAssets/assetRoot gate in producer: compile/probe/plan/fileServer/GIF+HEIC prep/sub-compositions are covered. The remaining assetRoot readers (duration probe, VFR/keyframe advisories) only run ffprobe and copy no bytes. Ran outsideProjectAssets.test.ts locally: 7/7 pass. CI on head is green, and the cancelled runs were superseded.

A project font file that links outside the project is now left out of
font embedding with the same warning the file server gives, instead of
being inlined into the compiled HTML.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@heygengenesis heygengenesis 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.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · 09324191 → 81e62ed7. ✅ Font blocker fixed: embedLocalFontFaces now runs refusesOutsideLink, and my .ssh symlink repro no longer inlines the key. CI on the head is green.

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 2c2b399 Nov 13, 2026
139 of 140 checks passed
@jrusso1020
jrusso1020 deleted the fix/local-render-outside-file-policy branch November 13, 2026 16:46
@heygengenesis heygengenesis Bot mentioned this pull request Nov 13, 2026
1 of 2 tasks
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