From 01d85b1eb410b3aaf048cbeb546a5af5791b453f Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:42:40 -0700 Subject: [PATCH 1/4] fix(obs-studio): reject non-finite geometry in position/size/crop validate_position accepted NaN coordinates into project JSON. size/crop converted NaN via int() with a cryptic error. Reject non-finite values up front, matching validate_range. --- .../tests/test_validate_geometry_nan.py | 32 +++++++++++++++++++ .../obs_studio/utils/obs_utils.py | 25 +++++++++++---- 2 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py new file mode 100644 index 0000000000..c588edd5b6 --- /dev/null +++ b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py @@ -0,0 +1,32 @@ +"""Reject non-finite values in OBS geometry validators.""" + +from __future__ import annotations + +import math + +import pytest + +from cli_anything.obs_studio.utils.obs_utils import ( + validate_crop, + validate_position, + validate_size, +) + + +def test_validate_position_rejects_nan(): + with pytest.raises(ValueError, match="finite"): + validate_position({"x": float("nan"), "y": 1.0}) + + +def test_validate_size_rejects_nan(): + with pytest.raises(ValueError, match="finite"): + validate_size({"width": float("nan"), "height": 1080}) + + +def test_validate_crop_rejects_inf(): + with pytest.raises(ValueError, match="finite"): + validate_crop({"left": float("inf"), "right": 0, "top": 0, "bottom": 0}) + + +def test_validate_position_accepts_finite(): + assert validate_position({"x": 1.5, "y": -2.0}) == {"x": 1.5, "y": -2.0} diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py b/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py index 7c6f90a7bf..494f1912f1 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py @@ -1,6 +1,7 @@ """OBS Studio CLI - JSON helpers and utilities.""" import json +import math import os import copy from typing import Dict, Any, List, Optional @@ -34,16 +35,23 @@ def validate_range(value: float, min_val: float, max_val: float, name: str) -> f def validate_position(pos: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a position dict.""" - return { - "x": float(pos.get("x", 0)), - "y": float(pos.get("y", 0)), - } + x = float(pos.get("x", 0)) + y = float(pos.get("y", 0)) + if not math.isfinite(x) or not math.isfinite(y): + raise ValueError(f"Position coordinates must be finite numbers, got x={x}, y={y}") + return {"x": x, "y": y} def validate_size(size: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a size dict.""" - w = int(size.get("width", 1920)) - h = int(size.get("height", 1080)) + width_raw = float(size.get("width", 1920)) + height_raw = float(size.get("height", 1080)) + if not math.isfinite(width_raw) or not math.isfinite(height_raw): + raise ValueError( + f"Size must be finite numbers, got width={width_raw}, height={height_raw}" + ) + w = int(width_raw) + h = int(height_raw) if w < 1: raise ValueError(f"Width must be positive, got {w}") if h < 1: @@ -55,7 +63,10 @@ def validate_crop(crop: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a crop dict.""" result = {} for key in ("top", "bottom", "left", "right"): - val = int(crop.get(key, 0)) + raw = float(crop.get(key, 0)) + if not math.isfinite(raw): + raise ValueError(f"Crop {key} must be a finite number, got {raw}") + val = int(raw) if val < 0: raise ValueError(f"Crop {key} must be non-negative, got {val}") result[key] = val From c37df116c968367718441f3239069f8f94545744 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:42:50 -0700 Subject: [PATCH 2/4] test(obs-studio): drop unused math import in geometry nan tests --- .../cli_anything/obs_studio/tests/test_validate_geometry_nan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py index c588edd5b6..5dd8c8a221 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py @@ -2,8 +2,6 @@ from __future__ import annotations -import math - import pytest from cli_anything.obs_studio.utils.obs_utils import ( From 516177b974688fa7b98a951ec33a870796a0af87 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:46:43 -0700 Subject: [PATCH 3/4] fix(obs-studio): wire finite geometry checks into source transforms validate_position/size/crop were unused while add_source and transform_source accepted NaN into project JSON. Call the validators from those paths and cover the live transform entrypoint. --- .../cli_anything/obs_studio/core/sources.py | 54 +++++++++++-------- .../tests/test_validate_geometry_nan.py | 40 ++++++++++++++ 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/core/sources.py b/obs-studio/agent-harness/cli_anything/obs_studio/core/sources.py index 3e99c4a917..d18c1804b5 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/core/sources.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/core/sources.py @@ -1,8 +1,17 @@ """OBS Studio CLI - Source management.""" import copy +import math from typing import Dict, Any, List, Optional -from cli_anything.obs_studio.utils.obs_utils import generate_id, unique_name, get_item, validate_range +from cli_anything.obs_studio.utils.obs_utils import ( + generate_id, + unique_name, + get_item, + validate_range, + validate_position, + validate_size, + validate_crop, +) SOURCE_TYPES = { @@ -122,13 +131,9 @@ def add_source( src["visible"] = visible if position: - src["position"] = {"x": float(position.get("x", 0)), "y": float(position.get("y", 0))} + src["position"] = validate_position(position) if size: - w = int(size.get("width", 1920)) - h = int(size.get("height", 1080)) - if w < 1 or h < 1: - raise ValueError(f"Size must be positive: {w}x{h}") - src["size"] = {"width": w, "height": h} + src["size"] = validate_size(size) if settings: src["settings"].update(settings) @@ -201,25 +206,28 @@ def transform_source( source = get_item(sources, source_index, "source") if position: - source["position"] = { - "x": float(position.get("x", source["position"]["x"])), - "y": float(position.get("y", source["position"]["y"])), - } + source["position"] = validate_position( + { + "x": position.get("x", source["position"]["x"]), + "y": position.get("y", source["position"]["y"]), + } + ) if size: - w = int(size.get("width", source["size"]["width"])) - h = int(size.get("height", source["size"]["height"])) - if w < 1 or h < 1: - raise ValueError(f"Size must be positive: {w}x{h}") - source["size"] = {"width": w, "height": h} + source["size"] = validate_size( + { + "width": size.get("width", source["size"]["width"]), + "height": size.get("height", source["size"]["height"]), + } + ) if crop: - for key in ("top", "bottom", "left", "right"): - if key in crop: - val = int(crop[key]) - if val < 0: - raise ValueError(f"Crop {key} must be non-negative, got {val}") - source["crop"][key] = val + merged_crop = dict(source.get("crop") or {}) + merged_crop.update(crop) + source["crop"] = validate_crop(merged_crop) if rotation is not None: - source["rotation"] = float(rotation) + rot = float(rotation) + if not math.isfinite(rot): + raise ValueError(f"Rotation must be a finite number, got {rot}") + source["rotation"] = rot return source diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py index 5dd8c8a221..9639b54307 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/tests/test_validate_geometry_nan.py @@ -4,6 +4,7 @@ import pytest +from cli_anything.obs_studio.core.sources import add_source, transform_source from cli_anything.obs_studio.utils.obs_utils import ( validate_crop, validate_position, @@ -11,6 +12,31 @@ ) +def _project() -> dict: + return { + "scenes": [ + { + "name": "Scene", + "sources": [ + { + "id": 0, + "name": "Cam", + "type": "browser", + "visible": True, + "locked": False, + "opacity": 1.0, + "rotation": 0.0, + "position": {"x": 0.0, "y": 0.0}, + "size": {"width": 1920, "height": 1080}, + "crop": {"top": 0, "bottom": 0, "left": 0, "right": 0}, + "settings": {}, + } + ], + } + ] + } + + def test_validate_position_rejects_nan(): with pytest.raises(ValueError, match="finite"): validate_position({"x": float("nan"), "y": 1.0}) @@ -28,3 +54,17 @@ def test_validate_crop_rejects_inf(): def test_validate_position_accepts_finite(): assert validate_position({"x": 1.5, "y": -2.0}) == {"x": 1.5, "y": -2.0} + + +def test_add_source_rejects_nan_position(): + with pytest.raises(ValueError, match="finite"): + add_source( + _project(), + "browser", + position={"x": float("nan"), "y": 0}, + ) + + +def test_transform_source_rejects_nan_position(): + with pytest.raises(ValueError, match="finite"): + transform_source(_project(), 0, position={"x": float("nan")}) From 8438eb9f0b66a96df219abbb8a8a5b0977292efd Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:07:08 -0700 Subject: [PATCH 4/4] fix(obs-studio): validate finite geometry without float-rounding ints --- .../obs_studio/utils/obs_utils.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py b/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py index 494f1912f1..f95305e829 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/utils/obs_utils.py @@ -7,6 +7,22 @@ from typing import Dict, Any, List, Optional + +def _finite_number(value: Any, name: str): + """Reject non-finite values without float-rounding large ints.""" + if isinstance(value, bool): + raise ValueError(f"{name} must be a finite number, got {value!r}") + if isinstance(value, int): + return value + try: + num = float(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{name} must be a finite number, got {value!r}") from exc + if not math.isfinite(num): + raise ValueError(f"{name} must be a finite number, got {value!r}") + return num + + def generate_id(items: List[Dict[str, Any]]) -> int: """Generate the next unique ID for a list of items.""" if not items: @@ -35,21 +51,15 @@ def validate_range(value: float, min_val: float, max_val: float, name: str) -> f def validate_position(pos: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a position dict.""" - x = float(pos.get("x", 0)) - y = float(pos.get("y", 0)) - if not math.isfinite(x) or not math.isfinite(y): - raise ValueError(f"Position coordinates must be finite numbers, got x={x}, y={y}") + x = _finite_number(pos.get("x", 0), "Position x") + y = _finite_number(pos.get("y", 0), "Position y") return {"x": x, "y": y} def validate_size(size: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a size dict.""" - width_raw = float(size.get("width", 1920)) - height_raw = float(size.get("height", 1080)) - if not math.isfinite(width_raw) or not math.isfinite(height_raw): - raise ValueError( - f"Size must be finite numbers, got width={width_raw}, height={height_raw}" - ) + width_raw = _finite_number(size.get("width", 1920), "Size width") + height_raw = _finite_number(size.get("height", 1080), "Size height") w = int(width_raw) h = int(height_raw) if w < 1: @@ -63,9 +73,7 @@ def validate_crop(crop: Dict[str, Any]) -> Dict[str, Any]: """Validate and normalize a crop dict.""" result = {} for key in ("top", "bottom", "left", "right"): - raw = float(crop.get(key, 0)) - if not math.isfinite(raw): - raise ValueError(f"Crop {key} must be a finite number, got {raw}") + raw = _finite_number(crop.get(key, 0), f"Crop {key}") val = int(raw) if val < 0: raise ValueError(f"Crop {key} must be non-negative, got {val}")