firewall guard: барьер покоя fail-closed и явный контракт транзиентного таймера

Барьер, обязанный ДОКАЗАТЬ отсутствие асинхронного исполнителя, в трёх местах
принимал за доказательство отсутствие наблюдения.

- отказ `systemctl` больше не выдаётся за отсутствие guard: вместо `return []`
  введён единый наблюдатель inspectRollbackGuard с исходами quiescent/pending/
  unknown и отдельным типом отказа GuardStateUnknownError;
- покой перечисляется белым списком (inactive, failed): maintenance,
  refreshing и любое незнакомое состояние systemd блокируют операцию;
- у транзиентного таймера явно заданы AccuracySec=1s (умолчание 1min
  превращало обещанные 45 секунд в 45-105) и RemainAfterElapse=no; барьер
  дополнительно опознаёт SubState=elapsed у *.timer как покой;
- команда взведения строится чистой buildArmGuardArgv и выполняется новым
  runMutatingArgv без shell, поэтому её контракт проверяется значением, а не
  грепом по исходнику;
- status перестал листить guard-юниты своей копией кода: без --plain, с
  `|| true` и с трактовкой failed как «вооружён» отчёт вечно противоречил
  барьеру. Добавлены rollback_guard_state и firewall_state=guard_unknown;
- purge-v0.sh пропускал failed-юниты из-за маркера в первой колонке.

Барьер покрыт поведенческими тестами через подставляемый SystemdUnitProbe:
прежние проверки грепом по тексту функции пережили инверсию смысла - строка
`return [];` была на месте, а решение стало неверным.

Документация (README, docs/07, 11, 12, 13, 14, CHANGELOG) приведена к реальному
окну 45-46 секунд и к новому тексту отказа. Отдельно исправлен комментарий
PNPM_AUDIT_LEVEL в versions.env: гейт давно проверяет весь lock-граф.
This commit is contained in:
2026-08-31 16:44:20 +05:00
parent 76d78ac71f
commit 2259f7c847
14 changed files with 1115 additions and 118 deletions
+10 -3
View File
@@ -111,10 +111,17 @@ require_root() {
# Таймеры отката firewall переживают неудачную установку и продолжат менять
# ruleset уже после очистки, если их не снять.
#
# `--plain` обязателен, а разбор идёт по ЛЮБОМУ полю строки, а не по первому.
# У юнита в состоянии `failed` systemctl печатает первой колонкой маркер `●`,
# поэтому прежний `awk '{print $1}' | grep -E '^hy2xs-fw-rollback-'` отбрасывал
# строку целиком — и purge не снимал именно аварийно сработавший guard, тот
# единственный, ради которого эта функция написана.
rollback_guard_units() {
systemctl list-units --all --no-legend 'hy2xs-fw-rollback-*.timer' 'hy2xs-fw-rollback-*.service' 2>/dev/null \
| awk '{print $1}' \
| grep -E '^hy2xs-fw-rollback-' || true
systemctl list-units --all --plain --no-legend 'hy2xs-fw-rollback-*.timer' 'hy2xs-fw-rollback-*.service' 2>/dev/null \
| tr -s '[:space:]' '\n' \
| grep -E '^hy2xs-fw-rollback-.+\.(timer|service)$' \
| sort -u || true
}
show_plan() {