diff --git a/backend/tests/test_yolo_training_supervisor.py b/backend/tests/test_yolo_training_supervisor.py index 0b11596d..fc35cd3a 100644 --- a/backend/tests/test_yolo_training_supervisor.py +++ b/backend/tests/test_yolo_training_supervisor.py @@ -33,5 +33,17 @@ def test_container_running_fails_closed_on_inspect_error(monkeypatch) -> None: def test_completion_command_requires_non_empty_string_list(tmp_path: Path) -> None: invalid = tmp_path / "command.json" invalid.write_text('{"shell": "unsafe"}', encoding="utf-8") - value = MODULE.json.loads(invalid.read_text(encoding="utf-8")) - assert not isinstance(value, list) + try: + MODULE.load_completion_command(invalid) + except ValueError as exc: + assert "non-empty JSON list" in str(exc) + else: + raise AssertionError("mapping-shaped completion command was accepted") + + +def test_completion_command_preserves_exact_argv_without_shell_parsing(tmp_path: Path) -> None: + command = tmp_path / "command.json" + command.write_text('["docker", "exec", "-d", "geointel", "python", "loop.py"]', encoding="utf-8") + assert MODULE.load_completion_command(command) == [ + "docker", "exec", "-d", "geointel", "python", "loop.py" + ] diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 8288b56d..10f291dc 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -11799,6 +11799,11 @@ Deployment evidence: `results.png` appears. The bound command permits up to 20 iterations, keeps protected test/background closed until calibration passes, and preserves the exact AdamW, 180-degree rotation, flip, scale and translation contract. +- Extracted and tested strict completion-command validation. Only a non-empty + JSON list of non-empty argv strings is accepted; mappings, empty arguments, + malformed JSON and filesystem errors produce an explicit fail-closed + supervisor state before any subprocess is started. The updated supervisor + was activated live with one remaining v37 GPU process. - Added those aerial augmentation parameters to the orchestrator CLI and training command, preventing later failure-driven checkpoints from silently reverting to generic orientation assumptions. diff --git a/scripts/supervise_container_yolo_training.py b/scripts/supervise_container_yolo_training.py index 4e7716e1..c4e98fe1 100644 --- a/scripts/supervise_container_yolo_training.py +++ b/scripts/supervise_container_yolo_training.py @@ -37,6 +37,13 @@ def write_state(path: Path, payload: dict) -> None: temporary.replace(path) +def load_completion_command(path: Path) -> list[str]: + command = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(command, list) or not command or not all(isinstance(x, str) and x for x in command): + raise ValueError("completion command must be a non-empty JSON list of non-empty strings") + return command + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--container", required=True) @@ -64,9 +71,11 @@ def main() -> int: write_state(state_path, state) return 0 if args.completion_command_json: - command = json.loads(args.completion_command_json.read_text(encoding="utf-8")) - if not isinstance(command, list) or not command or not all(isinstance(x, str) and x for x in command): + try: + command = load_completion_command(args.completion_command_json) + except (OSError, json.JSONDecodeError, ValueError) as exc: state["status"] = "invalid_completion_command" + state["completion_handoff_error"] = str(exc) write_state(state_path, state) return 4 result = subprocess.run(command, check=False)