Skip to content

Refuse agent uploads over the hosted Clips size limit up front - #7697

Merged
3mdistal merged 6 commits into
mainfrom
t3/clips-upload-size-limits
Oct 19, 2026
Merged

3mdistal merged 6 commits into
mainfrom
t3/clips-upload-size-limits

Conversation

@3mdistal

@3mdistal 3mdistal commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Agents upload local video files to Clips with create-recording-upload (added in #7571). The tool description and its sizeBytes field say a file can be up to 2 GiB. That is true only for accounts with their own video storage connected (Builder.io or S3-compatible).

Accounts on Clips' free hosted storage (#7624) have a much smaller per-clip limit: 250 MB by default, set by CLIPS_HOSTED_STORAGE_MAX_CLIP_BYTES. That limit is a storage budget for the free tier, not a technical ceiling, and this PR keeps it as it is. But nothing told the agent about it. A hosted account could start uploading a 600 MB file, have the chunk route accept the first ~250 MB, and then fail partway through with "Recording exceeds the 250 MB size limit". The agent had no way to know beforehand, and the user was left with a failed clip.

Approach

State the real limits up front, and refuse a file that can't fit before a hosted grant is reserved or the recording is created. (The small recovery-policy row described under Safety is still written first.)

The agent already sends sizeBytes when it asks for an upload. Which limit applies depends on where the clip will be stored, and that decision is made in one place: resolveClipsVideoStorage. It picks the user's own storage when one is connected, and otherwise reserves one of the account's free hosted clips. The size now travels through create-recording to that resolver. If the clip would go to hosted storage and the file is over the hosted limit, the resolver refuses with a 413 and error code upload_too_large before it reserves the free-clip grant. So a refusal never has to release anything, and can't leave a grant held by no recording. The message gives the caller's actual limit and says that connecting your own storage raises it. For connected storage, create-recording's schema caps sizeBytes at MAX_UPLOAD_BYTES, as the upload action's schema already did.

The chunk route still enforces the limit on the bytes it actually receives. This PR adds an earlier refusal; it doesn't replace that one.

What changed

  • Tool description: create-recording-upload's description and its sizeBytes field now state both limits. They give the connected-storage limit as configured (MAX_UPLOAD_BYTES, 2 GB unless CLIPS_MAX_UPLOAD_BYTES changes it) and the hosted limit as 250 MB by default. A refusal always states the configured limit.
  • Early refusal:
    • resolveClipsVideoStorage takes an optional sizeBytes and refuses an oversized hosted file before reserving. It checks only after confirming the account would actually use hosted storage, so accounts with their own storage, or with hosted storage turned off, are unaffected.
    • The error's details carry sizeBytes, maxUploadBytes and storageMode.
    • create-recording passes sizeBytes through, and its schema caps it at MAX_UPLOAD_BYTES.
  • Sizes in messages: a small shared formatUploadBytes in shared/upload-limits.ts rounds a file's size up and a limit down, so a file one byte over the limit reads "250.1 MB … up to 250 MB", never as fitting.
  • Skill doc: the Clips recording skill's agent-upload section now describes both limits and where the refusal happens.
  • Changelog: a fixed entry in Clips' changelog.

Browser uploads are unchanged. The record page already checks hosted limits on the client, and it doesn't send sizeBytes.

Safety and operations

  • No schema change, no migration, no new configuration. The 250 MB default and its environment override are unchanged.
  • sizeBytes is optional on both create-recording and the resolver, so existing callers (recorders, desktop, extension, mobile, Loom import, chunk reset) behave as before.
  • A refused file reserves no hosted grant.
  • Like the existing "no video storage" refusal, a size refusal leaves the small recovery-policy app-state row that create-recording writes before it resolves storage. Nothing deletes these rows for any recording today, and moving the write after storage resolution would let a failed write strand a hosted reservation. So this PR leaves the order alone.

Verification

  • New resolver tests (video-storage.hosted.test.ts): on hosted storage, a file one byte over 250 MB is refused with 413 upload_too_large, the limit in the message and details, and no grant reserved. A file of exactly 250 MB reserves and resolves as hosted. A 300 MB file resolves to the user's own storage when one is connected. With the check removed, the refusal test fails.
  • New create-recording test: the size is handed to the resolver, and when the resolver refuses, nothing is inserted and no upload state is written. create-recording-upload.test.ts asserts that the size is passed through.
  • Related tests: before the main-branch merge, every test touching storage resolution, recording creation, Loom import and chunk reset passed locally (10 files, 125 tests). After resolving the conflict while preserving the MCP annotations from main and the limit description, the targeted upload, recording-policy and hosted-storage tests pass (3 files, 35 tests).
  • Typecheck: pnpm typecheck for Clips passes with no errors.
  • Guards: every guard passed except guard:i18n-catalogs. It couldn't import three templates outside Clips (Assets, Content and Slides), because this worktree only had Clips' dependencies installed, and it reported nothing in Clips. CI runs it on a full install. guard:i18n-changed-copy passes.
  • Not tested: a deployed hosted account. The refusal happens before any storage call, and the tests cover it.

Review focus

  • Where the check lives: is resolveClipsVideoStorage the right place? It is the only place that knows both the storage mode and whether a hosted grant is about to be reserved.
  • Error wording for hosted accounts: an agent relays it to the user, so it suggests both options, connecting storage or trimming the video.

create-recording-upload advertised 2 GiB, but Clips' free hosted storage
caps a clip at maxClipBytes (250 MB by default), enforced only as chunks
arrive. The action now passes sizeBytes to create-recording, which refuses
a file over the storage's limit with 413 upload_too_large before creating
the row, and the tool description states both limits.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

builder-io-integration[bot]

This comment was marked as outdated.

A failed hosted-allowance release no longer replaces the 413 with a
generic error: it is logged and the refusal still reaches the caller.
Sizes now round up and limits down, so a file one byte over never reads
as fitting, and the tool text states the configured connected-storage
limit instead of a fixed 2 GiB.
@3mdistal

Copy link
Copy Markdown
Contributor Author

On the three low findings:

  • Configurable connected-storage limit: fixed in 178d67d. The tool description and the sizeBytes field now state MAX_UPLOAD_BYTES as configured (2 GB by default) instead of a fixed 2 GiB.
  • Rounding and units: fixed in 178d67d. A shared formatUploadBytes rounds a file's size up and a limit down, to one decimal, in MB or GB (1024-based, matching the app's other size messages). A file one byte over 250 MB now reads "250.1 MB … up to 250 MB", and a test covers it.
  • Recovery-policy row for a refused ID: left as is. create-recording writes that small app-state row before it resolves storage, and the existing "no video storage" refusal leaves one in the same way; nothing deletes these rows for any recording today. Moving the write after storage resolution would add a new way to leak a hosted reservation, because the write can throw after the reservation is taken. The PR body notes this.

