Skip to content

Develop - #144

Merged
dkackman merged 26 commits into
masterfrom
develop
Sep 14, 2026
Merged

dkackman merged 26 commits into
masterfrom
develop

Conversation

@dkackman

Copy link
Copy Markdown
Owner

No description provided.

dkackman and others added 26 commits September 13, 2026 13:58
…ocation

A workflow JSON is untrusted input under the default posture, and every
loader used to trust the location it named: fetch_image took any absolute
path, gather_images handed its glob straight to the filesystem, any http(s)
URL was fetched whatever host it named, and remote_text_encoder POSTed this
machine's HuggingFace token to an address the JSON picked. dw/locations.py
is the one place that answers where a caller-supplied location may point:

- a path must land inside a root this installation works in (the workflow's
  directory, the asset libraries, the output root) - the remedy for anything
  else is an 'asset:' reference (#114)
- an http(s) URL must not resolve to an address inside the deployment
  (loopback, link-local, private), checked after DNS rather than on the
  literal string (#115)
- a gather_images/gather_videos glob is contained, and each match re-checked
  on its real path so a symlink cannot carry the expansion out (#116)
- remote_text_encoder is https-only, and the HF token is attached only for
  huggingface.co / .cloud / hf.space (#112)
- model_name must be a Hub repo id, or a path inside a root (#117), the same
  check download_model already applied to repo_id

Containment is tested before existence, so the refusal is not a
file-existence oracle for the whole filesystem (#114). All of it yields to
--trust-workflows, exactly as the import and remote-code gates do.

Enforced twice: location_errors() at validation time, so validate_workflow
refuses before a pipeline load is spent; and in the loaders, for a location
that arrives through a variable or a previous result.

Also in this batch:

- #113: over a dw.serve --mcp endpoint download_output writes on the server,
  so its destination is confined to that workspace - realpath containment,
  not a substring test for '..', and a relative destination is joined onto
  the workspace rather than onto the server's cwd. A stdio dw-mcp is
  unchanged, because there 'local disk' is genuinely the caller's own.
- #118: additionalProperties:false on step, task, workflow_reference and
  pipeline_reference (each after a zero-stray-key sweep of the catalog),
  with the jsonschema message rewritten to name the step, the key and what
  the object actually takes. pipeline/pipeline_component stay open - a
  component's name is one of their keys.
- #120: get_server_info reports trust_workflows, so a security check can
  confirm the posture it is testing rather than inferring it from behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ented conventions

#108: concat_videos refused to join videos whose soundtracks were at
different sample rates, mid-run, after the earlier steps had written their
files, with an error that named neither which shot to fix nor the
resample_audio task that was the remedy. Unlike a level jump the difference
carries no editorial meaning, so it is converted: every track is resampled to
the highest rate among them (or to an explicit `sample_rate`), with a warning
naming each video and its rate. resample_waveform is the conversion
resample_audio already did, lifted out so there is one implementation.

#110: `denoise_total_steps` comes back one less than `num_inference_steps` on
every H3 run because MiniMaxH3Scheduler counts sigma grid points with the
terminal zero among them and evaluates the model N-1 times - the vendor's
convention, not a dropped step or an off-by-one in our reporting, which
faithfully reports len(scheduler.timesteps). Documented in wait_for_job, in
diagnose, and in the H3 skill, and pinned to the scheduler in
tests/test_plugin_skills.py because from outside the two are
indistinguishable.

#109: a shot entry's subject reference takes `from_file` with an `asset:`
path, so an episode can be cast from portraits that already exist - it worked
and was documented nowhere. Said now in the template's description and in the
H3 skill, including what it does not yet do: the two Z-Image steps still run
and their portraits are discarded. Eliding a step nothing references is an
engine change, proposed in docs/proposals/unreferenced-step-elision.md rather
than taken here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t's confinement

SECURITY.md gets a "Where a workflow may read and reach" section under the
trust model - the second thing a workflow file chooses besides which code to
import. WORKFLOW_GUIDE.md's authoring section (which the MCP `workflows`
guide serves verbatim) gets the agent-facing version plus "A step takes only
the keys the engine reads", so an agent learns there is no `when`/`retry`
before it invents one. SECURITY_QUICKREF.md points new filesystem access at
dw/locations.py rather than at validate_path directly. MCP.md says
download_output is confined to the workspace over a mounted endpoint and why
the transport is what decides it.

Refs #112-#118, #120.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ree objects that stop ignoring keys

#108: the resample concat_videos performs is emitted with emit_warning
rather than logger.warning, kind 'sample_rate_mismatch', carrying the rate
per video and the chosen target - a conversion made on the caller's behalf
was landing with nothing in the job saying so.

#117: a URL model_name joined onto the workflow directory resolved inside a
root and validated clean; it is neither a repo id nor a model directory and
is refused as such.

#123: the workflow object and result are closed the way #118 closed step,
and pipeline is closed to everything except a key whose value is a component
definition - which is the rule declared_component_names already applies.
'argument_template' is declared, being engine-injected onto a sub-workflow.

#124: a relative '..' media argument is refused at validation, where the
absolute form and gather_images' glob already were, rather than three
seconds into a queued job.
#126: slice_audio zero-pads a slice that reaches past the end of its source
and said nothing. In templates/assemble-and-score that is the default path -
total_frames is the length of the cut, the score is a separate asset with its
own length - so a short score left the film unscored for the rest of its
length with warnings: []. It now emits a `slice_past_end` warning carrying the
source, requested and padded lengths, and naming loop_audio as the remedy;
padding under 10 ms is the rounding frame-aligned slicing produces and stays
quiet. The padding itself is unchanged - a few frames of tail pad is a
legitimate thing to want. The task's docs, TASKS.md and the template's
description now say what happens past the end.

#127: get_gallery_metadata resolved its name against the outputs root only, so
an input asset's duration, frame count, fps, sample rate and channels were
unreadable - the numbers a caller has to supply as total_frames, fps and
sample_rate were obtainable for a file it had generated and not for one it was
about to consume. `name` may now be an `asset:` reference, resolved down the
same search path a run resolves one in; `job` is null and a new `source` field
says which root answered. The MCP hint for an asset says these are pre-run
numbers, and list_assets points at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A list-driven step showed as one box on the job page's flow view while
its members ran one by one in the Progress list. The group box now grows
and renders one inset chip per entry, beneath the header the ordinary
box's three lines occupy: chips named as the workflow wrote them (an
entry's name, else its index - _entry_keys' rule) with the engine's
group@entry name on hover, a chip greening as its step_end arrives and
the running one pulsing amber. The group box keeps its existing
all-members-done rule, and the arrows and fan-in labels still attach to
it as a whole; column layout is height-aware so a tall box no longer
overlaps the node beneath it.

The member list is read from the definition - a literal for_each list,
or a variable: reference resolved against variables, which the realized
workflow carries with the run's actual list folded in (dw/realize.py) -
so historical jobs show their members too, with no server change. A
list that cannot be read statically shows a muted for_each stand-in.
Between one entry finishing and the next starting, activeMember now
reports nothing: the run is on neither, and a chip still amber there
would lie about progress.

Co-Authored-By: Claude <noreply@anthropic.com>
feat(ui): for_each entries drawn inset in the flow view
The job page already fetched the workflow a job ran for the flow graph -
that response is the realized copy when the run wrote one, and carried a
`realized` flag the UI dropped. The Workflow section now names which copy
it is (realized / as submitted) and offers a show/hide JSON toggle that
renders the definition in a readonly editor, mounted only while open.
Tests stub JsonEditor so the suite never boots Monaco in jsdom.

Co-Authored-By: Claude <noreply@anthropic.com>
Monaco's default is no wrap, so a long prompt string in a JSON view
forced a horizontal scroll. wordWrap: 'on' wraps at the container width
for every view that uses the editor - the job page's realized JSON, the
workflow page's definition, and the editable split views alike.

Co-Authored-By: Claude <noreply@anthropic.com>
Job page: view the realized workflow JSON
…p, validation, two security orderings

- #128/#129: templates/dissolve-between-shots declares match_levels /
  match_levels_dbfs and passes them to dissolve_videos, and its description
  carries #126's score-length paragraph plus the dissolve arithmetic
  (n*f - (n-1)*d) that assemble-and-score's total_frames does not need.
- #133: a safety-checker blanking now reaches the run as a warning event
  (emit_warning), not just the server log - a consumer of a 'succeeded' job
  could not tell a black frame from a render. templates/text-to-image loads
  with 'safety_checker': null, since the reference template must not have a
  silent content filter in the path.
- #134: delete_output sweeps the run directory once its last media file is
  gone, sidecars included, and accepts a '<workflow>/<run id>' name to clear
  a run that failed before writing any media.
- #136: validation refuses a reference set the pipeline would refuse
  (dw/reference_limits.py) - per-kind and total counts, and H3's audio-may-
  not-stand-alone rule - reading every limit off the diffusers block that
  enforces it rather than restating it.
- #137: Pipeline.check_trusted() runs the trust gates over the definition
  before the 'loading' phase event is emitted, so job events distinguish
  'refused before load' from 'loaded, then refused' again.
- #138: upload_asset confines file_path to the server's own directories over
  a mounted dw.serve endpoint, ahead of the existence and extension checks so
  it cannot be used as a path-existence oracle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A task command's argument schema is its implementation's signature, which
carries no range, so validate_workflow had nothing to check a number
against. Both defects that surfaced were silent successes, not failures:
slice_audio(num_frames=-10) reached Python's slice semantics and returned
the track minus its last ten frames (#139), and
resample_audio(target_sample_rate=0) left the samples untouched and then
hit the 44100 Hz save default, writing a header 38% off over a 32 kHz
waveform (#140).

dw/task_domains.py declares the domains that are not a judgement call -
counts and rates above zero, offsets zero or above - for the audio and
video-join commands, and checks them in two places: statically in
validation_errors, so a literal is a free pre-flight error at its JSON
path, and inside the commands, which is the only layer that sees a value
arriving from a variable: or an earlier step. _as_track now refuses a
non-positive rate outright, since relabelling a waveform changes its speed
and pitch and the save default makes a missing rate look valid, and
resample_waveform refuses one too. get_task reports the domain beside the
parameter it constrains, so an agent composing a call can read it.

tests/test_task_domains.py pins every registry entry to a real parameter
of a real command, so a rename cannot leave a domain checking nothing.
@dkackman
dkackman merged commit ba67d9d into master Sep 14, 2026
5 checks passed
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.

1 participant