From 4105c59930751e1e54d350a3220abd69d7e2f6b8 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 12 Aug 2026 17:14:34 +0900 Subject: [PATCH] Accept safe timestamp from agent runner --- builder/jarvis-agent-pipeline | 11 ++++++- builder/tests/test_agent_ping_auto_run.py | 40 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/builder/jarvis-agent-pipeline b/builder/jarvis-agent-pipeline index 6ac8e14..b23052b 100755 --- a/builder/jarvis-agent-pipeline +++ b/builder/jarvis-agent-pipeline @@ -665,13 +665,14 @@ append_agent_runner_result() { import json import os import re +from datetime import datetime from pathlib import Path from urllib.parse import urlsplit SAFE_STATES = {"checking", "working", "idle", "blocked", "failed"} REQUIRED_KEYS = {"role", "state", "summary", "next_step", "auto_started"} -OPTIONAL_KEYS = {"issue_url", "issue_number"} +OPTIONAL_KEYS = {"issue_url", "issue_number", "updated_at"} PRIVATE_MARKERS = ( "/users/", "/private/", "file://", "http://", "https://", "ghp_", "github_pat_", "prompt", "issue_body", "tool_output", "model_output", @@ -756,6 +757,14 @@ try: raise ValueError("invalid keys") if not REQUIRED_KEYS.issubset(data): raise ValueError("missing keys") + if "updated_at" in data: + updated_at = data["updated_at"] + if not isinstance(updated_at, str) or not re.fullmatch( + r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z", + updated_at, + ): + raise ValueError("invalid updated_at") + datetime.strptime(updated_at, "%Y-%m-%dT%H:%M:%SZ") role = os.environ.get("JARVIS_AGENT_RUNNER_ROLE", "") if data.get("role") != role or data.get("state") not in SAFE_STATES: raise ValueError("invalid role or state") diff --git a/builder/tests/test_agent_ping_auto_run.py b/builder/tests/test_agent_ping_auto_run.py index fd81813..141f2f7 100644 --- a/builder/tests/test_agent_ping_auto_run.py +++ b/builder/tests/test_agent_ping_auto_run.py @@ -347,6 +347,46 @@ def test_ac1_ac4_valid_pong_precedes_exact_installed_runner_delegation(self): self.assertIn("- Agent state: idle", report_text) self.assertIn("- Agent auto-started: false", report_text) + def test_bug_safe_runner_timestamp_is_accepted_and_malformed_values_fail_closed(self): + result, calls, report = self._run_pipeline( + pong="PONG — Разработчик на связи", + runner_output={ + "role": "builder", + "state": "idle", + "summary": "Разработчик на связи.", + "next_step": "Нет одобренной задачи для этого агента.", + "auto_started": False, + "updated_at": "2026-08-12T08:11:39Z", + }, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls.read_text(encoding="utf-8").splitlines(), ["builder"]) + report_text = report.read_text(encoding="utf-8") + self.assertIn("- Agent state: idle", report_text) + self.assertIn("- Agent auto-started: false", report_text) + + for updated_at in ("2026-08-12 08:11:39", "../../private", True): + with self.subTest(updated_at=updated_at): + result, calls, report = self._run_pipeline( + pong="PONG — Разработчик на связи", + runner_output={ + "role": "builder", + "state": "idle", + "summary": "Разработчик на связи.", + "next_step": "Нет одобренной задачи для этого агента.", + "auto_started": False, + "updated_at": updated_at, + }, + ) + self.assertNotEqual(result.returncode, 0) + self.assertEqual( + calls.read_text(encoding="utf-8").splitlines(), ["builder"] + ) + report_text = report.read_text(encoding="utf-8") + self.assertIn("- Agent state: failed", report_text) + self.assertIn("- Agent auto-started: false", report_text) + def test_ac1_invalid_pong_never_calls_the_mutating_runner(self): result, calls, report = self._run_pipeline( pong="на связи без обязательного маркера",