защита: в главную ветку доставлены пять починок сторожа боевой базы — новые рабочие папки больше не рождаются дырявыми

По прямому слову владельца 07.08.2026 («выкати в мэйн, только всё проверь, чтоб никого не потёр»).

🔴 ЗАЧЕМ. Замерено смено́й 33: тело сторожа `172931cf` лежало в 13 рабочих папках из 23,
включая ЭТУ ветку. В нём не было НИ ОДНОЙ починки от 05.08 и 07.08. Хук подключён в настройках
ОТНОСИТЕЛЬНЫМ путём, значит исполняется тело, лежащее рядом с папкой сессии, — и всякая
НОВАЯ рабочая папка, отведённая от главной ветки, рождалась с дырявым сторожем.

🔴 ЧТО ПРОПУСКАЛО СТАРОЕ ТЕЛО (замерено живым вызовом, одним прибором по обоим файлам):
  · php artisan db:wipe --force --database=liderra
  · php artisan migrate:fresh --force
  · dropdb "$PROD_DB"                       (цель спрятана в переменной)
  · psql "$PROD_URL" -c "DROP SCHEMA public CASCADE"
Четыре красных из семи образцов. Контрольный образец (снос кластера) старое тело
останавливало — значит замер работал и не красил всё подряд.

🟢 ЧТО СТАЛО. Тот же замер по этому файлу после доставки: 0 красных из 7. Обе мирные
команды (обычная миграция `php artisan migrate`, снос тестовой базы `dropdb liderra_testing`)
по-прежнему проходят.

ЧТО ИМЕННО ДОБАВЛЕНО В СТОРОЖА:
  4. штатная программа dropdb (стоит на этой машине)
  5. SQL DROP SCHEMA … CASCADE — базу оставляет, содержимое выносит
  6. снос средствами приложения: db:wipe / migrate:fresh / migrate:refresh
  + короткие имена облака (yc mdb pg / yc mdb postgresql)
  + цель, спрятанная в переменной, обязана быть названа словами
🔴 Цена строгости, названная честно: снос ТЕСТОВОЙ базы по связи из переменной теперь тоже
останавливается — сторож не может знать, что под переменной тестовая. Обе двери владельца
(маркер PROD-DESTROY-OK и ALLOW_PROD_DB_DESTROY=1) работают.

Указатель `prod-db-pointer.mjs` доставлен тем же коммитом: он рассказывает сессии, что ловит
сторож, и в старом виде ВРАЛ — называл 5 приёмов из 6 и прямо обещал, что снос со спрятанной
целью пройдёт.

🔴 ЧТО ПРОВЕРЕНО, ЧТОБЫ НИКОГО НЕ ЗАТЕРЕТЬ:
 1. различия обоих файлов просмотрены построчно: из прежних тел не пропало НИ ОДНОЙ мысли —
    все прежние строки либо сохранены, либо заменены своими же расширенными видами;
 2. в этой папке идёт ЧУЖАЯ незакоммиченная работа по СМС (правка MtsSmsProvider.php,
    новые SmsConnectionBreaker.php и MtsSmsProviderConnectionTest.php, ПРОМТ-следующей-смене.md).
    Она НЕ тронута и в этот коммит НЕ входит: добавлены строго два файла поимённо;
 3. 🔴 на сервер GitHub НИЧЕГО не отправлено. Замерено: тамошний main разошёлся с местным
    очень сильно — местный впереди на 3572 коммита, серверный впереди на 1525, последний
    серверный коммит от 03.06.2026. Любая отправка туда либо будет отвергнута, либо снесёт
    полторы тысячи чужих коммитов. Это отдельное решение владельца, а не часть этой работы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-08-07 15:54:24 +03:00
