"""One-shot, caller-authenticated single-Space CLI. No inference on import. Examples: python client.py presets python client.py health python client.py plan --input request.json python client.py run --input request.json --run-dir runs/unique-name \ --expected-revision Run requires HF_INFERENCE_TOKEN (or --token-env NAME) and the operator's HF_POOL_ZEROGPU_OVERAGE_DISABLED=1 attestation. This client does not change or verify the account's billing settings. No SDK credential lookup is allowed. A claimed directory is never reused, including after errors. An unknown hop may still be running remotely. Reconcile its event_id/session_hash manually; there is deliberately no resume, retry, cancellation, or token rotation. """ from __future__ import annotations import argparse from concurrent.futures import Future from contextlib import contextmanager, redirect_stderr, redirect_stdout, suppress from dataclasses import dataclass import hashlib import json import math import os from pathlib import Path import re import signal import stat import sys import tempfile import threading import time from urllib.request import HTTPRedirectHandler, Request, build_opener GENERATOR = "https://owenisas-workflow-studio.hf.space" GENERATOR_REPO = "owenisas/workflow-studio" class AuthenticationRequired(ValueError): pass class OverageAttestationRequired(ValueError): pass class OwnerTokenForbidden(ValueError): pass class RevisionMismatch(ValueError): pass class RuntimeNotReady(ValueError): pass class IntegrityError(ValueError): pass class InputError(ValueError): pass class CLIArgumentError(ValueError): pass class TerminationRequested(KeyboardInterrupt): pass @contextmanager def _termination_guard(): if threading.current_thread() is not threading.main_thread(): yield return previous = signal.getsignal(signal.SIGTERM) def terminate(signum, frame): raise TerminationRequested() signal.signal(signal.SIGTERM, terminate) try: yield finally: signal.signal(signal.SIGTERM, previous) def _unique_object(pairs): result = {} for key, value in pairs: if key in result: raise InputError() result[key] = value return result def _bad_constant(value): raise InputError() def _local_path(value, base): if not isinstance(value, str) or not value or "://" in value or "\x00" in value: raise InputError() path = Path(value) if not path.is_absolute(): path = base / path if path.is_symlink() or not path.is_file(): raise InputError() return path.resolve(strict=True) @dataclass(frozen=True) class RequestInput: preset_id: str prompt: str canvas: str = "" duration: float = 5 seed: int = 42 image: Path | None = None last_image: Path | None = None def plan_args(self): return (self.preset_id, self.prompt, self.canvas, self.duration, self.seed, self.image is not None, self.last_image is not None) def load_input(filename): path = _local_path(str(filename), Path.cwd()) with _open_regular(path) as handle: raw = handle.read(1024 * 1024 + 1) if len(raw) > 1024 * 1024: raise InputError() data = json.loads(raw, object_pairs_hook=_unique_object, parse_constant=_bad_constant) allowed = {"preset_id", "prompt", "canvas", "duration", "seed", "image", "last_image"} if not isinstance(data, dict) or set(data) - allowed: raise InputError() for field in ("preset_id", "prompt"): if not isinstance(data.get(field), str) or not data[field].strip(): raise InputError() if (len(data["prompt"]) > 12000 or data["prompt"] != data["prompt"].strip() or not isinstance(data.get("canvas", ""), str)): raise InputError() duration, seed = data.get("duration", 5), data.get("seed", 42) if type(duration) not in (int, float) or not 2 <= duration <= 8 or not math.isfinite(duration): raise InputError() if type(seed) is not int or not 0 <= seed <= 2147483647: raise InputError() for field in ("image", "last_image"): if data.get(field) is not None: data[field] = _local_path(data[field], path.parent) return RequestInput(**data) def _open_regular(path): # Nonblocking prevents a swapped FIFO from hanging before fstat(). fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) try: if not stat.S_ISREG(os.fstat(fd).st_mode): raise InputError() return os.fdopen(fd, "rb") except BaseException: os.close(fd) raise def _fsync_dir(path): fd = os.open(path, os.O_RDONLY) try: os.fsync(fd) finally: os.close(fd) def _sanitize(value, token=""): if isinstance(value, str): if token: value = value.replace(token, "[REDACTED]") return re.sub(r"hf_[A-Za-z0-9_]+", "[REDACTED]", value) if isinstance(value, dict): return {_sanitize(str(k), token): _sanitize(v, token) for k, v in value.items() if not re.search(r"token|authorization|password|secret|signature", str(k), re.I)} if isinstance(value, (list, tuple)): return [_sanitize(v, token) for v in value] if value is None or type(value) in (bool, int): return value if type(value) is float and math.isfinite(value): return value raise ValueError() def _atomic_json(path, data): # Both initial publication and replacements have private modes and fsync. fd, temporary = tempfile.mkstemp(prefix=".journal-", dir=path.parent) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: os.fchmod(handle.fileno(), 0o600) json.dump(data, handle, ensure_ascii=True, allow_nan=False, sort_keys=True) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) _fsync_dir(path.parent) finally: with suppress(FileNotFoundError): os.unlink(temporary) class Journal: def __init__(self, directory, token, revision): # Resolve the parent only: a final symlink must fail the exclusive mkdir. directory = Path(directory) self.directory = directory.parent.resolve(strict=True) / directory.name self.directory.mkdir(mode=0o700, exist_ok=False) self.directory.chmod(0o700) _fsync_dir(self.directory.parent) self.token = token self.data = { "schema_version": 2, "state": "preflight", "checkpoint": 0, "generator": GENERATOR, "generator_revision": revision, "overage_disabled_operator_attestation": True, "hops": {"workflow": {"state": "not_submitted", "event_id": None, "session_hash": None}}, } self.save() def save(self): self.data["checkpoint"] += 1 self.data["updated_at_unix"] = time.time() _atomic_json(self.directory / "journal.json", _sanitize(self.data, self.token)) def _copy_private(source, destination, expected=None): """Snapshot regular bytes, detect concurrent source writes, then publish.""" fd, temporary = tempfile.mkstemp(prefix=".copy-", dir=destination.parent) hasher = hashlib.sha256() size = 0 try: with os.fdopen(fd, "wb") as target, _open_regular(source) as original: before = os.fstat(original.fileno()) while chunk := original.read(1024 * 1024): hasher.update(chunk) size += len(chunk) target.write(chunk) after = os.fstat(original.fileno()) stamp = lambda s: (s.st_dev, s.st_ino, s.st_size, s.st_mtime_ns, s.st_ctime_ns) if stamp(before) != stamp(after) or size != before.st_size: raise IntegrityError() digest = hasher.hexdigest() if expected is not None and digest != expected: raise IntegrityError() os.fchmod(target.fileno(), 0o600) target.flush() os.fsync(target.fileno()) os.replace(temporary, destination) _fsync_dir(destination.parent) return {"path": destination.name, "sha256": digest, "bytes": size} finally: with suppress(FileNotFoundError): os.unlink(temporary) def _verify_file(path, expected): with _open_regular(path) as handle: actual = hashlib.file_digest(handle, "sha256").hexdigest() if actual != expected: raise IntegrityError() class _NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): # Never forward caller authorization to an unexpected destination. return None def _hub_json(route, token): request = Request("https://huggingface.co/api/" + route, headers={"Authorization": "Bearer " + token, "Accept": "application/json"}) with build_opener(_NoRedirect).open(request, timeout=20) as response: return json.load(response) def fetch_identity(token): return _hub_json("whoami-v2", token) def fetch_metadata(repo, token): if repo != GENERATOR_REPO: raise ValueError() return _hub_json("spaces/" + repo, token) def _client_factory(src, **options): from gradio_client import Client # 2.5.0 uses an OR with this env even when analytics_enabled=False. os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" return Client(src, **options) def _file_wrapper(path): from gradio_client import handle_file return handle_file(str(path)) def _new_client(factory, src, token, downloads: bool | Path = False): # token=False is intentional: None permits saved SDK owner credentials. return factory(src, token=token, max_workers=8, verbose=False, download_files=downloads, analytics_enabled=False, httpx_kwargs={"timeout": 20}) def _within_deadline(call, deadline, clock, cleanup=None): """Bound a read-only preflight operation, never an inference submission. SDK predict/initialization may wait beyond an HTTP request timeout. The daemon performs only this one read; it cannot advance to a GPU submission or update the journal after the caller times out. Late-created clients close. """ if clock() >= deadline: raise TimeoutError() future = Future() def invoke(): try: future.set_result(call()) except BaseException as error: future.set_exception(error) threading.Thread(target=invoke, name="workflow-preflight", daemon=True).start() try: remaining = deadline - clock() if remaining <= 0: raise TimeoutError() return future.result(timeout=remaining) except BaseException: if cleanup is not None: def close_late(done): with suppress(Exception): if not done.cancelled() and done.exception() is None: cleanup(done.result()) future.add_done_callback(close_late) raise def _capture(hop, client, job=None): session = getattr(client, "session_hash", None) if isinstance(session, str): hop["session_hash"] = session event = getattr(getattr(job, "communicator", None), "event_id", None) if isinstance(event, str): hop["event_id"] = event def _submit_once(client, name, args, api, journal, deadline, clock): if clock() >= deadline: raise TimeoutError() hop = journal.data["hops"][name] # An abrupt process death after this fsync is ambiguous, never resubmittable. hop["state"] = "unknown" journal.data["state"] = "unknown" journal.data["active_hop"] = name _capture(hop, client) journal.save() job = None try: job = client.submit(*args, api_name=api) while True: _capture(hop, client, job) journal.save() remaining = deadline - clock() if remaining <= 0: raise TimeoutError() try: result = job.future.result(timeout=min(2.0, remaining)) except TimeoutError: if job.future.done(): raise continue _capture(hop, client, job) hop["state"] = "complete" journal.data["state"] = name + "_complete" journal.save() return result finally: # Event IDs often arrive while waiting, including just before errors. _capture(hop, client, job) journal.save() def _artifact(result, downloads): if not isinstance(result, (tuple, list)) or len(result) != 2 or not isinstance(result[1], dict): raise IntegrityError() value = result[0] # gr.Video may deserialize into (video_path, subtitle_path). if isinstance(value, (list, tuple)) and len(value) == 2: value = value[0] if isinstance(value, dict): value = value.get("path") or value.get("video") if isinstance(value, dict): value = value.get("path") if isinstance(value, Path): value = str(value) path = _local_path(value, downloads) if not path.is_relative_to(downloads.resolve()): raise IntegrityError() return path, result[1] def _sha(value): if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None: raise IntegrityError() return value def _plan(client, request): result = client.predict(*request.plan_args(), api_name="/plan") if (not isinstance(result, dict) or not isinstance(result.get("canvas"), str) or not result["canvas"] or not isinstance(result.get("adapter"), str)): raise InputError() prompt = result.get("prompt") if not isinstance(prompt, str) or not prompt or prompt != prompt.strip() or len(prompt) > 12000: raise InputError() for field in ("height", "width", "num_frames", "steps"): if type(result.get(field)) is not int or result[field] <= 0: raise InputError() return result def _run(args, request, token, factory, metadata, identity, wrap, clock): journal = Journal(args.run_dir, token, args.expected_revision) clients = [] deadline = clock() + args.max_wall_seconds try: staged = {} for name in ("image", "last_image"): source = getattr(request, name) if source is not None: suffix = source.suffix if re.fullmatch(r"\.[A-Za-z0-9]{1,10}", source.suffix) else ".bin" target = journal.directory / (name + suffix) staged[name] = _copy_private(source, target) journal.data["inputs"] = staged journal.data["request"] = {"preset_id": request.preset_id, "prompt": request.prompt, "canvas": request.canvas, "duration": request.duration, "seed": request.seed} journal.save() caller = _within_deadline(lambda: identity(token), deadline, clock).get("name") if not isinstance(caller, str) or not caller: raise AuthenticationRequired() if caller.casefold() == "owenisas": raise OwnerTokenForbidden() journal.data["caller"] = caller if _within_deadline(lambda: metadata(GENERATOR_REPO, token), deadline, clock).get("sha") != args.expected_revision: raise RevisionMismatch() downloads = journal.directory / "workflow-downloads" downloads.mkdir(mode=0o700) generator = _within_deadline( lambda: _new_client(factory, GENERATOR, token, downloads), deadline, clock, cleanup=lambda client: client.close()) clients.append(generator) health = _within_deadline(lambda: generator.predict(api_name="/health"), deadline, clock) # Models are staged and loaded inside GPU calls, not held resident here. if (not isinstance(health, dict) or type(health.get("schema_version")) is not int or health["schema_version"] != 2 or health.get("status") != "ready" or health.get("blockers") != [] or health.get("runtime_initialized") is not True or health.get("conditioning_transport") != "in_process"): raise RuntimeNotReady() journal.data["health"] = health plan = _within_deadline(lambda: _plan(generator, request), deadline, clock) journal.data["plan"] = plan journal.save() def image(name): if name not in staged: return None path = journal.directory / staged[name]["path"] _verify_file(path, staged[name]["sha256"]) return wrap(path) first, last = image("image"), image("last_image") result = _submit_once(generator, "workflow", (request.preset_id, plan["prompt"], first, last, plan["canvas"], request.duration, request.seed), "/generate", journal, deadline, clock) path, report = _artifact(result, downloads) expected = _sha(report.get("output_sha256")) output = _copy_private(path, journal.directory / "final.mp4", expected) journal.data["result"] = _sanitize(report, token) journal.data["output"] = output journal.data["state"] = "succeeded" journal.save() return {"state": "succeeded", "output": str(journal.directory / "final.mp4"), "result": journal.data["result"]} except BaseException as error: journal.data["state"] = ("unknown" if any(h["state"] == "unknown" for h in journal.data["hops"].values()) else "failed") journal.data["error"] = type(error).__name__ journal.save() raise finally: for client in clients: with suppress(Exception): client.close() class _Parser(argparse.ArgumentParser): def error(self, message): raise CLIArgumentError() def _parser(): parser = _Parser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) commands = parser.add_subparsers(dest="command", required=True) commands.add_parser("presets", help="List public presets without credentials") commands.add_parser("health", help="Read public runtime readiness without credentials") plan = commands.add_parser("plan", help="Validate and plan; no files uploaded") plan.add_argument("--input", required=True, type=Path) run = commands.add_parser("run", help="One-shot authenticated single-Space inference") run.add_argument("--input", required=True, type=Path) run.add_argument("--run-dir", required=True, type=Path) run.add_argument("--expected-revision", required=True) run.add_argument("--token-env", default="HF_INFERENCE_TOKEN") run.add_argument("--max-wall-seconds", default=900.0, type=float) return parser def main(argv=None, *, env=None, client_factory=None, metadata_fetcher=None, identity_fetcher=None, file_wrapper=None, clock=time.monotonic): """Dependencies are injectable for offline tests; return an exit status.""" env = os.environ if env is None else env token = "" try: args = _parser().parse_args(argv) request = load_input(args.input) if args.command in ("plan", "run") else None factory = client_factory or _client_factory if args.command == "run": if re.fullmatch(r"[0-9a-f]{40}", args.expected_revision) is None: raise InputError() if not math.isfinite(args.max_wall_seconds) or not 0 < args.max_wall_seconds <= 86400: raise InputError() if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", args.token_env) is None: raise InputError() token = env.get(args.token_env, "") if not isinstance(token, str) or not token or any(c.isspace() for c in token): raise AuthenticationRequired() if env.get("HF_POOL_ZEROGPU_OVERAGE_DISABLED") != "1": raise OverageAttestationRequired() # Do not relay SDK prints, provider errors, or warnings to CLI output. with _termination_guard(), open(os.devnull, "w") as sink, redirect_stdout(sink), redirect_stderr(sink): if args.command == "run": result = _run(args, request, token, factory, metadata_fetcher or fetch_metadata, identity_fetcher or fetch_identity, file_wrapper or _file_wrapper, clock) else: client = _new_client(factory, GENERATOR, False) try: result = (_plan(client, request) if args.command == "plan" else client.predict(api_name="/" + args.command)) finally: with suppress(Exception): client.close() print(json.dumps(_sanitize(result, token), sort_keys=True, allow_nan=False)) return 0 except SystemExit as error: # argparse help is the only non-JSON successful output. return int(error.code or 0) except (Exception, KeyboardInterrupt) as error: print(json.dumps({"state": "error", "error": _sanitize(type(error).__name__, token)}), file=sys.stderr) if isinstance(error, TerminationRequested): return 143 return 130 if isinstance(error, KeyboardInterrupt) else 1 if __name__ == "__main__": code = main() sys.stdout.flush() sys.stderr.flush() # ThreadPoolExecutor's atexit join can outlive an unknown remote job. The # CLI must exit within its polling bound without cancelling that job. # All durable writes above are fsynced before returning from main(). os._exit(code)