fix(producer): serve symlinked render assets#486
Conversation
f920d79 to
9677c6f
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Symlinked render-asset fix verified end-to-end. Cherry-picked the new createFileServer symlink test onto pre-PR main and confirmed it produces the exact [FileServer] 404 Not Found: /shared/brand.css Nate hit; on this branch it returns 200 with the expected stylesheet content.
The fix drops { resolveSymlinks: true } from two isPathInside calls but keeps path.resolve() normalization, so URL-layer .. traversal is still blocked (the existing isPathInside traversal tests cover this). This brings producer's render FileServer into parity with engine's preview FileServer, which has no containment check at all and was already following symlinks — exactly the asymmetry that produced the bug.
The regression-harness copyFixtureSupportFiles change is well-scoped: walks the suite dir excluding {src, output, meta.json, failures} and copies the rest to tempRoot before the existing src copy. Existing fixtures (which only contain those excluded entries) are no-op; the new fixture's shared/ directory lands at tempRoot/shared/ so src/shared -> ../shared resolves at render time.
CI for the latest commit:
- Lint / Format / Typecheck / Build / Test / Test: runtime contract / Smoke / Tests on windows / Render on windows / CodeQL / Semantic PR title — all ✅
- regression-shards (render-compat) ✅; style-prod shards still finishing but unrelated to FileServer/harness scope
Security trade-off acknowledged correctly in the PR body: in-project symlink to /etc/shadow is no longer rejected by realpath check, but (a) preview already allowed this, (b) the threat model is the project author themselves on localhost, (c) URL-layer traversal is still blocked.
Non-blocking suggestion (worth a quick follow-up, not gating): a createFileServer integration test asserting the URL-level traversal block still works (fetch(/shared/../../etc/passwd) → 404) would make the security boundary explicit at the integration level. The lower-level isPathInside tests cover it but it'd be nice to assert it on the actual server.
— Rames Jusso
Problem
The AIS ads repro exposed a render-mode parity bug around shared project assets.
Those compositions can keep
shared/inside each ad folder as a symlink to a sibling../shared/directory.hyperframes previewfollows that symlink, soshared/brand.cssloads and the authored glass cards, reveal wrappers, and layout primitives render correctly.hyperframes renderwas different. The producer FileServer resolved the requested file throughrealpathbefore checking containment under the project root. Forshared -> ../shared, that turned a project-local request like/shared/brand.cssinto a real path outside the ad folder, so the FileServer returned404 Not Found.Without
brand.css, the visual result is not a small styling drift: the AIS card chrome disappears, centered reveal layout breaks, and dark callouts can render as blunt overlays on top of the source video.What this fixes
compiledDirandprojectDir..requests that normalize outside the intended rootshared -> ../sharedasset layoutsrc/, so fixtures can model sibling shared asset folders without copying those assets intosrc/Root cause
The previous hardening mixed two separate concerns:
The second behavior is stricter than preview mode and broke a real composition-authoring pattern. The request path itself was still inside the project root (
shared/brand.css), but the real path pointed at the sibling shared folder, so render rejected an asset the browser preview could load.This PR keeps the URL traversal guard at the request-path layer and lets the filesystem follow the symlink when reading the file.
The regression harness needed one small companion change because it previously copied only each fixture's
src/directory into the render temp dir. That loses the target for a fixture likesrc/shared -> ../shared. Copying fixture-level support files preserves the same sibling-folder shape during the render test.Verification
Local checks
bun test packages/producer/src/services/fileServer.test.tsbun run build:hyperframes-runtime:modularbun run --filter @hyperframes/producer typecheckbunx oxlint packages/producer/src/services/fileServer.ts packages/producer/src/services/fileServer.test.ts packages/producer/src/regression-harness.tsbunx oxfmt --check packages/producer/src/services/fileServer.ts packages/producer/src/services/fileServer.test.ts packages/producer/src/regression-harness.ts packages/producer/tests/render-symlinked-assets/meta.json packages/producer/tests/render-symlinked-assets/src/index.html packages/producer/tests/render-symlinked-assets/shared/brand.css packages/producer/tests/render-symlinked-assets/output/compiled.htmllint,format,typecheckCI-matching regression checks
docker buildx build --platform linux/amd64 -f Dockerfile.test -t hyperframes-producer:test-amd64 --load .docker run --rm --platform linux/amd64 --security-opt seccomp=unconfined --shm-size=4g -v "$PWD/packages/producer/tests:/app/packages/producer/tests" hyperframes-producer:test-amd64 --update --sequential render-symlinked-assetsdocker run --rm --platform linux/amd64 --security-opt seccomp=unconfined --shm-size=4g -v "$PWD/packages/producer/tests:/app/packages/producer/tests" hyperframes-producer:test-amd64 --sequential render-symlinked-assetsdocker run --rm --platform linux/amd64 --security-opt seccomp=unconfined --shm-size=4g -v "$PWD/packages/producer/tests:/app/packages/producer/tests" hyperframes-producer:test-amd64 --sequential --exclude-tags slow,render-compat,hdrTotal: 9 | Passed: 9 | Failed: 0Repro verification
Verified against the cloned private repro
aidan785/ais-ads-hyperframes-bug-reprowithannual-upsell-2/shared -> ../shared:404for/shared/brand.css200and includes the expected.aisplus-glassstylesheet contentRender / browser verification
/Users/miguel07code/.codex/artifacts/ais-ads-render-fix/symlink-render-check.mp4/Users/miguel07code/.codex/artifacts/ais-ads-render-fix/symlink-render-frame.pngagent-browseragainst a local render-mode FileServer page and confirmed computed styles from the symlinked stylesheet (backgroundColor: rgb(33, 197, 93), textSYMLINK CSS)/Users/miguel07code/.codex/artifacts/ais-ads-render-fix/agent-browser-symlink-css.png/Users/miguel07code/.codex/artifacts/ais-ads-render-fix/agent-browser-symlink-css.webmNotes
packages/producer/tests/render-symlinked-assets/output/output.mp4, about 10 KB)linux/amd64Docker image to match the GitHub Actions regression runner