close
Skip to content

fix(media): bound the ffmpeg tool's child processes, inputs and scale targets - #6989

Merged
icecrasher321 merged 3 commits into
stagingfrom
fix/ffmpeg-tool-execution-bounds
Aug 22, 2026
Merged

fix(media): bound the ffmpeg tool's child processes, inputs and scale targets#6989
icecrasher321 merged 3 commits into
stagingfrom
fix/ffmpeg-tool-execution-bounds

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

The copilot ffmpeg tool shells out to FFmpeg in the Sim app process — not in a sandbox (docker/app.Dockerfile installs the binary for exactly this) — and nothing bounded the run.

runCommand had no timer, no .kill() and no signal, so a transcode ran until it finished and survived the request that asked for it: a stopped copilot turn left the encode pinning both cores of a shared instance. scale=30000:30000 reached libavfilter unvalidated, which sizes its per-frame buffers from those numbers (~2.7 GB a frame) inside a child that shares the instance's memory.

Change

Deadline + kill. Every operation shares one 10-minute wall-clock deadline; both the deadline and the caller's cancellation SIGKILL the child. Per-operation rather than per-command, because concat runs a full re-encode per input — a per-command timeout would let N inputs multiply into N timeouts.

Cancellation is wired to context.abortSignal. Every abort reason on that controller is an explicit user stop (UserStop / RedisPoller / MarkerObservedAtBodyClose); the copilot lifecycle tracks a passive client disconnect separately as publisher.clientDisconnected and does not abort on it. So an encode dies when the user presses stop and never otherwise.

Worth flagging separately: userStopSignal, which the existing assertServerToolNotAborted calls read, is declared on ServerToolContext but never populatedcreateServerToolHandler forwards only abortSignal, and ToolExecutionContext has no such field. Those assertions are currently no-ops on this path. Left alone here; abortSignal is the signal that actually arrives.

ffprobe had the same shape in miniature. fluent-ffmpeg's static ffprobe hands back no process handle, so a timeout could only race the callback and leave a wedged prober alive — and concat probes once per input, so the leak scaled with the request. Now runs through execFile, which takes timeout, killSignal and signal natively.

Scale targets are rejected outside 16–4096 with a message the model can act on, plus an area cap. concat's targets come from the source container rather than a caller assertion, so those are clamped instead of rejected.

Two holes not in the original report: readOut buffered the output with no ceiling (CRF-18 re-encodes routinely exceed their input, so the input budget did not bound it), and the handler accepted unlimited input files. Both capped; the byte check now runs against the recorded size before the download rather than after.

Single source of truth

FFMPEG_LIMITS holds the four numbers the Go tool catalog mirrors into its schema, pinned by ffmpeg-schema-parity.test.ts. Without it, changing a limit here would leave the model reading a stale ceiling off its own schema — silent, and user-visible as "the tool said 4096 but refused at 2048". Verified non-vacuous: flipping maxInputFiles to 21 and maxScaleDimension to 2048 fails it on exactly those assertions.

Scope note on the generated bindings

Regenerating the catalog the obvious way pulls in load_slide_layout, an unrelated tool from copilot 7e741a39 (house deck system / slide-layout library) that this branch has not synced. The bindings here were generated from the copilot commit these files already matched, plus only this change — proven by first regenerating from the unmodified baseline contract and confirming a byte-identical match with the committed files. The diff touches exactly Ffmpeg, GenerateAudio, GenerateImage, GenerateVideo; zero slide-layout leakage. That feature syncs whenever someone brings it over.

Verification

