fix: harden tool governance and subprocess safety

- persist budget warnings and approved paid-tool decisions
- support binary dependency declarations
- include stderr/stdout details for failed subprocesses
- escape lavfi movie paths used by ffmpeg scene detection

Verification:
- python3 tests/tools/test_cost_tracker_governance.py
- python3 tests/tools/test_scene_detect_lavfi_escape.py
- python3 tests/tools/test_base_tool_dependencies.py
- python3 -m py_compile ...
This commit is contained in:
Morpheus
2026-07-03 20:07:00 +08:00
parent 80e51fd618
commit 6580987931
6 changed files with 187 additions and 21 deletions

View File

@@ -209,8 +209,9 @@ class BaseTool(ABC):
def check_dependencies(self) -> None:
"""Verify all dependencies are installed. Raises DependencyError if not."""
for dep in self.dependencies:
if dep.startswith("cmd:"):
cmd_name = dep[4:]
if dep.startswith(("cmd:", "binary:")):
prefix = "cmd:" if dep.startswith("cmd:") else "binary:"
cmd_name = dep[len(prefix):]
if shutil.which(cmd_name) is None:
raise DependencyError(
f"Command {cmd_name!r} not found. {self.install_instructions}"
@@ -329,20 +330,28 @@ class BaseTool(ABC):
exe = shutil.which(resolved_cmd[0])
if exe:
resolved_cmd[0] = exe
return subprocess.run(
resolved_cmd,
capture_output=True,
text=True,
# Force UTF-8 decoding. The default uses the OS locale (cp1252 on
# Windows), which raises UnicodeDecodeError on a subprocess that
# emits Unicode/emoji (e.g. Remotion's progress output), killing the
# reader thread and potentially swallowing the real error text.
encoding="utf-8",
errors="replace",
timeout=timeout,
cwd=cwd,
check=True,
)
try:
return subprocess.run(
resolved_cmd,
capture_output=True,
text=True,
# Force UTF-8 decoding. The default uses the OS locale (cp1252 on
# Windows), which raises UnicodeDecodeError on a subprocess that
# emits Unicode/emoji (e.g. Remotion's progress output), killing the
# reader thread and potentially swallowing the real error text.
encoding="utf-8",
errors="replace",
timeout=timeout,
cwd=cwd,
check=True,
)
except subprocess.CalledProcessError as exc:
stderr = (exc.stderr or "").strip()
stdout = (exc.stdout or "").strip()
detail = stderr or stdout or str(exc)
raise RuntimeError(
f"Command failed with exit code {exc.returncode}: {' '.join(resolved_cmd)}\n{detail}"
) from exc
class DependencyError(Exception):