Skip to content

fix(producer): render servers only load assets from inside the project - #7579

Merged
jrusso1020 merged 4 commits into
mainfrom
fix/render-confine-outside-assets
Nov 12, 2026
Merged

jrusso1020 merged 4 commits into
mainfrom
fix/render-confine-outside-assets

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What

Render servers (the producer HTTP handlers) and the distributed planner now load assets only from inside the project they are asked to render. Local hyperframes render, --start/--end range renders, hyperframes benchmark and Studio renders keep importing outside files such as ../shared/logo.png exactly as before.

Why

When markup points at a file outside the project directory, the compiler copies it into the render and serves it to the page. That is the right behaviour for a local render of your own project, which often shares a logo or font folder with sibling projects. A render service, though, should only read the project it was handed, so the producer needs a way to tell the two apart, with the service path as the default.

Related work

None.

How

  • New opt-in, allowOutsideProjectAssets, on RenderConfig, CompileForRenderOptions and the planner options (plan() / buildLocalExecutionPlan). It is off by default. It is deliberately not a field on the serializable render request or DistributedRenderConfig: renderConfigFromRequest takes it as a runtime argument and sets it explicitly, so request JSON cannot turn it on.
  • With it off:
    • no hf-ext/ copies are recorded;
    • an entry file or sub-composition that leads outside the project once symlinks are resolved is refused with "outside the project folder", the same as a ../ sub-composition already is. This covers the pre-flight check, the sub-composition compile and the inliner, and the entry read in the compile stage and in compileForRender;
    • compile-time media probes, GIF/HEIC prep and local font embedding skip any path that leaves the project once symlinks are resolved. The compile-time advice no longer probes a file that does not exist;
    • the compile and probe stages treat as missing any composition video, audio or image whose source resolves outside the project dir, the compiled dir or the render's own download dir. That includes absolute paths, file: URLs, ../ and percent-encoded ..%2F paths, and symlinks. A warning names each one as a missing source, and it never reaches ffprobe or ffmpeg. The frame extractor, audio mixer, media-type preflight, HDR probes and video-layer passthrough all read these filtered entries, and the check also covers sources a page script sets at runtime. Remote media fetched during the render still works;
    • the render file server checks project files by real path (compiled files stay lexical, since extracted-frame directories are linked in);
    • the planner's project copy skips symlinks that lead outside the project.
  • Trusted callers pass true: the CLI local render and range render, hyperframes benchmark, the CLI Studio server render and the Vite Studio adapter render. The HTTP handlers, planV2 / planV2WithPublisher and the cloud adapters pass nothing.
  • The regression harness reads renderConfig.allowOutsideProjectAssets from a fixture's meta.json. render-symlinked-assets sets it, because that fixture's src/shared link to a sibling folder is the trusted local case it exists to keep working.
  • Preview bundling (bundleToSingleHtml) already inlines only real-path project files and is unchanged.
  • Package size: the @hyperframes/producer unpacked budget goes from 3,165,274 to 3,210,000 bytes.
    • The tarball grows about 12.5 KB unpacked over main, all in shipped source. The new checks live in the compiler and render stages, and the index, public-server and distributed bundles each include those, at about 3.5 KB per bundle, plus typings.
    • main itself was within 400 bytes of the 1% tolerance.
    • No test files ship in the package.

No visible change

The one Studio line (packages/studio/vite.adapter.ts) passes the new opt-in, so Studio renders keep importing outside files exactly as before. Nothing on screen changes.

Test plan

  • Unit tests added/updated
    • New outsideProjectAssets.test.ts. Every node:child_process call is recorded, and the media files outside the project are real clips, so a probe or extraction of one would succeed and be visible.
      • In-process default: the project reaches outside files through <img src="../…">, a <style> url(), an inline-style url(), a project symlink, and video/audio sources given as an absolute path, a file: URL, ../ and ..%2F.
        • There is no hf-ext/ entry, and the file server returns 404 for each path.
        • Each outside media element is reported as a missing source, and no recorded ffprobe/ffmpeg call names an outside file.
        • An https video in the same composition is still downloaded and its frames extracted.
      • Plan v2 default: no artifact carries hf-ext or the outside image, no ffprobe/ffmpeg call names an outside file, the remote clip is published, and a materialized chunk's file server returns 404.
      • A sub-composition symlinked to a file outside the project, and an entry index.html symlinked the same way, are refused both in-process and by Plan v2.
      • Trusted opt-in: the in-process compile and plan() still import outside files, and the symlinked sub-composition is compiled in.
    • Mutation checks:
      • Reverting all producer source to main fails the four default tests, and the opt-in test still passes.
      • Each guard reverted alone fails the test that covers it: the file server check, the media drop, the plan copy filter, the compile-time probe guard, the sub-composition check, and the entry checks.
    • The fileServer.test.ts symlinked-directory test now passes the opt-in and also checks for a 404 by default. The probeStage.test.ts fixture uses an existing project dir.
  • Manual testing performed
    • Ran a full in-process render (executeRenderJob, headless Chrome) of a composition whose only image is ../shared/logo.png, a solid red PNG. With the opt-in, the output frame is red (253,0,0). Without it, the frame is black.
    • Ran the render-symlinked-assets regression fixture locally in-process and distributed-simulated.
      • With the flag, shared/brand.css loads and the frame matches the golden's layout.
      • Without the flag, the stylesheet 404s and PSNR drops to 6.4.
      • Off the regression Docker image, both this branch and unmodified main score 20.77 dB on this host because of system fonts. CI's image is the reference.
    • Typecheck passes for producer, cli and studio.
    • Related producer, cli and studio suites pass. The only failures seen were 60s/5s timeouts under host load. They also fail on unmodified main here, or pass when re-run alone.
  • 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

