diff --git a/infra/bin/fly-loop-recover b/infra/bin/fly-loop-recover index 404ad0b..71de1c0 100755 --- a/infra/bin/fly-loop-recover +++ b/infra/bin/fly-loop-recover @@ -17,8 +17,10 @@ COOLDOWN = 3600 def verdict(report): url = os.environ.get("FLY_LOOP_ROUTER_URL") model = os.environ.get("FLY_LOOP_MODEL") - if not url or not model: + if not url and not model: return True + if not url or not model: + return False payload = {"model": model, "temperature": 0, "messages": [ {"role": "system", "content": "Classify whether a suspected repeating game macro loop is truly stuck. Return only JSON {\"stuck\":true|false}. No instructions or commands."}, {"role": "user", "content": json.dumps({key: report.get(key) for key in ("reason", "sequence", "window", "places", "milestone")})}, diff --git a/infra/docs/loop-recovery.md b/infra/docs/loop-recovery.md index 41c01df..4982482 100644 --- a/infra/docs/loop-recovery.md +++ b/infra/docs/loop-recovery.md @@ -18,8 +18,8 @@ containing `FLY_LOOP_ROUTER_URL` (base URL ending in `/v1`) and `FLY_LOOP_MODEL` (an available free-tier model). A private router can additionally use `FLY_LOOP_ROUTER_KEY`; provision it outside this public checkout and limit file permissions to `0640 root:fly`. Never put credentials in the unit or the -repository. When a router is configured, malformed or unavailable responses -prevent recovery; the model may only return `{"stuck": true|false}` and cannot +repository. When either router setting is present, both must be set; malformed or +unavailable responses prevent recovery. The model may only return `{"stuck": true|false}` and cannot choose commands, buttons, or checkpoint paths. Confirm the model actually exists and is reachable from the release container before configuring it. diff --git a/infra/tests/test_loop_recover.py b/infra/tests/test_loop_recover.py index 190b42b..7d0c481 100644 --- a/infra/tests/test_loop_recover.py +++ b/infra/tests/test_loop_recover.py @@ -76,6 +76,12 @@ class RecoveryTests(unittest.TestCase): self.assertIn("did not confirm", recover.run(1300)) restart.assert_not_called() + def test_partial_router_configuration_cannot_bypass_veto(self): + with patch.dict("os.environ", {"FLY_LOOP_ROUTER_URL": "http://router/v1"}, clear=True): + self.assertFalse(recover.verdict(self.report)) + with patch.dict("os.environ", {"FLY_LOOP_MODEL": "free"}, clear=True): + self.assertFalse(recover.verdict(self.report)) + def test_unconfigured_router_uses_deterministic_confirmation(self): with patch.dict("os.environ", {}, clear=True): self.assertTrue(recover.verdict(self.report))