защита: в главную ветку доставлены пять починок сторожа боевой базы — новые рабочие папки больше не рождаются дырявыми
По прямому слову владельца 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:
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user