diff --git a/beaver_agent/hands/komodo_logs.py b/beaver_agent/hands/komodo_logs.py index b4fcd64..790bb64 100644 --- a/beaver_agent/hands/komodo_logs.py +++ b/beaver_agent/hands/komodo_logs.py @@ -6,6 +6,11 @@ и обратный переход «было → нет» - с временем последнего события, не с временем прохода (логи берутся с `--timestamps`, время в строке настоящее). +Показанную строку лога чистит общий `security.redact` - тот же, что стоит на +логах и на выдаче MCP: правила в одном месте, и он знает литеральные значения +секретов из env. Голые блобы без имени рядом он не трогает намеренно - id +задач, сессий и коммитов это ровно то, по чему беду потом и опознают. + Показание всегда несёт свою область: сколько стеков в области, сколько из них прочиталось, какой отрезок логов попал в хвост. Без этого «всё чисто» неотличимо от «половину не посмотрел», поэтому чего не смог - тоже в инжекте. @@ -20,6 +25,8 @@ from dataclasses import dataclass, field from datetime import UTC, datetime, timedelta, tzinfo from typing import TYPE_CHECKING, Any +from beaver_gateway.security.redact import redact + from beaver_agent.hands.komodo import ANSI if TYPE_CHECKING: @@ -43,9 +50,16 @@ SCOPE = ("dell-home", "personal-main") """Свои серверы. Машины заказчиков (md-cars-*, veris-*, ms-agents-*) не наши: там чужой прод и чужие секреты, и читать их логи в свой контекст - нельзя.""" -TAIL = 500 -"""Строк на стек. Болтливый стек столько строк проживает за минуты - об этом -в инжекте говорится отдельно, а не выдаётся за просмотренный промежуток.""" +TAIL = 2000 +"""Строк на контейнер, и число из замера, а не «чтобы хватало». + +Замер 20.09.2026 по всем 36 стекам области: при 500 строках хвост покрывал +меньше трёх часов (промежуток между проходами) ровно у одного стека - +`dell-home-music`, ~1170 строк/ч, то есть 51 минута; следующий по болтливости +`personal-main-caddy` покрывал 11,6 часа, `beaver-agent` - 20 ч, медиана по +области около 110 часов. 2000 строк закрывают и music (~3,4 часа) почти +бесплатно: тихие стеки отдают столько, сколько у них есть, растут три-четыре +болтливых. Где хвост всё равно короче промежутка, инжект говорит об этом сам.""" SIGNALS: tuple[tuple[str, str], ...] = ( ("критично", r"\bCRITICAL\b|\bFATAL\b|\bEMERG\b"), @@ -72,11 +86,6 @@ STAMP_RE = re.compile( """Пустая строка тоже со штампом (`сервис | время` и ничего) - это не мусор.""" DIGITS_RE = re.compile(r"\b[0-9a-f]{7,}\b|\d+") """Числа и хеши в строке - шум: без них «то же самое» узнаётся между проходами.""" -SECRET_RE = re.compile( - r"(?i)\b(?:token|secret|api[-_ ]?key|password|passwd|authorization)\b\s*[=:]\s*\S+" - r"|\b[A-Za-z0-9_\-]{32,}\b" -) -"""Инжект уезжает в чат и в vault: похожее на ключ в показании не нужно.""" SAMPLE_CHARS = 180 MAX_NEW = 25 @@ -314,11 +323,6 @@ def _parse(raw: Any) -> datetime | None: return at if at.tzinfo else at.replace(tzinfo=UTC) -def _mask(text: str) -> str: - """Замазать похожее на секрет: строка лога едет мастеру как есть.""" - return SECRET_RE.sub("<скрыто>", text) - - def _normal(text: str) -> str: """Строка без чисел, хешей и адресов: по ней узнаётся «та же самая беда».""" return DIGITS_RE.sub("#", ANSI.sub("", text)).strip() @@ -374,7 +378,7 @@ def read_stack(stack: str, log: dict[str, Any]) -> StackScan: stack=stack, service=service, signal=signal, - sample=(before.sample if before else _mask(rest)[:SAMPLE_CHARS]), + sample=(before.sample if before else redact(rest)[:SAMPLE_CHARS]), at=at or (before.at if before else None), count=(before.count if before else 0) + 1, ) diff --git a/tests/test_komodo_logs.py b/tests/test_komodo_logs.py index 04a73e9..8be19b7 100644 --- a/tests/test_komodo_logs.py +++ b/tests/test_komodo_logs.py @@ -289,7 +289,7 @@ def test_the_scope_says_how_much_was_looked_at(tmp_path): "Область: dell-home, personal-main - прочитано 1 стеков из 2" ) assert "во флоте 53" in head - assert "хвост 500 строк на стек: строки с 20.09 17:40 по 20.09 17:40" in head + assert "хвост 2000 строк на стек: строки с 20.09 17:40 по 20.09 17:40" in head assert "самый короткий хвост у beaver-agent - только с 20.09 17:40" in head @@ -448,18 +448,22 @@ def test_a_first_pass_into_a_dead_komodo_does_not_burn_the_baseline(tmp_path): assert later.cold and later.new == () -def test_a_secret_in_the_line_does_not_ride_into_the_chat(): +def test_a_named_secret_in_the_line_does_not_ride_into_the_chat(): scan = read_stack( "gitea", - log( - line( - "svc", - "2026-09-20T17:40:00.0", - "ERROR auth failed token=ghp_ZZ123 for kJ8sdfLKJ23kjhsdf98234hkjsdf7823x", - ) - ), + log(line("svc", "2026-09-20T17:40:00.0", "ERROR auth failed token=ghp_ZZ123")), ) (hit,) = scan.hits assert "ghp_ZZ123" not in hit.sample - assert "kJ8sdfLKJ23kjhsdf98234hkjsdf7823x" not in hit.sample - assert hit.sample.startswith("ERROR auth failed") + assert hit.sample.startswith("ERROR auth failed token=") + + +def test_a_bare_blob_stays_readable(): + # Размен с открытыми глазами: id задач, сессий и коммитов - это сигнал, + # и замазывать их ради редкого безымянного ключа дороже, чем оставить. + scan = read_stack( + "komodo", + log(line("svc", "2026-09-20T17:40:00.0", "ERROR task kJ8sdfLKJ23kjhsdf98234x")), + ) + (hit,) = scan.hits + assert "kJ8sdfLKJ23kjhsdf98234x" in hit.sample