tsc --noEmit, lint:check, 2586 tests across lib/copilot + lib/media + lib/uploads + lib/workspace-files, and check:api-validation / boundaries / client-boundary / utils — all pass, rebased onto latest staging (which includes #6986's provenance change to this same file).

Companion

Companion: https://github.com/simstudioai/mothership/pull/447

That PR carries the schema side — the tool-catalog wording and the maxItems/minimum/maximum this branch mirrors. Merge in lockstep.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
Image docs Skipped Skipped Aug 22, 2026 10:45pm

Request Review

@cursor

cursor Bot commented Aug 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches in-process FFmpeg child-process lifecycle, abort/kill behavior, and resource limits on a shared app instance. Miswired cancellation or limit drift could leave hung encodes or reject valid media jobs.

Overview
Hardens the in-process copilot ffmpeg tool so transcodes can no longer pin shared instance CPU/memory after a request ends.

Every operation now shares a 10-minute wall-clock deadline; timeout and explicit user cancel both SIGKILL the child. ffprobe runs via execFile (timeout, kill, abort) instead of fluent-ffmpeg’s unkillable static probe. scale_pad rejects dimensions outside 16–4096 and frames above DCI 4K area; concat clamps probed source frames into the same budget with even yuv420p sizes.

The handler also caps 20 inputs, checks recorded file size before download, aborts between input fetches, and refuses oversized outputs before buffering. Limits live in FFMPEG_LIMITS and are pinned against the generated catalog (sandbox-only I/O fields stripped from ffmpeg/audio/image/video tools).

Reviewed by Cursor Bugbot for commit 0dee35b. Configure here.

@github-actions github-actions Bot added the requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep label Aug 22, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Cross-repo companion check

One or more companion PRs aren't merged into staging yet. Merging this without them will leave copilot and sim out of sync — merge them in lockstep.

  • simstudioai/mothership#447OPEN, not merged (targets staging) — fix(tools): stop telling four media tools they run in a sandbox

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR bounds in-process FFmpeg work with operation deadlines, cancellation, input/output limits, and validated scale targets while synchronizing the generated Copilot schemas.

  • Adds shared FFmpeg limits and schema-parity checks.
  • Replaces callback-based probing with bounded, cancellable execFile execution.
  • Clamps concat frame targets and validates explicit scale dimensions.
  • Adds admission, cancellation, output-size, and boundary tests.

Confidence Score: 4/5

The PR needs a cancellation-plumbing fix before merging because an explicit Stop cannot interrupt an in-flight input download.

The child-process cancellation path is bounded, but input preparation still performs non-cancellable storage work after its single pre-iteration abort check, so a stopped turn can continue a slow or large current download until completion.

Files Needing Attention: apps/sim/lib/copilot/tools/server/media/ffmpeg.ts

Important Files Changed

Filename Overview
apps/sim/lib/copilot/tools/server/media/ffmpeg.ts Adds input-count, recorded-size, and cancellation checks, but cancellation cannot interrupt the current input download.
apps/sim/lib/media/ffmpeg.ts Adds shared operation deadlines, process termination, bounded probing and output reads, and corrected scale-area enforcement.
apps/sim/lib/media/ffmpeg-limits.ts Centralizes input-count and frame-dimension limits used by runtime enforcement and schema parity.
apps/sim/lib/media/ffmpeg.test.ts Covers process bounds, output limits, scale validation, and concat frame clamping, including prior rounding counterexamples.
apps/sim/lib/media/ffmpeg-schema-parity.test.ts Ensures generated tool schemas remain synchronized with runtime FFmpeg limits.
apps/sim/lib/copilot/generated/tool-catalog-v1.ts Updates generated FFmpeg parameter descriptions and structural limits.
apps/sim/lib/copilot/generated/tool-schemas-v1.ts Mirrors FFmpeg input-count and dimension bounds into runtime validation schemas.

Sequence Diagram

sequenceDiagram
  participant User
  participant Handler as FFmpeg server tool
  participant Storage
  participant Media as Media FFmpeg runner
  participant Child as ffprobe/ffmpeg
  User->>Handler: Invoke media operation
  loop Each input
    Handler->>Handler: Check abort signal
    Handler->>Storage: Resolve and download input
  end
  Handler->>Media: Run with shared deadline and signal
  Media->>Child: Start bounded child process
  alt User stops during child execution
    User->>Handler: Abort
    Media->>Child: SIGKILL
  else Deadline expires
    Media->>Child: SIGKILL
  else Operation completes
    Child-->>Media: Output
    Media->>Media: Enforce output-size limit
  end
Loading

Reviews (3): Last reviewed commit: "fix(media): floor the scaled concat axes..." | Re-trigger Greptile

Comment thread apps/sim/lib/media/ffmpeg.ts Outdated
Comment thread apps/sim/lib/copilot/tools/server/media/ffmpeg.ts
Comment thread apps/sim/lib/media/ffmpeg.ts Outdated
icecrasher321 and others added 2 commits August 22, 2026 15:33
… targets

The copilot ffmpeg tool shells out to FFmpeg in the Sim app process — not in a
sandbox — and nothing bounded the run. runCommand had no timer, no kill and no
signal, so a transcode ran until it finished and survived the request that
asked for it: a stopped copilot turn left the encode pinning both cores of a
shared instance.

Every operation now shares one 10-minute wall-clock deadline, and both the
deadline and the caller's cancellation SIGKILL the child. The budget is
per-operation rather than per-command because concat runs a full re-encode per
input, so a per-command timeout would let N inputs multiply into N timeouts.
Cancellation is wired to context.abortSignal, whose every abort reason is an
explicit user stop — the copilot lifecycle tracks a passive client disconnect
separately and does not abort on it — so an encode dies when the user asks and
not before. (userStopSignal, which assertServerToolNotAborted reads, has no
producer on this path.)

ffprobe had the same shape in miniature: fluent-ffmpeg's static ffprobe hands
back no process handle, so a timeout could only race the callback and leave a
wedged prober alive, and concat probes once per input. It now runs through
execFile, which takes timeout, killSignal and signal natively.

Scale targets reached the filter graph unvalidated, where libavfilter sizes its
per-frame buffers from them — scale=30000:30000 is ~2.7 GB a frame, allocated
in a child that shares the instance's memory. Dimensions are now rejected
outside 16-4096 with a message the model can act on, plus an area cap. concat's
targets come from the source container rather than a caller assertion, so those
are clamped instead.

Two further holes: readOut buffered the output with no ceiling, and CRF-18
re-encodes routinely exceed their input, so the input budget did not bound it;
and the handler accepted unlimited input files. Both are capped, and the byte
check now runs against the recorded size before the download rather than after.

FFMPEG_LIMITS is the single source for the four numbers the Go tool catalog
mirrors into its schema, pinned by ffmpeg-schema-parity.test.ts so the model is
never told a ceiling the executor does not enforce.

Companion: simstudioai/copilot#PENDING

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rep on cancel

Two review findings, both real.

concat clamped each probed axis independently, so a 4096x4096 source produced a
normalization target of exactly that — nearly double the area budget scale_pad
enforces through the same scale=/pad= graph. The pair is now scaled down
together, so aspect ratio survives and one ceiling governs both entry points.
Dimensions come back even too, which yuv420p requires and per-axis rounding did
not guarantee.

Preparing the inputs is itself the expensive half of a many-file call — up to
the whole byte budget in storage reads — and the abort signal was only consulted
after the loop, so an explicit stop was observed only once every download had
already finished. The loop now checks it per input.

Also corrects the rationale comments on the mirrored limits. maxItems/minimum/
maximum do not reach the model: copilot's NormalizeToolParameters allowlists
type/properties/items/description/enum/required, so the bounds travel only the
contract path, where Sim's router validates against them. That is still worth
having — it turns an out-of-range argument into a structured rejection before
any storage read or child process — but the model learns the limits from the
parameter descriptions, and the comments now say so instead of claiming the
schema teaches it.
@icecrasher321
icecrasher321 force-pushed the fix/ffmpeg-tool-execution-bounds branch from 2361d23 to 627509f Compare August 22, 2026 22:34
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/media/ffmpeg.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 627509f. Configure here.

…olds

The scale factor lands both axes on a product of exactly the budget, so an axis
allowed to round up can put the pair back over it — and when both round up and
both land even, nothing pulls them back. A brute force over every dimension pair
in 16..4096 finds 219,280 that violate the bound under round-to-even, worst
2694x3520 -> 2688x3512, 3072 pixels over. Under floor-to-even: none.

Flooring keeps each axis at or below its exact target, so the product cannot
exceed the budget. Probed dimensions are already integers, so this changes only
the scaled path.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0dee35b. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/copilot/tools/server/media/ffmpeg.ts
@icecrasher321
icecrasher321 merged commit 91e9274 into staging Aug 22, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant