Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.