builder-io-integration[bot]

This comment was marked as outdated.

Releasing a hosted grant after a size refusal could itself fail, and a
grant with no recording row is never reclaimed. resolveClipsVideoStorage
now takes the file size and refuses a file over the hosted limit before
it reserves anything, so there is nothing to release. create-recording's
schema caps sizeBytes at MAX_UPLOAD_BYTES for connected storage, and the
upload tool points agents at get-video-storage-status for the configured
hosted limit.
@3mdistal

3mdistal commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

On the two low findings in the second review:

  • Hosted limit in the tool text: the description still says "250 MB by default". It's a static string fixed when the module loads, and this PR doesn't add an app-config read to module load. It now points agents to get-video-storage-status, which already returns the live maxClipBytes. The refusal itself always states the configured limit.
  • Recovery-policy row: unchanged, for the reason in my earlier comment. The write happens before storage resolution, as it does for the existing "no video storage" refusal. Moving it later would let a failed write strand a hosted reservation.

builder-io-integration[bot]

This comment was marked as outdated.

create-recording still writes its recovery-policy row before it resolves
storage, so "before anything is created" overstated it.
@3mdistal

Copy link
Copy Markdown
Contributor Author

On the low finding in the third review: the wording was too broad, and I've narrowed it in 70df857. The tool description now says a larger file is refused "before the recording is created", and create-recording's sizeBytes field says "before a hosted grant is reserved or the recording is created". The recovery-policy row stays as it is, for the reason in my earlier comment. The PR body says so under Safety.

builder-io-integration[bot]

This comment was marked as outdated.

That action is not in Clips' compact MCP catalog, so an outside agent
reading this description cannot call it. The refusal already states the
configured limit.
@3mdistal

Copy link
Copy Markdown
Contributor Author

Correction to my earlier reply about the hosted limit in the tool text: I've taken out the get-video-storage-status pointer in ed6ff85. That action isn't in Clips' compact MCP catalog (beta's /mcp lists 12 tools, and tool-search only searches those), so an outside agent reading the description couldn't call it. The description now just says 250 MB by default. A refusal always states the configured limit.

builder-io-integration[bot]

This comment was marked as outdated.

@3mdistal

Copy link
Copy Markdown
Contributor Author

Both low notes in the ed6ff85 review are known and left as they are on purpose. The recovery-policy row is explained in my earlier comment and in the PR body under Safety. On the hosted limit's source: no tool in Clips' compact MCP catalog reports the configured maxClipBytes, so the description gives the default, and a refusal states the configured value.

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and has a few items to flag 🟡

Review Details

Incremental review of the latest PR #7697 diff: the size check still runs only on the hosted-storage path and before hosted grant reservation; connected-storage uploads bypass it, and the existing byte limit remains consistent with chunk/finalize enforcement. The prior hosted-quota-release concern therefore remains addressed. Both reviewers confirmed one remaining low-severity issue: recovery-policy state is snapshotted before storage resolution can reject an oversized upload, leaving an orphaned app-state row for that recording ID. I found no further actionable issue in the current diff. Risk remains Standard because this changes upload validation and quota behavior.

🧪 Browser testing: Skipped — the PR changes backend storage resolution, action schemas, tests, and agent guidance; no browser UI impact.

@3mdistal
3mdistal merged commit 1532424 into main Oct 19, 2026
42 of 43 checks passed
@3mdistal
3mdistal deleted the t3/clips-upload-size-limits branch October 19, 2026 17:24
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