The remote-GPU line, verified on hardware rather than asserted. **Dubbing renders on the worker.** dub_generate.py dispatches the coarse `dub_segments` operation through the gateway, following the audiobook pattern: per-unit local fallback after consecutive remote failures, one aggregated notice rather than one per segment. A 40-minute dub that loses its worker at segment 200 degrades instead of producing 200 error rows. **An out-of-date worker is now refused by name.** This was the worst defect in the plan and it was silent: an un-upgraded worker registered cleanly, then ignored `inputs` and rendered a clone with NO reference audio — returned as success. A plausible wrong result with nothing anywhere to surface it. Workers now declare features, and one missing them is turned away with the features named and `no task was run`. Verified live: a worker one commit behind was correctly refused. **"Offline" and "cannot run this" are different facts.** Asking a live worker for an engine it lacks answered "is offline or cannot be reached. Wake the selected worker" — while that worker reported ready, one free slot and 3.6 ms latency. The user was sent to wake a machine that was already awake. The scheduler now distinguishes absent from present-but- incapable, and names the engine rather than the operation, because the engine is the thing a user can install. **An engine with no catalog entry is no longer hidden.** A `repo_ids` non-emptiness check had been implemented as a runtime filter, so a worker silently refused to advertise any engine lacking a models.yaml entry — which is four registered engines, including CosyVoice. Users with those already installed would have lost remote support with only a log line. Empty `repo_ids` now means "not downloadable here", never "not runnable". **And a script so this stops being done by hand.** scripts/verify-remote-worker.sh runs the per-phase acceptance checks against a live worker, non-destructively. Its preconditions are the mistakes that cost the most time: exactly one listener on the control port (two instances silently shared it), and never detecting the worker with a pgrep pattern that matches the ssh shell running it. Its first real run found the dubbing picker claiming remote placement. That turned out to be the CHECK being stale, not the picker — the port had landed since it was written. It now asserts self-consistency instead: the picker may claim remote only for an operation the control plane actually advertises as remotely producible, which cannot rot the next time an op is ported. Backend 5291 passed, frontend 1812 passed. Acceptance script: no automated failures across Phases 4-8 on an RTX 4090. Four checks remain MANUAL by design — true airplane mode, concurrent downloads, killing a worker mid-audiobook, and the model-list UI — and are reported as unverified rather than passed.
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
"""Filesystem trust-boundary helpers.
|
|
|
|
Paths persisted in SQLite are still untrusted: older clients and imported job
|
|
records can contain absolute paths, traversal components, or symlink escapes.
|
|
Keep containment checks at the filesystem boundary instead of relying on the
|
|
route or database layer to have sanitised a value earlier.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ntpath
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_WINDOWS_RESERVED_NAMES = frozenset({"CON", "PRN", "AUX", "NUL"}) | frozenset(
|
|
f"{prefix}{number}" for prefix in ("COM", "LPT") for number in range(1, 10)
|
|
)
|
|
|
|
|
|
class UnsafePath(ValueError):
|
|
"""Raised when a path crosses its allowed filesystem boundary."""
|
|
|
|
|
|
def safe_filename(value: object) -> str:
|
|
"""Return a portable bare filename, rejecting traversal and drive paths."""
|
|
name = str(value or "")
|
|
if (
|
|
not name
|
|
or name in {".", ".."}
|
|
or "/" in name
|
|
or "\\" in name
|
|
or os.path.isabs(name)
|
|
or ntpath.isabs(name)
|
|
or ntpath.basename(name) != name
|
|
or name.endswith((" ", "."))
|
|
or re.search(r"[\x00-\x1f]", name)
|
|
or name.split(".", 1)[0].upper() in _WINDOWS_RESERVED_NAMES
|
|
or len(name.encode("utf-8")) > 240
|
|
):
|
|
raise UnsafePath("expected a bare filename")
|
|
return name
|
|
|
|
|
|
def resolve_within(root: os.PathLike[str] | str, value: os.PathLike[str] | str) -> Path:
|
|
"""Resolve *value* beneath *root*, rejecting traversal and symlink escapes.
|
|
|
|
Absolute values are accepted only when they already resolve inside the
|
|
root. This preserves existing database rows, which historically stored a
|
|
mixture of relative filenames and absolute job-artifact paths.
|
|
"""
|
|
raw = os.fspath(value) if value is not None else ""
|
|
if not isinstance(raw, str) or not raw:
|
|
raise UnsafePath("path is empty")
|
|
# Treat both separator families as structural on every host. Otherwise a
|
|
# Windows traversal string is an innocent-looking filename when validated
|
|
# on Linux (and can become dangerous after persisted data is moved).
|
|
if os.sep != "\\" and ("\\" in raw or bool(ntpath.splitdrive(raw)[0])):
|
|
raise UnsafePath("path uses a foreign separator or drive")
|
|
root_path = Path(root).expanduser().resolve(strict=False)
|
|
root_text = str(root_path)
|
|
if os.path.isabs(raw):
|
|
prefix = root_text.rstrip(os.sep) + os.sep
|
|
if not os.path.normcase(raw).startswith(os.path.normcase(prefix)):
|
|
raise UnsafePath("path escapes its allowed root")
|
|
raw = raw[len(prefix):]
|
|
|
|
# Rebuild from individually sanitized basenames. Besides making the
|
|
# containment proof explicit to static analysis, this rejects empty,
|
|
# dot, parent, drive, and separator-bearing components before Path sees
|
|
# any persisted/request-derived string.
|
|
parts = raw.split(os.sep)
|
|
clean_parts: list[str] = []
|
|
for part in parts:
|
|
clean = os.path.basename(part)
|
|
if not clean or clean in {".", ".."} or clean != part:
|
|
raise UnsafePath("path contains an unsafe component")
|
|
clean_parts.append(clean)
|
|
candidate = root_path.joinpath(*clean_parts)
|
|
resolved = candidate.resolve(strict=False)
|
|
try:
|
|
if os.path.commonpath((str(root_path), str(resolved))) != str(root_path):
|
|
raise UnsafePath("path escapes its allowed root")
|
|
except ValueError as exc: # Windows paths on different drives
|
|
raise UnsafePath("path escapes its allowed root") from exc
|
|
if resolved == root_path:
|
|
raise UnsafePath("path must name an item below its allowed root")
|
|
return resolved
|