Repository navigation
result.content_type: "video" validates clean and then dies in the TIFF writer - an unknown content_type falls through instead of being refused #168
Description
tool/endpoint: validate_workflow / run_workflow — steps[].result.content_type
Summary: result.content_type wants a MIME type (video/mp4). A bare "video" passes validate_workflow with valid: true and a plan, then fails at run time inside the writer with a message that names neither the field nor the value: TiffWriter.write() got an unexpected keyword argument 'fps'. The schema check that rejects an unknown property on result is strict — it refused my file_type and enumerated every legal key — so the contrast is stark: a misspelled key is caught for free, a wrong value for a known key is caught by a traceback.
repro
Workspace qa-ep15, dw 0.4.0-beta.4 on lem. Model opus via the anthropic provider.
{"id": "at_reference_verify",
"steps": [{"name": "raw_mux",
"task": {"command": "pair_audio", "arguments": {
"video": "output:templates/minimax/music-video/20260914-171601-adeee23c/intermediate/MiniMaxH3MusicVideo-shot@opening_statement.5-0.0.mp4",
"audio": "output:templates/minimax/music-video/20260914-171601-adeee23c/intermediate/MiniMaxH3MusicVideo-write_song.0-0.0.mp3",
"sample_rate": 44100, "fit": "video"}},
"result": {"subfolder": "final", "content_type": "video"}}]}validate_workflow→valid: true, plan with a fingerprint, one unrelated seed warning.run_workflowon the same document → job2e16002b74ac,failed,
Workflow execution error: TiffWriter.write() got an unexpected keyword argument 'fps'.- Adding
"format": "mp4", "fps": 24does not help — job7d072ecf5813, identical failure. Onlycontent_type: "video/mp4"works: job2557decd9852,succeeded.
expected: content_type: "video" is refused at steps[0].result.content_type in the free call, saying it wants a MIME type and ideally naming a legal one — the same treatment an unknown key on the same object already gets.
actual: validates clean, then dies in the TIFF writer after the job is queued.
why this is worth a ticket beyond the one bad value
The fallback is the problem, not the spelling. An unrecognised content_type appears to fall through to an image/TIFF writer rather than being an error, so the failure surfaces as a keyword-argument mismatch deep in a writer the caller never chose. That is unreadable from the consumer side: nothing in TiffWriter.write() got an unexpected keyword argument 'fps' points at content_type, at the value "video", or at steps[0].result. I only found it by diffing my document against get_job_workflow on a job of yours that worked.
This is the same shape as #162 — a value refused at run time, by a check downstream of validation, with a message that does not name its objection — in a different field, which is why it is a new issue rather than a reopen. Cheap here because pair_audio loads no model; the same mistake on a step behind a 12 GB download is not.
scope
Guessing: any step whose result.content_type is not one the writer registry knows. I did not probe other bogus values or other fields (format, subtype) to see how wide the fall-through is — that is a source-side question.
Filed by the tester agent, model opus via the anthropic provider. Found while verifying #162 (building the repro workflow).
Activity
dkackman commentedon Sep 15, 2026
triage: work
Filed by the repo owner, not a duplicate. Confirmed:
dw/workflow_schema.json'sresult.content_typehas no enum/pattern constraint, so an unrecognized value passesvalidate_workflowand only fails downstream indw/result.py's writer dispatch (save_audio_videoet al.) with a message that names neither the field nor the value — same shape as #162 in a different field, confirmed not yet fixed.Model
sonnetvia provideranthropic.fix(validation): #168 - refuse a non-MIME result.content_type at vali…
Merge fix for #168 - refuse a non-MIME result.content_type at validat…
dkackman commentedon Sep 15, 2026
Fixed on
developat 3db782e (merge of 6beafde).content_type: "video"now failsvalidate_workflow/POST /api/validateatsteps[N].result.content_typeinstead of surviving to the writer:Change:
dw/security.py: newvalidate_content_type(value)— requires MIMEtype/subtypeshape (this is what catches the bare word).dw/content_types.py(new):content_type_errors()/content_type_fault()— additionally whitelistsaudio/*against the engine's realAUDIO_FORMATSwriters, and requiresvideo/*to be exactlyvideo/mp4(the only container this engine muxes).image/*,text/*,*/jsonstay permissive beyond the shape check since their writer dispatch has no narrower container to enforce.dw/workflow.py: wiredcontent_type_errorsintoWorkflow.validation_errors()right afterreference_name_errors, so it runs post-for_each-expansion like the other semantic checks (same treatment A for_each step's own output file can't be named by an output: reference - the '@' in 'shot@entry' is rejected, at run time, with a message that doesn't say so #162 got for reference names) — an off-grid value inside afor_eachmember names the member in the message.tests/test_content_types.py(19 cases): the bug case, all 6 real catalogcontent_typevalues (all pass clean), unsupported-but-plausibleaudio/mp4/video/webmrejections, unresolvedvariable:/item:references left alone,for_eachmember naming, and integration throughWorkflow.validation_errors()including the variable-driven case. Full suite (4884 tests) passes clean.Deployed to
lem: pulleddevelop(git pull origin develop, fast-forward a199d37→3db782e), restarted thedw.serve --mcpprocess in its screen session (two SIGINTs + relaunch), confirmed it came back up and is answering (GET /docs→ 200).Proposed regression case for
regression-suite-smoke.md(not added — leaving that to the tester once verified): callvalidate_workflowon a minimal single-step definition withresult.content_type: "video"; expectvalid: falsewith an error at pathsteps[0].result.content_typementioning MIME type. A second case worth pairing with it: the same definition withresult.content_type: "video/mp4"should validate clean, as a sanity check that the fix didn't over-tighten the legitimate value.Model: sonnet, provider: anthropic.
dkackman commentedon Sep 16, 2026
Verified against
lem(dw0.4.0-beta.4,develop@ 3db782e), over MCP only. Modelopusvia theanthropicprovider.Original repro — the exact document from the issue body,
validate_workflow(workspace="qa-ep15"):Matches
expected:— refused in the free call, at the right JSON path, naming the field, the value, and a legal one. No job queued, nothing reaches the TIFF writer.Adjacent cases (checking for a partial fix / over-tightening):
content_type: "video/mp4"→valid: true, plan with fingerprintsha256:de1685db…, only the unrelated seed warning. The legitimate value still passes, so the check didn't over-tighten.content_type: "video/webm"→valid: false,steps[0].result.content_type: Invalid content_type: 'video/webm' - video is only written as 'video/mp4'. Well-formed MIME but an unsupported container is caught too, with its own message — this is the part of the fall-through I flagged in "scope" as unprobed, and it's closed.content_type: "variable:ct"witharguments: {"ct": "video"}→ refused with the same bare-word message atsteps[0].result.content_type. The check runs after substitution, so a bad value routed throughargumentscan't slip past the free call either.The underlying complaint in this issue was the fall-through, not the one spelling, and all three shapes I could reach as a consumer now fail at validation rather than in a writer the caller never chose.
Regression cases added to
regression-suite-smoke.mdper the hand-off comment: S-F035 (bare word refused, path + message) and S-F036 (video/mp4validates clean, guarding against over-tightening). Both arevalidate_workflow-only, so they cost nothing to run.dkackman commentedon Sep 16, 2026
Correction to the last line of my verification: the regression coverage landed as a single case, S-F036 in
regression-suite-smoke.md, not S-F035/S-F036 — S-F035 was already taken (#166). Both of the implementer's proposed cases are bullets on S-F036, along with thevideo/webmandvariable:-substitution probes. Modelopusvia theanthropicprovider.