Skip to content

fix: keep private project files out of preview bundles and local sharing - #7563

Merged
michaelwang11394 merged 7 commits into
mainfrom
harden-r4-studio
Nov 11, 2026
Merged

michaelwang11394 merged 7 commits into
mainfrom
harden-r4-studio

Conversation

@michaelwang11394

@michaelwang11394 michaelwang11394 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Private project files stay out of preview bundles, and local sharing only answers this machine.

  • Bundler. The HTML bundler never inlines git metadata or env files, whether a composition names them directly or reaches them through a link. A refused reference stays as the author wrote it.
  • Studio preview. A new bundle option, privateFiles: "studio", makes the bundler also refuse the app's records under .hyperframes/, as Studio's file routes do. It is a plain string, not a callback, so a host that builds on a worker thread can send it across and can still share one build between same-content requests.
  • Preview main file. When index.html (or <id>.html) is a link to an env file or into .git, the preview's disk fallback no longer serves it. A main file linked from outside the project is still served, because preview keeps reading through links.
  • Render. The render compiler no longer inlines git metadata or env files named by data-composition-src. The pre-render check names the refused reference and stops the render.
  • Thumbnail capture. Capture read grants are checked against the decoded path that the server resolves. An encoded / or \ can no longer hide a dot folder. A path that cannot be decoded is still refused.
  • Local sharing. The /api/share/* routes were already meant for this machine only. They now also require the connection itself to come from a loopback address. The existing header checks are unchanged.
  • Studio's private-file rule (git, env, and .hyperframes/ records) now lives once, in @hyperframes/core/safe-path, and studio-server reuses it.

Behaviour changes

  • A composition that references a .env* file or anything under .git gets the reference left as written instead of the file's text. A render whose data-composition-src names one stops with a message naming it. No template, example or test does this.

  • Sharing from a Studio that runs inside a container and is reached through a mapped port now gets a 403, because the connection does not come from loopback. Running Studio in a container is not documented.

  • Inspector size budget. The inspector already sat about 36.8 KB over its 3,684,092-byte unpacked budget, just inside the 1% tolerance. Moving Studio's private-file rule into core added a few hundred bytes, which pushed it to 3,721,419 bytes. This PR raises that budget to 3,722,000 in scripts/package-size-budgets.json.

Testing

  • core 247 passed (bundler and safe-path), studio-server 893 passed (preview and helpers), CLI share routes 34 passed.
  • fails without the fix: "never inlines git metadata or env files, spelled directly or through a link", "refuses the app's records only under the studio private-file policy", "does not inline project files no route serves", "asks for private files to be refused with plain data, so a build thread can join same-content builds", "does not serve an env file that index.html links to, but still serves a main file linked from outside", "compileForRender refuses to inline an env file named as a data-composition-src", captureLoads encoded-separator cases, and the shareRoutes peer cases, shown by reverting each fix locally (for the last preview test, by adding the old canRead callback back) and rerunning the test.
  • Ran the share routes on a real server. Local Studio still gets a token (200). A request from a LAN address with forged local headers gets 403.
  • Lint, format, comment ratchet and fallow pass. core and studio-server typecheck cleanly.
  • Bundled all 79 producer test projects with and without inlined assets, before and after this change. All 158 outputs are byte-identical. Publish (a zip of project files) does not go through bundleToSingleHtml. Render uses the producer's own compiler, which now refuses only git and env sub-compositions; its 178 compiler tests pass.
  • Names that look close to the rule are still inlined: environment.mp4, .github/..., .gitignore, .gitkeep, and a folder named git/. Any hidden name that starts with .env (such as .envelope.svg) is refused, which matches what Studio's file routes already did. Publish already skips hidden files.
  • Share routes, checked on a real server: the full Studio server under Bun on 127.0.0.1, and the share routes alone under both Node and Bun on 127.0.0.1 and ::1, all return 200 to a local request. The Studio dev server (Vite) does not serve /api/share, so it is not affected.
  • Capture grants still allow encoded names such as logo%20v2.png, caf%C3%A9.png, 100%25.png and a%23b.png.

🤖 Generated with Claude Code

michaelwang11394 and others added 3 commits November 11, 2026 09:35
The bundler now checks every file it reads against the project's private
paths (git metadata and env files, spelled directly or through a link)
and leaves a refused reference as authored instead of inlining it.
Bundle options gain a canRead predicate; Studio's preview passes its own
private-file rule so the preview document matches what the file routes
serve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A capture page's read grant now looks at the path the way the static
preview route decodes it, including encoded separators, so a URL that
resolves to a dotfile or into a dot-folder gets no CORS grant. Ordinary
project files keep loading in thumbnails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local share routes already require loopback Host and same-origin
headers. They now also require the connection itself to come from this
machine, so a Studio bound to a network address still keeps sharing
local to the person running it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

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

Unstable (1)

  • seqplainkeys-none-pct-r0-nested-z100: tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at c4ad9b17f09e0bddd7d15c0f55b1eb615c540dbf.

Important: the always-present canRead function at packages/studio-server/src/routes/preview.ts:529 changes the bundle-adapter contract for worker-backed hosts. Desktop’s existing worker-backed consumer was checked separately. A function-valued option cannot cross its worker boundary, so its adapter that falls back to its calling thread for unrecognized callbacks now builds every preview on that thread. Its option-keyed shared-build cache has the same callback exclusion. A separate replay of two concurrent same-content requests produced one build without canRead, and two builds with it. This becomes a host regression when adopting this package release.

I replayed the existing worker adapter with the same 20,000-path composition, alternating the route options with and without the new callback. The largest event-loop gaps were 10–11 ms without the callback and 707–782 ms with it. This is an integration regression on package adoption, rather than a change to the currently pinned Desktop release.

Concrete fix: represent the standard private-file policy as serializable bundle options and apply it inside the bundler/worker, rather than attaching a fresh callback to every preview. Alternatively, supply and verify a compatible host adapter before adoption. Keep the private-file protection, and add a regression asserting both worker execution/responsiveness and same-content build joining with the route’s new policy.

The loopback check itself is scoped to /api/share/*; project frame tokens and their preview paths remain separate. The read-guard mutation also makes both new bundler cases fail. Focused validation passed: bun run build, 220 bundler tests, 242 preview/capture/token-path tests and 34 share-route tests. The declared container mapped-port restriction is explicit.

Need: yes · Size: ok · Reuse: shared safe-path predicate and existing bundle adapter · Simpler: serializable standard policy instead of a per-request callback · Structure: fine (checked bundleToSingleHtml/read helpers, bundle adapter and worker/cache consumers; no split)

Risk: medium — standard preview builds acquire a callback that existing worker and cache consumers cannot preserve.

Verdict: REQUEST_CHANGES

Reasoning: the focused read-restriction and loopback-boundary tests pass, but the preview adapter needs to retain worker execution and build sharing.

— Rames

@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 · 1 important · 0 nits

Agree with Rames's worker-adapter finding. I didn't repeat it. This review is additive.

  • blocker: Studio preview still serves .env contents when index.html is a link to it. ↓ inline
  • important (follow-up): production renders don't use this rule. producer/src/services/htmlCompiler.ts:1097 (resolveCompositionSrc → readProjectFile) still inlines data-composition-src=".env" into the render page. The PR body says render doesn't go through the bundler, which is true. But that leaves the rule covering preview only, not every inlining path.
Scope

I read every source file in the diff. Checked and ruled out:

  • Bundler read paths: every project read goes through safeReadFile, safeReadFileBuffer or readAllowed.
  • Capture grant: it decodes once with the same decodeWellFormedEscapes the server uses, so double encoding and %5C don't get through.
  • Share routes: /api/share/* is mounted only in createStudioServer, which @hono/node-server serves, so c.env.incoming is always set there. IPv4-mapped loopback addresses are handled.

CI is green on this head.

— Claude (Genesis review session)

… plain data

The preview route passed a fresh `canRead` callback on every build. A host that builds on a
worker thread cannot send a function across, so it built on its own thread, and it could no
longer share one build between two same-content requests.

The bundler now takes `privateFiles: "studio"`, a plain string, and applies Studio's
private-file rule itself. That rule (git metadata, env files, and the app's records under
`.hyperframes/`) now lives in `@hyperframes/core/safe-path`, and studio-server reuses it.
The `canRead` option is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michaelwang11394

Copy link
Copy Markdown
Contributor Author

Thanks, Rames.

  • Per-request canRead callback breaks worker-backed bundle adapters and same-content build sharing: fixed in dbf7947. The callback is gone. The preview route now sends privateFiles: "studio", a plain string, and the bundler applies Studio's private-file rule itself. That rule (git, env, and .hyperframes/ records) moved into @hyperframes/core/safe-path, and studio-server reuses it. A new preview test uses a thread-style adapter. It checks that the route's options are plain data that survive structuredClone. It also checks that two same-content requests through two route instances produce one build. The test fails if the old callback is added back. The protection is unchanged: the bundler and preview private-file tests still pass.

michaelwang11394 and others added 2 commits November 11, 2026 10:20
…ate file

When index.html (or <id>.html) is a link to an env file or into git metadata, the
bundler refuses it and the route falls back to reading the main file from disk. That
fallback now skips a candidate that is a private project file, by the same rule the
file routes use. A main file linked from outside the project is still served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The render compiler resolved data-composition-src and inlined the file it named, so
`data-composition-src=".env"` put the env file's text into the render page. It now
refuses git metadata and env files with the same shared rule the bundler uses. The
pre-render check names the refused reference and stops the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michaelwang11394

Copy link
Copy Markdown
Contributor Author

Replies to the Genesis review:

  • Blocker: preview serves .env when index.html links to it. Fixed in 1b621ec. The disk fallback in resolveProjectMainHtml now skips a main file that is a private project file, by the same rule as the file routes, checked on both its spelled path and its real path. The new preview test fails without the fix. A main file linked from outside the project is still served, and the same test covers that. This is deliberate: preview keeps reading through links, because people share asset and project folders that way.
  • Important: render still inlines data-composition-src=".env". Fixed in 80cd6b4. The render compiler now refuses git and env sub-compositions with the shared rule from @hyperframes/core/safe-path, at all three reads. The pre-render check stops the render and names the reference. The new compileForRender test fails without the fix.
  • Agreed on the worker-adapter finding. It was addressed earlier in dbf7947.

The inspector already sat about 36.8 KB over its 3,684,092 budget, just inside the 1%
tolerance. Moving Studio's private-file rule into core added a few hundred bytes to the
bundles it ships, which pushed it to 3,721,419 bytes and over the tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 · 2 resolved, 0 open, 0 new · c4ad9b1 → db1fc64. CI is green on this head. Serving a main file linked from outside the project is the documented symlink-read decision, so I'm not raising it as a finding. Rames's worker finding is Rames's to clear.

— Claude (Genesis review session)

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at db1fc647aab804e028ce300cdba448972dd3a25e.

Verdict: APPROVE

The prior worker/cache blocker is resolved. Preview passes the plain privateFiles: "studio" option, and core applies that policy inside the build. The existing worker transports this data and the shared-build key includes it, so differently restricted builds do not share a cache entry. The shared Studio rule preserves its existing allowed files.

I read the complete delta since c4ad9b17f09e0bddd7d15c0f55b1eb615c540dbf, including Genesis's fallback and producer concerns: both main-file fallback candidates now check the private-file rule, and all three producer sub-composition reads apply the shared predicate. The intentionally retained external main-file link behavior is explicit.

Validation on this head:

  • bun run build passed; focused core tests 240 passed / 1 skipped, preview/capture/safe-path/token tests 253 passed, producer compiler tests 178 passed.
  • Restoring the previous route makes its two new targeted regressions fail; restoring the previous producer makes its new regression fail. Reviewed sources were restored.
  • A native Node 24 replay using the copied worker adapter and current route options kept worker execution. On the same 20,000-path fixture, peak main-loop gaps were 13–315 ms, versus 1,196–1,294 ms with the former callback added back. The copied shared-build implementation joined identical requests into one build, versus two with the callback. This is an adapter replay, not a full Electron walk or a claim that all UI stalls are gone.
  • Inspector npm pack --dry-run --json --ignore-scripts measured 3,721,910 unpacked bytes, below the new 3,722,000 budget. The budget rises 37,908 bytes, mostly absorbing the documented pre-existing overage.

The unchanged loopback share boundary remains separate from project frame-token paths. These render checks cover sub-composition reads; they do not assert a blanket policy over every producer file read. Independent code and review-gap passes found no new blocker.

Need: yes · Size: ok · Reuse: core safe-path predicate and existing worker/cache contracts · Simpler: serializable standard policy · Structure: fine (checked readAllowed, Studio private-file exceptions, producer read sites and adapter option keys; no split)

Risk: medium — changes preview/render read policy and cross-thread options; focused native tests and adapter replay pass, full Electron flow unexercised.

Reasoning: the measured worker/cache regression is fixed, the fallback and render deltas preserve the intended policy, and their targeted regressions distinguish the previous implementations.

— Rames

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 11, 2026
@michaelwang11394
michaelwang11394 added this pull request to the merge queue Nov 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 11, 2026
@michaelwang11394
michaelwang11394 added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit a243169 Nov 11, 2026
134 of 135 checks passed
@michaelwang11394
michaelwang11394 deleted the harden-r4-studio branch November 11, 2026 19:50
@terencecho terencecho mentioned this pull request Nov 11, 2026
3 of 4 tasks
terencecho added a commit that referenced this pull request Nov 11, 2026
…0.9.35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sshlg pushed a commit to ssheleg/hyperframes-agent that referenced this pull request Nov 11, 2026
* chore: release v0.9.35

* docs(release): note the render stop and container 403 from heygen-com#7563 in v0.9.35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants