@rt-tools/agent-kit 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +13 -0
  2. package/assets/agents/rules-reviewer.md +83 -0
  3. package/assets/checks/board.github.mjs +48 -1
  4. package/assets/checks/check-board.github.mjs +84 -1
  5. package/assets/checks/check-dupes.mjs +66 -6
  6. package/assets/checks/check-lib-layers.mjs +13 -524
  7. package/assets/checks/check-specs.mjs +61 -747
  8. package/assets/checks/check-styles.mjs +185 -15
  9. package/assets/checks/lib-boundaries.mjs +143 -0
  10. package/assets/checks/lib-common.mjs +149 -0
  11. package/assets/checks/lib-domains.mjs +205 -0
  12. package/assets/checks/lib-manifests.mjs +60 -0
  13. package/assets/checks/lib-reexports.mjs +101 -0
  14. package/assets/checks/rt-kit-checks.config.mjs +26 -0
  15. package/assets/checks/spec-anchors.mjs +297 -0
  16. package/assets/checks/spec-common.mjs +222 -0
  17. package/assets/checks/spec-contract.mjs +152 -0
  18. package/assets/checks/spec-scenarios.mjs +201 -0
  19. package/assets/commands/feedback.md +95 -0
  20. package/assets/commands/rules-review.md +98 -0
  21. package/assets/commands/skill-curator.md +39 -22
  22. package/assets/defaults/project.sh +8 -0
  23. package/assets/docs/GLOSSARY.md +21 -20
  24. package/assets/hooks/git-guard-push-tests.sh +8 -4
  25. package/assets/hooks/reuse-first-guard.sh +16 -2
  26. package/assets/hooks/skill-gate.sh +1 -1
  27. package/assets/hooks/sql-guard-parse.sh +187 -0
  28. package/assets/hooks/sql-guard-request.sh +117 -0
  29. package/assets/hooks/sql-guard-target.sh +134 -0
  30. package/assets/hooks/sql-guard-write.sh +212 -0
  31. package/assets/hooks/sql-guard.sh +26 -596
  32. package/assets/hooks/task-flow-guard.sh +16 -2
  33. package/assets/hooks/waiting-turn-guard.sh +116 -0
  34. package/assets/laws/delivery.md +35 -0
  35. package/assets/laws/project-documentation.md +18 -0
  36. package/assets/laws/work-conduct.md +25 -0
  37. package/assets/patterns/admin-lists-screen.md +25 -14
  38. package/assets/patterns/admin-nav-item.md +1 -1
  39. package/assets/patterns/component-structure-new.md +1 -1
  40. package/assets/patterns/entity-aside.md +4 -2
  41. package/assets/patterns/git-workflow-commit.azure.md +74 -2
  42. package/assets/patterns/git-workflow-commit.github.md +75 -2
  43. package/assets/patterns/git-workflow-commit.gitlab.md +75 -4
  44. package/assets/patterns/git-workflow-docker.md +30 -0
  45. package/assets/patterns/observability-record.md +9 -0
  46. package/assets/patterns/shared-code-new.md +2 -2
  47. package/assets/patterns/task-flow-close.md +160 -47
  48. package/assets/patterns/task-flow-handoff.md +1 -1
  49. package/assets/patterns/task-flow-resume.md +3 -3
  50. package/assets/patterns/task-flow-start.md +32 -5
  51. package/assets/rules/angular-patterns.md +22 -0
  52. package/assets/rules/api-layer.md +25 -0
  53. package/assets/rules/browser-verification.md +32 -0
  54. package/assets/rules/component-structure.md +21 -0
  55. package/assets/rules/dependencies.md +22 -0
  56. package/assets/rules/doc-style.md +24 -0
  57. package/assets/rules/entity-conventions.needs-admin.md +21 -0
  58. package/assets/rules/entity-models.md +21 -0
  59. package/assets/rules/git-workflow.azure.md +57 -1
  60. package/assets/rules/git-workflow.github.md +89 -2
  61. package/assets/rules/git-workflow.gitlab.md +56 -1
  62. package/assets/rules/lib-layers.md +29 -0
  63. package/assets/rules/lists.md +37 -10
  64. package/assets/rules/navigation.md +21 -0
  65. package/assets/rules/observability.needs-app.md +23 -0
  66. package/assets/rules/permissions.md +23 -0
  67. package/assets/rules/platform-access.md +21 -0
  68. package/assets/rules/reuse-first.md +20 -0
  69. package/assets/rules/seo.md +19 -0
  70. package/assets/rules/shared-code.md +20 -1
  71. package/assets/rules/spec-driven.md +32 -0
  72. package/assets/rules/styling-bem.md +19 -0
  73. package/assets/rules/task-flow.md +148 -18
  74. package/assets/rules/testing.md +62 -0
  75. package/assets/rules/translations.md +21 -0
  76. package/assets/rules/typescript-conventions.md +28 -0
  77. package/assets/samples/specs/_template/spec.md +83 -0
  78. package/assets/samples/tasks/_template/grill.md +28 -0
  79. package/assets/samples/tasks/_template/plan.md +39 -0
  80. package/assets/samples/tasks/_template/progress.md +23 -0
  81. package/assets/skills/agent-kit.md +52 -2
  82. package/assets/templates/proposal.md +21 -0
  83. package/assets/templates/rule.md +31 -2
  84. package/bin/agent-kit.d.ts.map +1 -1
  85. package/bin/agent-kit.js +115 -87
  86. package/bin/agent-kit.js.map +1 -1
  87. package/index.d.ts +1 -0
  88. package/index.d.ts.map +1 -1
  89. package/index.js +1 -0
  90. package/index.js.map +1 -1
  91. package/lib/argv.d.ts.map +1 -1
  92. package/lib/argv.js +6 -4
  93. package/lib/argv.js.map +1 -1
  94. package/lib/assets.d.ts.map +1 -1
  95. package/lib/assets.js +2 -1
  96. package/lib/assets.js.map +1 -1
  97. package/lib/cargo.d.ts +20 -0
  98. package/lib/cargo.d.ts.map +1 -1
  99. package/lib/cargo.js.map +1 -1
  100. package/lib/cascade.d.ts +55 -0
  101. package/lib/cascade.d.ts.map +1 -0
  102. package/lib/cascade.js +131 -0
  103. package/lib/cascade.js.map +1 -0
  104. package/lib/catalog.d.ts +0 -75
  105. package/lib/catalog.d.ts.map +1 -1
  106. package/lib/catalog.js +44 -127
  107. package/lib/catalog.js.map +1 -1
  108. package/lib/commands.d.ts.map +1 -1
  109. package/lib/commands.js +153 -85
  110. package/lib/commands.js.map +1 -1
  111. package/lib/companion.d.ts.map +1 -1
  112. package/lib/companion.js +5 -5
  113. package/lib/companion.js.map +1 -1
  114. package/lib/config.d.ts +12 -1
  115. package/lib/config.d.ts.map +1 -1
  116. package/lib/config.js +11 -5
  117. package/lib/config.js.map +1 -1
  118. package/lib/enroll.d.ts +56 -0
  119. package/lib/enroll.d.ts.map +1 -0
  120. package/lib/enroll.js +123 -0
  121. package/lib/enroll.js.map +1 -0
  122. package/lib/freshness.d.ts.map +1 -1
  123. package/lib/freshness.js +31 -17
  124. package/lib/freshness.js.map +1 -1
  125. package/lib/hooks-map.d.ts +30 -0
  126. package/lib/hooks-map.d.ts.map +1 -1
  127. package/lib/hooks-map.js +80 -18
  128. package/lib/hooks-map.js.map +1 -1
  129. package/lib/integrity.d.ts +1 -2
  130. package/lib/integrity.d.ts.map +1 -1
  131. package/lib/integrity.js +0 -1
  132. package/lib/integrity.js.map +1 -1
  133. package/lib/observations.d.ts.map +1 -1
  134. package/lib/observations.js +25 -12
  135. package/lib/observations.js.map +1 -1
  136. package/lib/order.d.ts +10 -0
  137. package/lib/order.d.ts.map +1 -0
  138. package/lib/order.js +14 -0
  139. package/lib/order.js.map +1 -0
  140. package/lib/picker.d.ts.map +1 -1
  141. package/lib/picker.js +8 -2
  142. package/lib/picker.js.map +1 -1
  143. package/lib/plan.js +1 -1
  144. package/lib/plan.js.map +1 -1
  145. package/lib/proposals.d.ts.map +1 -1
  146. package/lib/proposals.js +25 -8
  147. package/lib/proposals.js.map +1 -1
  148. package/lib/sections.js +1 -1
  149. package/lib/sections.js.map +1 -1
  150. package/lib/ship.d.ts.map +1 -1
  151. package/lib/ship.js +9 -1
  152. package/lib/ship.js.map +1 -1
  153. package/lib/shipment.d.ts.map +1 -1
  154. package/lib/shipment.js +14 -10
  155. package/lib/shipment.js.map +1 -1
  156. package/lib/snapshot.d.ts.map +1 -1
  157. package/lib/snapshot.js +2 -1
  158. package/lib/snapshot.js.map +1 -1
  159. package/lib/stamp.js +1 -1
  160. package/lib/stamp.js.map +1 -1
  161. package/lib/sync.d.ts +12 -2
  162. package/lib/sync.d.ts.map +1 -1
  163. package/lib/sync.js +11 -10
  164. package/lib/sync.js.map +1 -1
  165. package/lib/vars.d.ts.map +1 -1
  166. package/lib/vars.js +2 -3
  167. package/lib/vars.js.map +1 -1
  168. package/package.json +1 -1
  169. package/rt-tools-agent-kit-0.9.0.tgz +0 -0
  170. package/rt-tools-agent-kit-0.8.2.tgz +0 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|mcp__webstorm__create_new_file
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool
3
3
  # Требует: hooks/profile-check.sh
4
4
  # Гард «ничего не пишется с нуля». PreToolUse на правке кода и разметки.
5
5
  #
@@ -47,9 +47,23 @@ case "$tool" in
47
47
  # сменой не инструмента, а способа записи; текстом правки тогда служит сама команда, и
48
48
  # заведённое ею в heredoc читается наравне с телом правки. Разбор —
49
49
  # `2026-08-15-guard-denied-shell-wrote-anyway.md`.
50
- Bash)
50
+ #
51
+ # Терминал среды исполняет ту же командную строку и кладёт её в то же поле: без этих двух
52
+ # имён гард стоял бы объявленным на них и молча пропускал — состояние хуже необъявленного,
53
+ # потому что снаружи выглядит закрытым.
54
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
51
55
  shell_cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
52
56
  [ -z "$shell_cmd" ] && exit 0
57
+ # Универсальный исполнитель прячет настоящую команду во вложенной строке: без её разбора
58
+ # путь стоит за кавычкой, и до него не дотягивается ни один образец.
59
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
60
+ inner="$(printf '%s' "$shell_cmd" | perl -0ne '
61
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
62
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
63
+ }
64
+ ' 2>/dev/null)"
65
+ [ -n "$inner" ] && shell_cmd="$inner"
66
+ fi
53
67
  ;;
54
68
  *) exit 0 ;;
55
69
  esac
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash
2
+ # rt-hook: PreToolUse Edit|Write|MultiEdit|Bash|mcp__webstorm__create_new_file|mcp__webstorm__execute_terminal_command|mcp__webstorm__execute_tool|mcp__claude-in-chrome__.*
3
3
  # Гейт правил: не даёт править файл, пока не загружено правило, под которое он подпадает.
4
4
  #
5
5
  # Закон и правило, которых никто не открывает, не действуют. Напоминание в подсказке помогает
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # Разбор команды для гарда хранилища: приведение к одному виду, нарезка на сегменты и ответ
3
+ # на вопрос, доставляет ли сегмент SQL до сервера.
4
+ #
5
+ # Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
6
+ # помощник рядом хуком не регистрируется и в одиночку ничего не решает.
7
+
8
+ # Дальше разбираем без учёта регистра и переводов строк: `delete\n from bookings`
9
+ # — тот же запрос, что и в одну строку.
10
+ #
11
+ # Перевод строки при этом превращается в `;`, а не в пробел: в командной строке он РАЗДЕЛЯЕТ
12
+ # вызовы ровно как `;`, и склейка его в пробел стирала границу между ними. Многострочная
13
+ # команда становилась одним сегментом, доказательство чтения из первой строки покрывало
14
+ # неразобранный вызов из второй, и запись на боевую базу проходила молча — тот же обход,
15
+ # что чинили для `&&`, только набранный с новой строки.
16
+ normalize() {
17
+ perl -0ne '
18
+ # Продолжение длинной команды обратным слешем — это одна строка, а не две: без
19
+ # склейки адрес из первой уезжал в соседний сегмент, где клиента уже нет.
20
+ s/\\\n/ /g;
21
+ # Перевод строки заменяется на `;` ТОЛЬКО вне кавычек. Внутри `-c "…"` он часть
22
+ # запроса: многострочный SELECT резался на сегменты, и штатное чтение боевой базы
23
+ # переставало быть доказанным.
24
+ my ($out, $quote, $esc) = ("", "", 0);
25
+ for my $ch (split //, $_) {
26
+ if ($esc) { $out .= $ch; $esc = 0; next; }
27
+ if ($ch eq "\\") { $out .= $ch; $esc = 1; next; }
28
+ if ($quote ne "") {
29
+ $out .= ($ch eq "\n" ? " " : $ch);
30
+ $quote = "" if $ch eq $quote;
31
+ } elsif ($ch eq q{"} || $ch eq q{'"'"'}) {
32
+ $quote = $ch; $out .= $ch;
33
+ } elsif ($ch eq "\n" || $ch eq ";") {
34
+ $out .= "\x01"; # граница независимых команд
35
+ } elsif ($ch eq "|") {
36
+ $out .= "\x02"; # граница звена конвейера
37
+ } elsif ($ch eq "&") {
38
+ $out .= "\x01";
39
+ } else {
40
+ $out .= $ch;
41
+ }
42
+ }
43
+ # Соседние маркеры схлопываются: `&&` и `||` дают по два подряд.
44
+ $out =~ s/\x01+/\x01/g;
45
+ $out =~ s/\x02\x01/\x01/g;
46
+ $out =~ s/\x01\x02/\x01/g;
47
+ # Незакрытая кавычка означает, что автомат разъехался: апостроф в комментарии
48
+ # (`# don'"'"'t forget`) оставлял состояние «внутри строки» до конца ввода, и все
49
+ # последующие переводы строк переставали разделять вызовы — сегментация выключалась
50
+ # одним символом. В таком случае честнее ничего не печатать: вызывающий разделит
51
+ # строки грубым способом, и лишнее дробление сыграет в пользу строгости.
52
+ print $out if $quote eq "";
53
+ ' 2>/dev/null
54
+ }
55
+
56
+ # Грубое разделение — запасной путь: все переводы строк становятся разделителями. Оно строже
57
+ # точного (может разрезать многострочный запрос), поэтому годится и как фолбэк без perl,
58
+ # и как ответ на неразобранную команду.
59
+ sql_flatten() {
60
+ flat_rough="$(printf '%s' "$sql" | perl -0pe 's/\\\n/ /g' 2>/dev/null | tr '\n;&' '\001\001\001' | tr '|' '\002' | tr '\t' ' ' | tr '[:upper:]' '[:lower:]')"
61
+ [ -z "$flat_rough" ] && flat_rough="$(printf '%s' "$sql" | tr '\n;&' '\001\001\001' | tr '|' '\002' | tr '\t' ' ' | tr '[:upper:]' '[:lower:]')"
62
+
63
+ if command -v perl >/dev/null 2>&1; then
64
+ flat="$(printf '%s' "$sql" | normalize | tr '\t' ' ' | tr '[:upper:]' '[:lower:]')"
65
+ [ -z "$flat" ] && flat="$flat_rough"
66
+ else
67
+ flat="$flat_rough"
68
+ fi
69
+ }
70
+
71
+ # Разбор идёт по сегментам, а не по всей строке: `ls -l /opt && … psql -c "\copy …"` целиком
72
+ # выглядел как чтение, потому что `-l` от `ls` засчитывался за `psql -l`.
73
+ #
74
+ # Нарезка живёт в ЕДИНСТВЕННОМ месте. Пока наборов разделителей было два — один в раннем слое,
75
+ # другой здесь, — расхождение само по себе оказалось дырой: одиночный `|` считался границей
76
+ # тут и не считался там, и доставка файла конвейером в клиент проходила как чтение.
77
+ # Команда — то, что стоит между `;`, `&&`, `||` и переводами строк. Конвейер границей НЕ
78
+ # является: `cat fix.sql | psql …` — не две независимые команды, а одна доставка SQL, где
79
+ # левое звено питает правое. Пока `|` резал команды, глагол из `echo "drop table …"` терялся
80
+ # вместе с отброшенным звеном, и запись проходила молча.
81
+ split_segments() {
82
+ # Перевод строки в конце обязателен: без него `read` не отдаёт последнюю строку в тело
83
+ # цикла, и команда из одного сегмента молча выпадала из разбора целиком.
84
+ printf '%s\n' "$1" | tr '\001' '\n'
85
+ }
86
+
87
+ # Звенья конвейера внутри одной команды.
88
+ split_pipeline() {
89
+ printf '%s' "$1" | tr '\002' '\n'
90
+ }
91
+
92
+ # Команда считается вызовом клиента, только если клиент стоит ГОЛОВОЙ хотя бы одного звена.
93
+ # Имя в аргументе (`grep -rn "prisma db push" docs/`) вызовом не является.
94
+ calls_client() {
95
+ _found=""
96
+ _links="$(printf '%s' "$1" | tr '\002' '\n')"
97
+ while IFS= read -r link; do
98
+ [ -z "$link" ] && continue
99
+ # Голова звена: пропускаем присваивания окружения и обёртки вроде sudo/time/xargs.
100
+ head="$(printf '%s' "$link" | sed -E 's/^[[:space:]]*//; s/^([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*//; s/^(sudo|time|env|nice|xargs)[[:space:]]+//')"
101
+ case "$head" in
102
+ psql*|pg_restore*|pg_dump*|pg_dumpall*|prisma*|*/psql*|*/pg_restore*|*/pg_dump*)
103
+ _found="yes"; break ;;
104
+ esac
105
+ # Транспорт до машины: сам вызов стоит внутри строки, которую он исполняет.
106
+ case "$head" in
107
+ ssh\ *|scp\ *|docker\ *|*/ssh\ *|*/docker\ *)
108
+ printf '%s' "$link" | grep -qE '(^|[^[:alnum:]_.-])(psql|pg_restore|pg_dump|prisma)([^[:alnum:]_.-]|$)' \
109
+ && { _found="yes"; break; } ;;
110
+ esac
111
+ # Раннер пакетов перед клиентом: `npx prisma migrate deploy`.
112
+ case "$head" in
113
+ npx\ *|pnpm\ *|yarn\ *|bun\ *|npm\ *)
114
+ printf '%s' "$head" | grep -qE '^(npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+(psql|pg_restore|pg_dump|prisma)([[:space:]]|$)' \
115
+ && { _found="yes"; break; } ;;
116
+ esac
117
+ done <<CALLS_EOF
118
+ $_links
119
+ CALLS_EOF
120
+ [ -n "$_found" ]
121
+ }
122
+
123
+ # Голова сегмента — читающий инструмент: имя клиента стоит в его аргументе, то есть это поиск
124
+ # или просмотр файла, а не доставка SQL. `grep -rn "prisma db push" docs/` читает документацию.
125
+ # Решает именно ГОЛОВА: тот же приём применён в dev-server-guard якорем BOUND.
126
+ is_reader() {
127
+ printf '%s' "$1" | grep -qE '^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*((sudo|time|env|nice|xargs)[[:space:]]+)*([^[:space:]]*/)?(grep|rg|ag|ack|find|awk|sed|cat|less|more|head|tail|wc|echo|printf|jq|diff|comm|sort|uniq|column)([[:space:]]|$)'
128
+ }
129
+
130
+ # Сегменты с настоящим вызовом клиента: читающие головы отсеиваются здесь же, а не отдельным
131
+ # слоем с собственным выходом.
132
+ client_segments() {
133
+ split_segments "$flat" | while IFS= read -r seg; do
134
+ [ -z "$seg" ] && continue
135
+ calls_client "$seg" || continue
136
+ # Маркер звена больше не нужен: дальше команда разбирается как одна строка.
137
+ printf '%s\n' "$(printf '%s' "$seg" | tr '\002' ' ')"
138
+ done
139
+ }
140
+
141
+ # Адресат берётся ТОЛЬКО из сегментов вызова клиента и только из аргументов, а не из данных.
142
+ # Домен сайта живёт в самих строках — в почте владельца, в canonical и og:url объекта, — а
143
+ # рядом в цепочке его дёргают по HTTP. Пока признак искался по всей команде, `curl
144
+ # https://<домен приложения>/... && psql -p 5432 -c "update …"` отклонялся как запись в
145
+ # боевую базу, и опт-аута у этой ветки нет по замыслу: команда становилась неисполнимой.
146
+ # Текст запроса вырезается ДО нарезки на сегменты. Внутри `-c "…"` свободно живут `;` и `||`
147
+ # — по ним сегменты и режутся, поэтому кусок запроса с доменом в данных становился отдельным
148
+ # сегментом, и почта на домене приложения снова читалась как адрес боевой базы.
149
+ sql_addr_flatten() {
150
+ addr_flat="$flat"
151
+ if command -v perl >/dev/null 2>&1; then
152
+ addr_flat="$(printf '%s' "$flat" | perl -0pe '
153
+ s/(^|[^[:alnum:]])--?(c|command|querytext)(=|\s+)("([^"\\]|\\.)*"|\x27[^\x27]*\x27)/$1/gs
154
+ ' 2>/dev/null || printf '%s' "$flat")"
155
+ fi
156
+ }
157
+
158
+ addr_segments() {
159
+ split_segments "$addr_flat" | while IFS= read -r seg; do
160
+ [ -z "$seg" ] && continue
161
+ flat_seg="$(printf '%s' "$seg" | tr '\002' ' ')"
162
+ # Адрес берётся из команд с вызовом клиента и из транспорта до машины: `ssh root@…`
163
+ # называет боевой хост, а сам вызов стоит дальше по цепочке.
164
+ if calls_client "$seg"; then
165
+ printf '%s\n' "$flat_seg"
166
+ elif printf '%s' "$flat_seg" | grep -qE '(^|[^[:alnum:]_.-])(ssh|scp|docker)([^[:alnum:]_.-]|$)|(pghost|pgport|pgdatabase|pguser|database_url|postgres_url)='; then
167
+ is_reader "$flat_seg" && continue
168
+ printf '%s\n' "$flat_seg"
169
+ fi
170
+ done
171
+ }
172
+
173
+ # Сегменты вызова клиента для дальнейшего разбора. У запроса из редактора сегментов нет вовсе
174
+ # — там весь текст и есть запрос.
175
+ sql_collect_segments() {
176
+ segments="$(client_segments)"
177
+ if [ -z "$segments" ]; then
178
+ if [ -n "$conn" ] || [ "$context" != "команда psql/prisma" ]; then
179
+ segments="$flat"
180
+ else
181
+ # Команда, где имя клиента встретилось только в аргументах читающих инструментов,
182
+ # ничего никуда не доставляет. Выход здесь безопасен: он стоит ПОСЛЕ разбора, а не
183
+ # вместо него, и срабатывает лишь когда настоящего вызова в команде нет.
184
+ exit 0
185
+ fi
186
+ fi
187
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Запрос из ввода гарда хранилища: что именно поедет к серверу и через что.
3
+ #
4
+ # Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
5
+ # помощник рядом хуком не регистрируется и в одиночку ничего не решает.
6
+ #
7
+ # Отсюда выходят четыре значения: `sql` — текст запроса или команда целиком, `conn` —
8
+ # идентификатор подключения редактора, `context` — чем это доставляется, и `cmd` — исходная
9
+ # командная строка, нужная разбору адреса миграции в исходном регистре.
10
+
11
+ # Три вида ввода: запрос через подключение редактора, команда оболочки и та же команда,
12
+ # завёрнутая в универсальный исполнитель. Инструмент, не названный здесь, гарда не касается.
13
+ sql_read_request() {
14
+ sql=""
15
+ context=""
16
+ conn=""
17
+ case "$tool" in
18
+ mcp__webstorm__execute_sql_query)
19
+ sql="$(printf '%s' "$input" | jq -r '.tool_input.queryText // empty' 2>/dev/null)"
20
+ conn="$(printf '%s' "$input" | jq -r '.tool_input.connectionId // empty' 2>/dev/null)"
21
+ context="запрос через подключение редактора"
22
+ ;;
23
+ # Терминал IDE исполняет ту же командную строку и кладёт её в то же поле, что и Bash:
24
+ # без этой ветки весь гард обходился сменой инструмента.
25
+ Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
26
+ cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
27
+ [ -z "$cmd" ] && exit 0
28
+
29
+ # Универсальный исполнитель зовёт ЛЮБОЙ инструмент редактора по имени, в том числе
30
+ # `execute_sql_query`, — и тогда в строке нет ни одного имени клиента, по которому
31
+ # гард себя включает. Сам инструмент закрыт в permissions.deny, но полагаться только
32
+ # на настройку нельзя: её снимут, а гард останется.
33
+ if [ "$tool" = "mcp__webstorm__execute_tool" ] && command -v perl >/dev/null 2>&1; then
34
+ case "$cmd" in
35
+ *execute_sql_query*)
36
+ inner_sql="$(printf '%s' "$cmd" | perl -0ne '
37
+ if (/--queryText(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
38
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
39
+ }
40
+ ' 2>/dev/null)"
41
+ inner_conn="$(printf '%s' "$cmd" | perl -0ne '
42
+ if (/--connectionId(?:=|\s+)(?:"([^"]*)"|\x27([^\x27]*)\x27|(\S+))/) {
43
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
44
+ }
45
+ ' 2>/dev/null)"
46
+ ;;
47
+ esac
48
+ # Терминал, завёрнутый в тот же исполнитель: разбираем настоящую команду.
49
+ if [ -z "$inner_sql" ]; then
50
+ inner_cmd="$(printf '%s' "$cmd" | perl -0ne '
51
+ if (/--command(?:=|\s+)(?:"((?:[^"\\]|\\.)*)"|\x27([^\x27]*)\x27|(.+))/s) {
52
+ print defined $1 ? $1 : (defined $2 ? $2 : $3);
53
+ }
54
+ ' 2>/dev/null)"
55
+ [ -n "$inner_cmd" ] && cmd="$inner_cmd"
56
+ fi
57
+ fi
58
+
59
+ # Завёрнутый запрос разбирается как запрос из редактора, а не как команда оболочки:
60
+ # имени клиента в нём нет, зато есть подключение и текст SQL.
61
+ if [ -n "$inner_sql" ]; then
62
+ sql="$inner_sql"
63
+ conn="$inner_conn"
64
+ context="запрос через подключение редактора (обёртка исполнителя)"
65
+ else
66
+
67
+ # Команда, которая ничего никуда не доставляет, разбору не подлежит — даже если имя
68
+ # клиента стоит в её аргументах. `grep -rn "prisma db push" docs/` читает файлы, а не
69
+ # базу; раньше он получал deny, и обойти это можно было только испортив сам шаблон
70
+ # поиска. Тот же приём уже применён в dev-server-guard: решает ГОЛОВА команды, а не
71
+ # упоминание имени где-то внутри.
72
+ # Интересуют команды, которые доносят SQL до сервера. Клиент ищется КАК СЛОВО в любом
73
+ # месте команды, а не только в начале: удаление на бою выглядит как
74
+ # `ssh root@host "docker compose exec postgres psql -c '…'"` — при проверке одного
75
+ # лишь начала строки оно проходило мимо гарда целиком.
76
+ #
77
+ # Порядок здесь принципиален. Раньше выше стояла отсечка «команда начинается с git/gh»,
78
+ # снимавшая ложные срабатывания на тексте коммита, — и она же открывала обход: в
79
+ # `git log && psql … -c "DELETE …"` первое звено уносило с собой весь остальной SQL.
80
+ # Теперь сперва ищется клиент, и только его отсутствие завершает проверку.
81
+ printf '%s\n' "$cmd" | grep -qE \
82
+ '(^|[^[:alnum:]_.-])(psql|pg_restore|pg_dump|prisma)([^[:alnum:]_.-]|$)' \
83
+ || exit 0
84
+
85
+ # Ложные срабатывания на описаниях снимаются иначе — вырезанием текста сообщений
86
+ # (`-m '…'`, `-am "…"`, `--message="…"`, `-F file`), а не отказом от проверки всей
87
+ # команды. В самом сообщении SQL не исполняется, но слова «delete» и «drop» в нём
88
+ # обычны: `git commit -am "chore(api): update prisma schema"` разбирался как
89
+ # UPDATE без WHERE, потому что шаблон требовал `-m` вплотную к дефису.
90
+ if command -v perl >/dev/null 2>&1; then
91
+ # `file` в альтернации быть не должно: под него попадал `--file=cleanup.sql`
92
+ # у psql, и признак «SQL приехал файлом» умирал раньше, чем его проверяли.
93
+ # Здесь только флаги, которые несут ТЕКСТ сообщения.
94
+ cmd="$(printf '%s' "$cmd" | perl -0pe '
95
+ s/(^|[^[:alnum:]])--?[a-zA-Z]*(m|message|body|title|body-file|F)(=|\s+)("([^"\\]|\\.)*"|\x27[^\x27]*\x27|[^\s;&|]+)/$1/gs
96
+ ' 2>/dev/null || printf '%s' "$cmd")"
97
+ # После вырезания сообщения клиента может уже не остаться — тогда это была
98
+ # git-команда, лишь упоминавшая psql в тексте.
99
+ printf '%s\n' "$cmd" | grep -qE \
100
+ '(^|[^[:alnum:]_.-])(psql|pg_restore|pg_dump|prisma)([^[:alnum:]_.-]|$)' \
101
+ || exit 0
102
+ else
103
+ # Без perl вырезать текст сообщения нечем, и разбирать его как SQL нельзя:
104
+ # штатный коммит упирался бы в отказ. Доставки SQL в git-команде не бывает,
105
+ # поэтому здесь дешевле пропустить, чем ломать работу.
106
+ case "$cmd" in
107
+ git\ *|*/git\ *|gh\ *|*/gh\ *) exit 0 ;;
108
+ esac
109
+ fi
110
+
111
+ sql="$cmd"
112
+ context="команда psql/prisma"
113
+ fi
114
+ ;;
115
+ *) exit 0 ;;
116
+ esac
117
+ }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # Адресат запроса для гарда хранилища: боевая база, одноразовая база проверки или что-то
3
+ # третье — и что на боевой разрешено.
4
+ #
5
+ # Строки `# rt-hook:` здесь нет намеренно: событие и образец вызова объявляет сам гард, а
6
+ # помощник рядом хуком не регистрируется и в одиночку ничего не решает.
7
+
8
+ # --- прод: запись запрещена в любом виде ------------------------------------------------
9
+ # Признак берётся из профиля: порт туннеля, хост, домен — у каждого дерева свои.
10
+ #
11
+ # Ищем его в АДРЕСЕ, а не в данных: домен приложения живёт и в самих строках — в почте
12
+ # владельца, в канонической ссылке объекта. Пока признак брался по всей команде, вставка
13
+ # строки с таким адресом в локальную базу отклонялась как запись в бой, а опт-аута у этой
14
+ # ветки нет по замыслу — команда становилась неисполнимой.
15
+ # Поэтому текст запроса (то, что стоит после `-c`/`--command`) из проверки вырезается.
16
+ #
17
+ # Запрос из редактора адресата в тексте не называет: база выбирается идентификатором
18
+ # подключения, и по одному SQL отличить прод от локальной копии невозможно. Поэтому
19
+ # подключения опознаются в лицо. Список сверяется вызовом list_database_connections;
20
+ # добавили новое — допишите сюда, иначе оно попадёт в «неизвестные» ниже.
21
+ sql_resolve_target() {
22
+ is_prod=""
23
+ PROD_CONNECTIONS="${RT_PROD_CONNECTIONS:-}"
24
+ LOCAL_CONNECTIONS="${RT_LOCAL_CONNECTIONS:-}"
25
+
26
+ conn_known=""
27
+ if [ -n "$conn" ]; then
28
+ case " $PROD_CONNECTIONS " in *" $conn "*) is_prod="yes"; conn_known="prod" ;; esac
29
+ # Опознанное локальное подключение сильнее текстовой догадки: домен приложения живёт в
30
+ # самих данных — в почте владельца, в канонической ссылке объекта. Пока вывод не
31
+ # отменялся, обновление такой строки на локальной базе отклонялось как запись в бой, и
32
+ # обойти это было нечем.
33
+ case " $LOCAL_CONNECTIONS " in *" $conn "*) conn_known="local"; is_prod="" ;; esac
34
+ fi
35
+
36
+ # Для запроса из редактора адрес в тексте не назван вовсе: там решает опознанное
37
+ # подключение, а если оно неизвестно — ветка ниже спросит пользователя.
38
+ is_scratch=""
39
+ addr_other=""
40
+ if [ -z "$conn" ] && [ "$context" != "запрос через подключение редактора (обёртка исполнителя)" ]; then
41
+ while IFS= read -r seg; do
42
+ [ -z "$seg" ] && continue
43
+ if [ -n "$PROD_DSN" ] && printf '%s' "$seg" | grep -qE "$PROD_DSN"; then
44
+ is_prod="yes"
45
+ addr_other="yes"
46
+ continue
47
+ fi
48
+ # Одноразовая база проверки: петлевой адрес и порт из отведённого под них диапазона.
49
+ # Диапазон занимают контейнеры, которые поднимают на время одной проверки —
50
+ # восстановление копии, репетиция миграции на непустой базе — и сносят следом. Данных,
51
+ # которые стоило бы стеречь, там нет по построению, а вопрос на каждую строку такой
52
+ # проверки приучает отвечать «да» не читая и обесценивает тот вопрос, который был важен.
53
+ #
54
+ # Диапазон узкий и петлевой намеренно: под него не должны попадать ни рабочая база
55
+ # дерева, ни туннель к бою, ни базы соседних деревьев на этой машине. Какой он здесь,
56
+ # знает профиль; не назван — исключения нет вовсе, и вопрос задаётся всегда.
57
+ if [ -n "${RT_SCRATCH_PORT_RE:-}" ] \
58
+ && printf '%s' "$seg" | grep -qE "$RT_SCRATCH_PORT_RE" \
59
+ && printf '%s' "$seg" | grep -qE '127\.0\.0\.1|localhost|host\.docker\.internal'; then
60
+ is_scratch="yes"
61
+ else
62
+ # Любой другой адресованный вызов снимает исключение целиком: в цепочке
63
+ # `psql -p 19434 -f x.sql && psql -c "delete …"` ранний выход убрал бы проверку
64
+ # со второго звена, а это ровно тот обход, ради которого гард и написан.
65
+ addr_other="yes"
66
+ fi
67
+ done <<EOF
68
+ $(addr_segments)
69
+ EOF
70
+ fi
71
+ }
72
+
73
+ # Исключение стоит ПОСЛЕ разбора адреса и ПЕРЕД правилами записи, но строго после того, как
74
+ # признак боевой базы уже выставлен: прод перекрывает исключение при любом совпадении, а не
75
+ # наоборот. Условие тройное — одноразовый адрес найден, боевого нет, и других адресованных
76
+ # вызовов в команде нет вовсе.
77
+ sql_pass_scratch() {
78
+ if [ -n "$is_scratch" ] && [ -z "$is_prod" ] && [ -z "$addr_other" ]; then
79
+ exit 0
80
+ fi
81
+ }
82
+
83
+ # На боевой базе разрешено только то, что гард опознал как чтение — БЕЛЫМ списком, а не
84
+ # перечислением запретов. Чёрный список здесь принципиально не работает: SQL доезжает до
85
+ # сервера файлом, редиректом, `\copy`, `SELECT … INTO`, и каждая заделанная форма оставляет
86
+ # соседнюю. Поэтому вопрос перевёрнут: не «есть ли здесь запись», а «доказано ли чтение».
87
+ #
88
+ # Чтением считаются три формы, и каждая проверяется в СЕГМЕНТЕ своего вызова:
89
+ # pg_dump без --clean/--create — снятие дампа
90
+ # psql -c "<один SELECT>" — запрос без второго стейтмента
91
+ # psql -l / --version / --help — проверка соединения без запроса
92
+ sql_check_prod() {
93
+ if [ -n "$is_prod" ] && [ -n "$is_write" ]; then
94
+ deny "BLOCKED: запись в БОЕВОЕ хранилище (${context}). Адрес ведёт на бой — оттуда данные не восстанавливаются ничем, кроме копии. Схема на бою меняется миграцией через выкатку, данные — через панель владельца. Если правка данных на бою действительно нужна, её делает владелец руками, предварительно сняв копию; гард обойти нельзя."
95
+ fi
96
+
97
+ if [ -n "$is_prod" ]; then
98
+ unproven=""
99
+ while IFS= read -r seg; do
100
+ [ -z "$seg" ] && continue
101
+ seg_read=""
102
+ # Аргументы клиента — всё, что стоит ПОСЛЕ его имени. До имени в том же сегменте
103
+ # свободно живут чужие флаги: `docker compose -f docker-compose.prod.yml … psql`,
104
+ # и `-f` от compose однажды отменял доказательство чтения.
105
+ seg_tail="$(printf '%s' "$seg" | perl -0pe 's{^.*?(?<![[:alnum:]_./-])(psql|pg_restore|prisma)(?=\s|$)}{$1}s' 2>/dev/null)"
106
+ [ -z "$seg_tail" ] && seg_tail="$seg"
107
+
108
+ if printf '%s' "$seg" | grep -qE '(^|[^[:alnum:]_.-])pg_dump(all)?([^[:alnum:]_.-]|$)'; then
109
+ case "$seg" in
110
+ *--clean*|*--create*) ;;
111
+ *) seg_read="yes" ;;
112
+ esac
113
+ fi
114
+
115
+ # Один SELECT и ничего кроме: второй стейтмент через `;` уже не чтение.
116
+ if printf '%s' "$seg" | grep -qE '(^|[^[:alnum:]_])select([^[:alnum:]_]|$)' \
117
+ && ! printf '%s' "$seg" | grep -qE '(^|[^[:alnum:]_])(delete|update|insert|truncate|drop|alter|grant|revoke|copy)([^[:alnum:]_]|$)|\\copy|into[[:space:]]+[a-z_"]' \
118
+ && ! printf '%s' "$seg_tail" | grep -qE '(^|[[:space:]])(-f|--file)([[:space:]]|=)'; then
119
+ seg_read="yes"
120
+ fi
121
+
122
+ printf '%s' "$seg_tail" | grep -qE '(^|[[:space:]])(-l|--list|--version|--help)([[:space:]]|$)' \
123
+ && seg_read="yes"
124
+
125
+ [ -z "$seg_read" ] && unproven="yes"
126
+ done <<EOF
127
+ $segments
128
+ EOF
129
+
130
+ if [ -n "$unproven" ]; then
131
+ deny "BLOCKED: вызов клиента БД против БОЕВОЙ базы (${context}). На проде разрешено только доказанное чтение — \`pg_dump\` без \`--clean\`, одиночный SELECT через \`-c\`, либо \`-l\`/\`--version\`. Всё остальное отклоняется, даже если гард просто не разобрал команду: содержимое файлов (\`-f\`, \`< dump.sql\`), \`\\copy\` и восстановление дампа ему не видны. Если нужен разбор данных боевой базы — снимай дамп и работай с локальной копией."
132
+ fi
133
+ fi
134
+ }