parent 7bd9c67053
commit 3699ece6e3
2 changed files with 119 additions and 16 deletions
+93 -12
View File
@@ -10,6 +10,34 @@
// Боевая база = Managed PG кластер c9q2cvtjpq3hgq6l0r96 (rw-endpoint *.mdb.yandexcloud.net).
// Тест-база = отдельная liderra_testing (её сносить можно).
//
// Что считается сносом (проверено живым вызовом, сторож проверок — tools/prod-db-guard.test.mjs):
// 1. yc managed-postgresql cluster delete — и короткие имена yc mdb pg / yc mdb postgresql
// 2. yc managed-postgresql database delete — и те же короткие имена
// 3. SQL DROP DATABASE
// 4. штатная программа dropdb (стоит на этой машине: C:\Program Files\PostgreSQL\16\bin\dropdb.exe)
// 5. SQL DROP SCHEMA … CASCADE — базу оставляет, всё содержимое выносит
// 6. снос средствами самого приложения: artisan db:wipe / migrate:fresh / migrate:refresh
// Пункты 4-5 и короткие имена добавлены 05.08.2026: до этого они проходили насквозь.
// Пункт 6 добавлен 07.08.2026 (смена 32, по слову владельца) — см. ниже.
//
// 🟢 ДВЕ ДЫРЫ ИЗ ЗАМЕРА М-400 ЗАКРЫТЫ 07.08.2026. Прежде здесь стояло примечание
// «чего сторож не видит», и оно было верным: обе дыры замерены живым вызовом смены 23.
//
// 🔴 ПЕРВАЯ БЫЛА ДВОЙНОЙ. `php artisan db:wipe --force` не числился разрушительной командой
// ВООБЩЕ — хотя выносит из базы все таблицы разом. Значит даже с прямо названной боевой
// базой этот способ сноса сторожу был неизвестен. Туда же `migrate:fresh` и `migrate:refresh`:
// оба сперва сносят, и только потом наливают заново.
//
// 🔴 ВТОРАЯ: снос виден словами, а цель спрятана — ни имени базы, ни кластера, ни хоста
// в тексте нет, всё в переменной (`psql "$PROD_URL" -c "DROP SCHEMA public CASCADE"`).
// 🔑 Отличить по тексту боевую связь от тестовой НЕЛЬЗЯ — значит решать надо не «какая это
// база», а «названа ли она вообще». Разрушительная команда со спрятанной целью теперь
// останавливается: назови цель словами либо поставь маркер владельца.
//
// 🔴 ЦЕНА ЭТОЙ СТРОГОСТИ, НАЗВАННАЯ ЧЕСТНО: снос ТЕСТОВОЙ базы по связи из переменной
// тоже остановится — сторож не может знать, что под переменной тестовая. Это плата за то,
// что боевую нельзя снести молча. Обе двери владельца работают и здесь.
//
// Override владельца: маркер `PROD-DESTROY-OK` в самой команде ИЛИ env ALLOW_PROD_DB_DESTROY=1.
import { readFileSync } from 'node:fs';
@@ -39,21 +67,74 @@ const targetsProd =
/\bc-[a-z0-9]+\.(rw|ro)\.mdb\.yandexcloud\.net/i.test(cmd) ||
/\bliderra\b(?!_)/i.test(cmd);
// Имя команды PostgreSQL у `yc` пишется ТРЕМЯ способами: полным именем и двумя
// короткими алиасами. Замерено живым вызовом 05.08.2026: сторож знал только полное,
// и `yc mdb pg cluster delete --id c9q2cvtjpq3hgq6l0r96` проходил насквозь.
const YC_PG = String.raw`(?:managed-postgresql|mdb\s+(?:postgresql|postgres|pg))`;
// Деструктив над управляемой БД/кластером.
const clusterDelete = /managed-postgresql\s+cluster\s+delete/i.test(cmd); // снос кластера — всегда катастрофа
const databaseDelete = /managed-postgresql\s+database\s+delete/i.test(cmd); // снос управляемой БД
const dropDatabase = /\bdrop\s+database\b/i.test(cmd); // SQL DROP DATABASE
const clusterDelete = new RegExp(`${YC_PG}\\s+cluster\\s+delete`, 'i').test(cmd); // снос кластера — всегда катастрофа
const databaseDelete = new RegExp(`${YC_PG}\\s+database\\s+delete`, 'i').test(cmd); // снос управляемой БД
const dropDatabase = /\bdrop\s+database\b/i.test(cmd); // SQL DROP DATABASE
const destructive = clusterDelete || databaseDelete || dropDatabase;
// Штатная программа PostgreSQL `dropdb` — сносит базу так же насмерть, как SQL DROP DATABASE,
// и она СТОИТ на этой машине (C:\Program Files\PostgreSQL\16\bin\dropdb.exe). Ловим имя
// программы в любом виде: голым словом, с расширением, полным путём (слэши любые, кавычки).
// 🪤 Границы руками, а не `\b`: `\b` не отличает `dropdb` от `--dropdb`, а путь `bin\dropdb.exe`
// должен попадаться. Сноса ТЕСТОВОЙ базы это не касается — ниже решает targetsProd.
const dropdbTool = /(?<![\w-])dropdb(?:\.exe)?(?![\w-])/i.test(cmd);
// Снос кластера блокируем всегда; остальное — только если цель = прод.
if (destructive && (clusterDelete || targetsProd)) {
const reason =
'ЗАБЛОКИРОВАНО (prod-db-guard): попытка удалить/пересоздать БОЕВУЮ базу/кластер Лидерры. ' +
'Это снесёт портал (инцидент 26.06.2026). Боевая база = Managed PG кластер ' + PROD_CLUSTER + '. ' +
'Для тестов используй ОТДЕЛЬНУЮ базу liderra_testing, не прод. ' +
'Если это осознанное действие ВЛАДЕЛЬЦА — добавь в команду маркер PROD-DESTROY-OK ' +
'или запусти с env ALLOW_PROD_DB_DESTROY=1.';
// `DROP SCHEMA public CASCADE` не удаляет базу, но выносит из неё ВСЁ — таблицы, данные,
// RLS-политики. Для боевой это тот же снос портала, только другим словом.
const dropSchema = /\bdrop\s+schema\b/i.test(cmd);
// 🔴 СНОС СРЕДСТВАМИ САМОГО ПРИЛОЖЕНИЯ (дыра 1 из М-400). `db:wipe` выносит из базы все
// таблицы разом, `migrate:fresh` и `migrate:refresh` — сперва сносят, потом наливают заново.
// Для боевой это тот же лежащий портал, только другим словом.
// 🪤 Голый `php artisan migrate` сюда НЕ попадает нарочно: он ничего не сносит, и его
// гоняют каждый день. Ловится ровно двоеточие с опасным словом после него.
const artisanWipe = /\b(?:db:wipe|migrate:fresh|migrate:refresh)\b/i.test(cmd);
const destructive = clusterDelete || databaseDelete || dropDatabase || dropdbTool || dropSchema || artisanWipe;
// 🔴 ЦЕЛЬ СПРЯТАНА (дыра 2 из М-400): разрушительная команда налицо, а куда она бьёт —
// не написано нигде, всё в переменной оболочки или в окружении. По тексту такое неотличимо
// от работы с тестовой базой, поэтому решаем не «какая база», а «названа ли она вообще».
// Ловим подстановку любой из трёх оболочек и имена, которыми связь задают молча.
// 🪤 Сама по себе переменная ничего не значит — строгость включается ТОЛЬКО вместе
// с разрушительной командой (см. условие ниже), иначе сторож встал бы поперёк всей работы.
const celSpryatana =
/\$\{?\w+\}?/.test(cmd) || // $FOO, ${FOO}, $env:FOO
/%\w+%/.test(cmd) || // %FOO% — cmd.exe
/\b(?:PGDATABASE|PGSERVICE|DATABASE_URL|PGPASSFILE)\b/i.test(cmd) ||
/\.pgpass\b/i.test(cmd);
// Снос кластера блокируем всегда; остальное — если цель = прод ЛИБО цель спрятана.
if (destructive && (clusterDelete || targetsProd || celSpryatana)) {
const chto =
clusterDelete ? 'снос кластера (yc … cluster delete)'
: databaseDelete ? 'снос управляемой БД (yc … database delete)'
: dropDatabase ? 'SQL DROP DATABASE'
: dropdbTool ? 'штатная программа dropdb'
: artisanWipe ? 'снос средствами приложения (artisan db:wipe / migrate:fresh)'
: 'SQL DROP SCHEMA (выносит из базы всё содержимое)';
// 🔑 Отказ по спрятанной цели — отдельный разговор. Сказать «ты трогаешь боевую базу»
// смене, у которой боевой базы в команде нет и близко, значит сбить её с толку.
// Надо назвать ровно то, чего не хватило: имени цели.
const poSpryatannoyCeli = !clusterDelete && !targetsProd && celSpryatana;
const reason = poSpryatannoyCeli
? 'ЗАБЛОКИРОВАНО (prod-db-guard): разрушительная команда, у которой ЦЕЛЬ НЕ НАЗВАНА. ' +
'Что распознано: ' + chto + '. Куда она бьёт — в тексте не написано: база спрятана ' +
'в переменной или в окружении, а по тексту боевая неотличима от тестовой. ' +
'Назови базу словами (например `dropdb liderra_testing`) — и сторож пропустит. ' +
'Если цель и вправду боевая и это осознанное действие ВЛАДЕЛЬЦА — добавь в команду ' +
'маркер PROD-DESTROY-OK или запусти с env ALLOW_PROD_DB_DESTROY=1.'
: 'ЗАБЛОКИРОВАНО (prod-db-guard): попытка удалить/пересоздать БОЕВУЮ базу/кластер Лидерры. ' +
'Что распознано: ' + chto + '. ' +
'Это снесёт портал (инцидент 26.06.2026). Боевая база = Managed PG кластер ' + PROD_CLUSTER + '. ' +
'Для тестов используй ОТДЕЛЬНУЮ базу liderra_testing, не прод. ' +
'Если это осознанное действие ВЛАДЕЛЬЦА — добавь в команду маркер PROD-DESTROY-OK ' +
'или запусти с env ALLOW_PROD_DB_DESTROY=1.';
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
+26 -4
View File
@@ -19,10 +19,32 @@ const context = [
' НЕ путать с живой, НЕ менять там данные. `sudo -u postgres psql` на VM = старая копия.',
'- Для тестов — ОТДЕЛЬНАЯ база liderra_testing (через php artisan migrate),',
' НИКОГДА не прод `liderra`.',
'- НИКОГДА не удалять/пересоздавать боевую базу/кластер',
' (yc managed-postgresql database/cluster delete, DROP DATABASE liderra) —',
' это снесёт портал (инцидент 26.06, см. db/CHANGELOG_schema.md v8.57).',
' Хук prod-db-guard это блокирует; осознанный снос владельцем — маркер PROD-DESTROY-OK.',
'- НИКОГДА не удалять/пересоздавать боевую базу/кластер — это снесёт портал',
' (инцидент 26.06, см. db/CHANGELOG_schema.md v8.57).',
'',
'ЧТО ИМЕННО ЛОВИТ ХУК prod-db-guard (проверено живым вызовом, сторожа —',
'tools/prod-db-guard.test.mjs и tools/night/ukazatel-ne-otstayot-ot-storozha.test.mjs;',
'пункты 4-5 и короткие имена закрыты 05.08.2026, пункт 6 — 07.08.2026):',
' 1. yc managed-postgresql cluster delete — и короткие имена yc mdb pg / yc mdb postgresql',
' 2. yc managed-postgresql database delete — и те же короткие имена',
' 3. SQL DROP DATABASE',
' 4. штатную программу dropdb (она стоит на этой машине:',
' C:\\Program Files\\PostgreSQL\\16\\bin\\dropdb.exe)',
' 5. SQL DROP SCHEMA … CASCADE — базу оставляет, но выносит из неё ВСЁ',
' 6. снос средствами самого приложения: artisan db:wipe / migrate:fresh / migrate:refresh',
' (голый `php artisan migrate` НЕ трогается — он ничего не сносит)',
'Пункты 2-6 срабатывают, когда целью назван ПРОД (кластер c9q2cvtjpq3hgq6l0r96,',
'rw/ro-хост *.mdb.yandexcloud.net или база `liderra` отдельным словом) ЛИБО когда цель',
'не названа вовсе — см. ниже. Чтение и работу с `liderra_testing` хук не трогает.',
'Снос кластера блокируется всегда.',
'Осознанный снос владельцем — маркер PROD-DESTROY-OK в команде (или ALLOW_PROD_DB_DESTROY=1).',
'',
'🔴 ЦЕЛЬ ОБЯЗАНА БЫТЬ НАЗВАНА СЛОВАМИ (закрыто 07.08.2026). Разрушительная команда, у которой',
'база спрятана (строка подключения из окружения, PGDATABASE, .pgpass, напр.',
'psql "$PROD_URL" -c "DROP SCHEMA public CASCADE"), ОСТАНАВЛИВАЕТСЯ: хук читает текст',
'команды, а не связь, и отличить там боевую базу от тестовой нечем. Пиши цель словами',
'(`dropdb liderra_testing`) — и хук пропустит. 🔴 Цена, названная честно: снос ТЕСТОВОЙ',
'базы по связи из переменной тоже остановится. Это плата за то, что боевую нельзя снести молча.',
].join('\n');
process.stdout.write(JSON.stringify({