— Rames Jusso

🤖 Generated with Claude Code

The HTTP render handlers and the distributed planner now compile with
outside-project imports off: markup that reaches a file beyond the project
directory (through ../, an absolute path, a file: URL or a symlink) is no
longer copied into the compiled directory, served to the page, probed or
extracted. Local `hyperframes render`, `--start/--end` range renders,
`hyperframes benchmark` and Studio renders pass allowOutsideProjectAssets
and keep importing outside files exactly as before.

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

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 109 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)

@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 · 3 blockers · 0 important · 0 nits · head 1442930

  • Required regression is red (shard 2, render-symlinked-assets). That fixture's src/shared -> ../shared pins render/preview parity for a symlinked sibling folder, and the harness (regression-harness.ts:1160) renders without the opt-in. So /shared/brand.css 404s and all 100 frames fail (PSNR 6.4). Either have the harness pass the opt-in for this suite (for example a renderConfig flag in meta.json) or re-scope the fixture. Fail-fast cancelled the other shards, so they still need a full run.
  • Required Build is red: @hyperframes/producer unpacked size 3205746 bytes is over its budget of 3165274 by more than 1%. Main passes at the merge base. Either raise scripts/package-size-budgets.json and give the reason in the body, or find what grew the tarball.
  • Sub-composition symlink escape, see the inline comment.
Scope

I read all 21 files. outsideProjectAssets.test.ts passes 3/3 at the head. With the producer sources reverted to main, the two default tests fail, which matches the body. The fileServer and probeStage bun suites pass (133). I checked the other createFileServer and compile callers: the chunk and frame-cost servers serve the dereferenced plan copy, and server.ts and planV2 get the default. Windows render verification, CodeQL, Comments and PR captures are green.

— Genesis

jrusso1020 and others added 3 commits November 12, 2026 01:57
…l render

The render-symlinked-assets fixture links src/shared to a sibling folder
outside the project to keep local renders working like preview. The
regression harness now reads renderConfig.allowOutsideProjectAssets from a
fixture's meta.json and passes it to the in-process and
distributed-simulated renders, and this fixture sets it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nside the project

Without the trusted-local opt-in, a sub-composition or entry file that
leads outside the project once symlinks resolve is refused, the same as a
../ sub-composition already is. Local media that resolves outside the
project, compiled and download folders, including absolute paths, file:
URLs and percent-encoded ../ paths, is reported as a missing source and
never probed or extracted. Remote media fetched during the render keeps
working. A missing file is no longer probed for the compile-time advice.

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

The producer tarball grows by about 12.5 KB unpacked: the new checks live
in the compiler and render stages, which the index, public-server and
distributed bundles each include. No test files ship in the package.

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 · 3 resolved, 0 open, 0 new · 1442930 → 6a92e3b

Checked

At this head, a scratch test refuses a symlinked scene and a scene inside a symlinked folder by default, and the opt-in still inlines both. outsideProjectAssets.test.ts and the producer compile, stage and fileServer suites pass. The only local failures are 2 HEIC cases, which fail on main in this sandbox too. CI, regression, Windows render verification and CodeQL are green.

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 03ddfb1 Nov 12, 2026
139 of 140 checks passed
@jrusso1020
jrusso1020 deleted the fix/render-confine-outside-assets branch November 12, 2026 02:50
jrusso1020 added a commit that referenced this pull request Nov 13, 2026
… 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]>
meefs pushed a commit to meefs/hyperframes that referenced this pull request Nov 13, 2026
…om outside the project (heygen-com#7647)

* fix(producer): local renders bring in only media, font, CSS and JS from 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]>

* fix(cli): keep outsideFileType as a re-export of the shared module

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]>

* chore(producer): raise the unpacked size budgets for the outside-file 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 heygen-com#7579 raised producer's budget for the project-folder checks.

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

* fix(producer): apply the outside-file policy to embedded local fonts

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]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
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