Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion builder/jarvis-agent-pipeline
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down
40 changes: 40 additions & 0 deletions builder/tests/test_agent_ping_auto_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="на связи без обязательного маркера",
Expand Down