Current behavior
The CLI validates ingest URIs inline at src/hflow/cli.py:1113-1135. The workspace server implements the same rule again at packages/hflow-server/src/hflow_server/_runtime.py:412-428.
The two entry points already differ:
That leaves the validity and normalization of one runtime URI dependent on which public entry point received it.
Verified against main at fbfd553.
Pattern to copy
parse_step_version() parses a public string once and returns a refined StepVersion. _validated_relative_key() owns the analogous containment rule for storage keys.
What to build
Add one core parser for a data-root-relative ingest URI and return a refined type such as DataRootRelativeUri. Use that parser from the CLI, workspace server, and AirflowClient.ingest() so all three accept, normalize, and refuse the same values.
Keep transport-specific presentation at the edges: the CLI still prints an ingest error and exits 2, the server still returns a 400, and the SDK raises ValueError before making an HTTP request.
Definition of done
- One function owns trimming, empty-value refusal, absolute-path refusal, and parent-escape refusal.
- A normal URI such as
episodes-in/run_0001.mcap reaches the trigger conf unchanged.
- Surrounding whitespace has one documented result at every entry point. Preserve the server's current behavior by trimming it.
- Blank strings, absolute paths, and paths that normalize outside the data root are refused by the CLI, server, and SDK before runtime resolution or HTTP.
resolve_episode_reference() consumes the refined value and no longer repairs a leading slash.
- Existing CLI and server error status behavior stays unchanged.
- Extend the closest CLI, server, and client boundary tests. Assert accepted or refused outcomes, not private helper calls.
Non-goals
- Changing the trigger conf key or API response schema
- Supporting absolute host paths in scheduled runs
- Normalizing safe internal segments such as
a/../b into a different persisted identity
- Reusing every storage-key rule, including reserved mirror suffixes
Validation
uv run ruff check --fix
uv run ruff format
uv run ty check
uv run pytest -q tests/test_runtime_cli.py tests/test_ingest_in_process.py \
tests/test_runtime_client.py packages/hflow-server/tests/test_server_runtime.py
uv run pytest -q
Current behavior
The CLI validates ingest URIs inline at
src/hflow/cli.py:1113-1135. The workspace server implements the same rule again atpackages/hflow-server/src/hflow_server/_runtime.py:412-428.The two entry points already differ:
AirflowClient.ingest()atsrc/hflow/cli.py:1162-1168.list[str]and puts it into trigger conf unchanged atsrc/hflow/runtime/_client.py:312-352.src/hflow/stage_execution.py:124-128.That leaves the validity and normalization of one runtime URI dependent on which public entry point received it.
Verified against
mainatfbfd553.Pattern to copy
parse_step_version()parses a public string once and returns a refinedStepVersion._validated_relative_key()owns the analogous containment rule for storage keys.What to build
Add one core parser for a data-root-relative ingest URI and return a refined type such as
DataRootRelativeUri. Use that parser from the CLI, workspace server, andAirflowClient.ingest()so all three accept, normalize, and refuse the same values.Keep transport-specific presentation at the edges: the CLI still prints an ingest error and exits 2, the server still returns a 400, and the SDK raises
ValueErrorbefore making an HTTP request.Definition of done
episodes-in/run_0001.mcapreaches the trigger conf unchanged.resolve_episode_reference()consumes the refined value and no longer repairs a leading slash.Non-goals
a/../binto a different persisted identityValidation