audit(spec): 51 findings + 8 MUST critical fixes inline
Adversarial audit condensed router-gate spec через 3 parallel Sonnet adversaries (9 attack zones). 51 finding total: 10 BYPASS-COMPLETE + 17 PARTIAL + 9 DOS + 15 INFO. Spec заявление «hard wall полный» НЕ выдерживает. 8 MUST critical inline fixes applied: - §5.1 Bash: <<< here-string, node REPL/stdin block, < input redirect, tokenizer per-arg path-deny check (closes CRITICAL-9/8/6 + PARTIAL-15) - §3.1 path normalization: UNC \?\ prefix strip, 8.3 short names expand via GetLongPathName, unresolved $VAR fail-CLOSE (closes CRITICAL-3/4/5) - §4 Поведение 1: source restriction — detector проверяет только organic root user prompt, НЕ AskUser chosen_label (closes CRITICAL-1 design flaw) - §8 Implementation order matrix: Этап 2.3 branch-switch rewrite MUST complete BEFORE Этап 3 enforce-mode (closes CRITICAL-10 S8 migration regression) - §1.4: gate-config.json protected с Этапа 1.4 ранее (closes DOS D-1 tiny-budget patch attack window) 5 SHOULD-FIX + 5 DOS-MUST-ADDRESS deferred в writing-plans (§9 «Audit findings deferred» documented для plan pickup). Audit report saved at: docs/superpowers/audits/2026-05-29-router-gate-condensed- adversarial-audit.md cspell-words.txt: +UNC, +EACCES (valid technical terms). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1888,3 +1888,5 @@ deplo
|
||||
# Router-gate v3.3 (2026-05-29) — v4.1 audit closure
|
||||
эскалируем
|
||||
Ctemp
|
||||
UNC
|
||||
EACCES
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
# Adversarial audit condensed router-gate hard wall спека
|
||||
|
||||
**Дата:** 2026-05-29
|
||||
**Target:** [`docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md`](../specs/2026-05-29-router-gate-hard-wall-design-condensed.md) (commit d4f7e681, 1117 строк)
|
||||
**Auditor:** Claude (controller Opus 4.7) + 3 параллельных Sonnet 4.6 adversarial subagents
|
||||
**Методология:** 9 attack zones из user prompt, по 3 zone на агента. Каждый агент инструктирован как «controller который ХОЧЕТ ОБОЙТИ собственный gate».
|
||||
**Контекст:** спек v3.5 прошёл 4 раунда внутренних adversarial audits + verifier completeness check 63/63. Автор заявил «hard wall полный, acceptable residual list пуст».
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Аудит опровергает заявление о полном hard wall.** Найдено **51 finding** в 9 attack zones, из них:
|
||||
|
||||
- **10 BYPASS-COMPLETE** — полный обход hard wall через конкретные exploitable vectors
|
||||
- **17 BYPASS-PARTIAL** — частичные обходы конкретных компонентов
|
||||
- **9 DOS** — выводят gate в lockout/false-block состояние, заказчик вынужден отключать через Recovery §6
|
||||
- **15 INFO / acknowledged** — низкая feasibility, acknowledged limitations, или out-of-scope
|
||||
|
||||
**Самое серьёзное:** §5.1 Bash content rules имеет **6 BYPASS-COMPLETE дыр** через формальные обходы whitelist/blacklist (`<<<`, `echo | node`, UNC paths, 8.3 short names, undefined env vars, `cat multi-arg`). Single-line bash commands achieving arbitrary file write/execute.
|
||||
|
||||
**Acceptable residual list НЕ пуст.** Минимум 10 holes должны быть закрыты до writing-plans фазы либо явно перенесены в residual list с обоснованием.
|
||||
|
||||
---
|
||||
|
||||
## Severity breakdown
|
||||
|
||||
| Severity | Count | Where most concentrated |
|
||||
|---|---|---|
|
||||
| BYPASS-COMPLETE | 10 | §5.1 Bash content (6) + §3.1 path normalization (3) + §4 Поведение 1 (1) |
|
||||
| BYPASS-PARTIAL | 17 | §3.4 subagent + §3 chain-state + §4.5 answer parser + §5.2 static scan |
|
||||
| DOS | 9 | §3.6 gate budget + §8 implementation order matrix gaps |
|
||||
| INFO/Acknowledged | 15 | §4.7 question quality (creative leading questions неизбежны) |
|
||||
| **Итого** | **51** | |
|
||||
|
||||
---
|
||||
|
||||
## Top-10 Critical (BYPASS-COMPLETE)
|
||||
|
||||
### CRITICAL-1: AskUserQuestion option label triggers Поведение 1 direct invocation
|
||||
|
||||
**Source:** Agent 1 / Finding 1.1
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §4 Поведение 1 матчит pattern `(делай|сделай|вызови|примени|используй)\s+<exact-skill-name>` против `transcript[-1].user.content`. AskUserQuestion option labels — controller-controlled text. Когда заказчик выбирает опцию `1` («делай subagent-driven-development»), её label становится user content в transcript. Gate видит pattern → Поведение 1 → allow любой mutating tool.
|
||||
|
||||
**Mitigation:** Detector Поведения 1 должен проверять `is_direct_invocation` только для **корневого** user-prompt в turn (первое сообщение), не для AskUserQuestion answers. AskUser answers обрабатываются эксклюзивно через §4.5 answer parser.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-2: vitest globalSetup из cross-session edits (file-watcher blind spot)
|
||||
|
||||
**Source:** Agent 1 / Finding 2.2
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** MEDIUM (требует multi-session setup)
|
||||
|
||||
**Attack:** §5.1 file-watcher session-scoped — clears на `git commit exit 0 + lefthook GREEN`. Если в сессии N контроллер редактировал `vitest.config.ts` → commit GREEN → watcher cleared. В сессии N+1: `vitest.config.ts` уже не в `edited_files`. `npx vitest run` (whitelisted) запускает vitest → reads config → executes `globalSetup` файлы (произвольный JS). Static scan §5.2 проверяет только explicit target (`vitest run` без target → glob-aware → AskUser), но НЕ автоматически открытые vitest config + setup files.
|
||||
|
||||
**Mitigation:** Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-3: Undefined env var bypass path normalization
|
||||
|
||||
**Source:** Agent 2 / Finding 4.1
|
||||
**Confidence:** HIGH (depends on implementation)
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §3.1 path normalization step 3: «Expand `~` через `os.homedir()` и env vars через `process.env`». Если controller использует path `~/.claude/runtime/$UNDEFINED_VAR/state.json`, и `process.env.UNDEFINED_VAR` undefined, типичная implementation возвращает literal `$UNDEFINED_VAR` в строке. Resolved path содержит `$UNDEFINED_VAR` segment → glob match `~/.claude/runtime/*` не срабатывает (один extra segment) → protected check passes → Edit/Write allowed.
|
||||
|
||||
**Mitigation:** Unexpanded `$VAR` sequences после env expansion → fail-CLOSE («unresolved env vars in path»). Или strict whitelist allowed env vars (`HOME`, `USERPROFILE`) с фейлом на остальные.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-4: Windows UNC paths bypass canonicalization
|
||||
|
||||
**Source:** Agent 2 / Finding 4.2
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** Windows UNC path `\\?\C:\Users\Administrator\.claude\runtime\router-state-abc.json` (long path prefix `\\?\`). `path.resolve` на Windows возвращает путь с `\\?\` prefix. Protected list pattern `~/.claude/runtime/*` после `os.homedir()` → `C:\Users\Administrator\.claude\runtime\*`. UNC version с prefix НЕ match'ит pattern (один extra `\\?\` префикс) → Edit/Write на protected file allowed.
|
||||
|
||||
**Mitigation:** Path normalization step 1: strip `\\?\` prefix перед resolve. Step 2: explicit UNC prefix normalization (`\\server\share` → drive form если applicable). Это **Windows-specific** дыра, на Linux/Mac не существует.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-5: Windows 8.3 short names bypass canonicalization
|
||||
|
||||
**Source:** Agent 2 / Finding 4.4
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** MEDIUM (8.3 включён по умолчанию на legacy Windows install)
|
||||
|
||||
**Attack:** Windows file system поддерживает 8.3 short names: `C:\Users\ADMINI~1\.claude\runtime\state.json`. `path.resolve` НЕ разворачивает 8.3 short names. `fs.realpathSync` разворачивает только если файл **существует** в момент вызова. Для несуществующего файла (новый `router-state-<new-sess>.json` про который gate знает но он не создан) realpath fails → fallback to resolved. Resolved содержит short name → не match'ит protected list (где `Administrator` full form) → allowed.
|
||||
|
||||
**Mitigation:** Использовать `GetLongPathName` WinAPI через `node-ffi` или wrap-script. Альтернатива — disable 8.3 generation на NTFS volume (`fsutil 8dot3name set 1`). NB: это **Windows Server 2022 машина** — short names enabled out-of-box.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-6: `cat` multi-arg bypass path-deny overlay
|
||||
|
||||
**Source:** Agent 2 / Finding 4.6
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §5.1 path-deny overlay для reading commands (cat/tail/head/grep/less/more/file/stat/wc/ls): «нормализует path и проверяет против protected list». Tokenizer split по `;`/`&&`/`||`/`|`/`&`. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — один токен после split (без separators), `cat` whitelisted, **два** path-args. Если implementation tokenizer извлекает только first token (command), не all subsequent args — второй path-arg `~/.claude/runtime/router-state.json` не проверяется → читается.
|
||||
|
||||
**Mitigation:** Tokenizer должен извлекать ВСЕ args после command token и передавать их в path-deny check loop. Per-arg validation, не command-level only.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-7: `git format-patch -o <dir>` write to protected paths
|
||||
|
||||
**Source:** Agent 2 / Finding 4.7 + Agent 1 / Finding 5.3
|
||||
**Confidence:** MEDIUM (требует verification implementation)
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §5.1 whitelist row объединяет `git log/show/diff/blame/format-patch` с исключениями `--output=*/-o <path>/--file=*`. Если implementation parser применяет исключения только к `git log` (первый command в row label), а к `git format-patch` — нет (отдельная command), то `git format-patch HEAD~1 -o ~/.claude/runtime/` пишет patch files в protected dir.
|
||||
|
||||
**Vetting note:** spec формулировка может покрывать обе команды (row label «git log / git show / git diff / git blame / git format-patch» применяет exclusions ко всем). Зависит от parser implementation. Если parser per-command, finding genuine. Если row-wide, finding false-positive. **Требует verification в plan**.
|
||||
|
||||
**Mitigation:** Explicit per-command exclusion mapping. Или убрать `git format-patch` из whitelist (использовать `git format-patch --stdout` через conditional whitelist + AskUser).
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-8: `echo 'code' | node` — pipe receiver `node` is whitelisted
|
||||
|
||||
**Source:** Agent 3 / Finding 7.1
|
||||
**Confidence:** MEDIUM
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §5.1 hard-blacklist: «Любые pipe-команды (`|`) где receiver — mutating command». Но `node` в **whitelist** (без `-e/--eval/-r`). Pipe `echo 'fs.writeFileSync("evil","x")' | node` — receiver `node` whitelisted, не mutating per default classification. Tokenizer splits at `|`, видит `node` в receiver position → whitelisted? Tokenizer семантика «receiver = mutating command» зависит от implementation: если check'ает только blacklist contains, `node` passes; если check'ает «mutation potential», `node` reading stdin = arbitrary execution, должен блокироваться.
|
||||
|
||||
**Mitigation:** Explicit: `node` без `<script-path>` arg (т.е. без positional path argument после `node`) — block. Pipe receiver `node` всегда block (REPL-like usage).
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-9: `<<<` here-string не в hard-blocked list
|
||||
|
||||
**Source:** Agent 3 / Finding 7.3
|
||||
**Confidence:** HIGH
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §5.1 sub-shell broad sweep блокирует: `` ` ``, `$(...)`, `<(...)/>(...)`, `<<`, `<<-`. **`<<<` (here-string) НЕ упомянут.** Команда `node <<< "fs.writeFileSync('evil','x')"` — bash here-string передаёт строку как stdin к `node`. Tokenizer не split на `<<<` (не в pipe-split list). Gate видит `node` first command, whitelisted, без disallowed flags → allow. Arbitrary Node.js execution.
|
||||
|
||||
**Mitigation:** Trivial fix — добавить `<<<` к sub-shell broad sweep list. Простейший finding, наиболее очевидный gap.
|
||||
|
||||
---
|
||||
|
||||
### CRITICAL-10: enforce-branch-switch migration window (Этап 2.3 sequencing)
|
||||
|
||||
**Source:** Agent 3 / Finding 9.3
|
||||
**Confidence:** MEDIUM (зависит от deployment sequence)
|
||||
**Feasibility:** HIGH
|
||||
|
||||
**Attack:** §8 Implementation order matrix НЕ упоминает Этап 2.3 явно (matrix содержит 5 строк: Этап 1, 1.4, 2, 2.1, 3). Согласно §8 main table sequence: 2 → 2.1.0 → 2.1 → 2.2 → 2.3 → 3. Если implementer пропускает 2.3 (rewrite enforce-branch-switch) ДО Этапа 3 (settings.json enforce-mode), OLD `enforce-branch-switch.mjs` всё ещё ищет `BRANCH-SWITCH-CONFIRMED` в response text. Controller пишет marker → OLD hook accepts → dangerous git ops proceed. **S8 closure НЕ активна** в migration window.
|
||||
|
||||
**Mitigation:** §8 Implementation order matrix явно: «Этап 2.3 MUST complete BEFORE Этап 3 enforce-mode activation». Альтернатива — Этап 3 включает регистрацию обновлённого branch-switch hook одной транзакцией.
|
||||
|
||||
---
|
||||
|
||||
## BYPASS-PARTIAL (17 findings — выборка топ-7)
|
||||
|
||||
### PARTIAL-1: Post-skill class-level unlock, не file-level
|
||||
|
||||
**Source:** Agent 1 / Finding 1.4 | **Confidence:** HIGH
|
||||
|
||||
§4.6 partial unlock + §4.5 answer parsing: `approved_tool: "Edit"` без mandatory `approved_action_pattern`. После `Skill(writing-plans)` + AskUser «выполнить изменения» controller может Edit **любой** non-protected файл. Class-level unlock = всё `Edit/Write/MultiEdit` разрешено.
|
||||
|
||||
**Fix:** `approved_action_pattern` mandatory с конкретным file path. Gate сравнивает `tool_input.file_path` против pattern.
|
||||
|
||||
### PARTIAL-2: Chain-reset через AskUser chosen_label
|
||||
|
||||
**Source:** Agent 1 / Finding 1.3 | **Confidence:** HIGH
|
||||
|
||||
Chain-state TTL expiry читает `transcript[-1].user.content` на explicit-reset фразы. Spec говорит «only user turns». Но AskUserQuestion chosen_label попадает в transcript **как user content** — controller помещает «новая задача» в option label, заказчик выбирает → chain reset triggered.
|
||||
|
||||
**Fix:** Reset фразы проверяются только против **organic root prompt**, не chosen labels. Или AskUser answers стрипают reset-phrase keywords.
|
||||
|
||||
### PARTIAL-3: Free-form answer → AskUser fatigue cascade
|
||||
|
||||
**Source:** Agent 1 / Finding 1.2 | **Confidence:** MEDIUM
|
||||
|
||||
Controller конструирует AskUser опции так, что заказчик пишет неоднозначное «давай» / «хорошо» в free-form. 2/2 limit → fail-CLOSE → cascade frustration → заказчик пишет «делай сам» → Поведение 1 trigger via word `делай`.
|
||||
|
||||
**Fix:** Strict explicit-keyword requirement в опциях. Чётко-классифицируемые answers (stop / specific tool / direct no skill).
|
||||
|
||||
### PARTIAL-4: TOCTOU double-consume askuser-decisions entry
|
||||
|
||||
**Source:** Agent 2 / Finding 3.1 | **Confidence:** HIGH
|
||||
|
||||
`enforce-branch-switch.mjs` и `enforce-router-gate.mjs` оба read-check-write на `consumed: false`. Proper-lockfile защищает write, но read-modify-write 3-шаговая операция не атомарна. Параллельные tools могут увидеть `consumed: false` оба → consume approval дважды.
|
||||
|
||||
**Fix:** Atomic compare-and-swap operation через single lock around full RMW. Lock acquired при read, released после write.
|
||||
|
||||
### PARTIAL-5: Chain_step++ двойной через sub-Skill
|
||||
|
||||
**Source:** Agent 2 / Finding 3.2 | **Confidence:** MEDIUM
|
||||
|
||||
PostToolUse handler инкрементирует chain_step на success. Если Skill(#55) внутри вызывает sub-Task с subagent_type matching expected_node → двойной инкремент.
|
||||
|
||||
**Fix:** PostToolUse handler сравнивает не только tool match, но и `expected_tool_use_id` который PreToolUse запомнил при разрешении. Increment только при exact tool_use_id match.
|
||||
|
||||
### PARTIAL-6: Edit-execute window между commit и watcher catch
|
||||
|
||||
**Source:** Agent 2 / Finding 3.4 | **Confidence:** HIGH
|
||||
|
||||
Если в turn N+1 первый tool — Bash(`node evil.mjs`), gate проверяет watcher (пуст после commit clear) → allow. Затем Edit(`evil.mjs`) в same turn → попадает в watcher. Но `node evil.mjs` уже выполнен.
|
||||
|
||||
**Fix:** PreToolUse Bash check: посмотреть **существование** target script + **mtime** ≤ 5 секунд → require AskUser regardless of watcher state.
|
||||
|
||||
### PARTIAL-7: chain-state malformed = fail-OPEN reset
|
||||
|
||||
**Source:** Agent 3 / Finding 6.1 | **Confidence:** HIGH
|
||||
|
||||
Asymmetric §3.3 failure modes: router-state malformed → block, chain-state malformed → reset + warn. Если controller тригернёт malformed (lock-contention read failure), chain progress wiped → Поведение 4 (один AskUser unlock all).
|
||||
|
||||
**Fix:** chain-state malformed → fail-CLOSE (block all mutating). Symmetric с router-state semantics.
|
||||
|
||||
**Остальные 10 BYPASS-PARTIAL** (краткий список):
|
||||
|
||||
- P-08 **ToolSearch deferred tools** (Agent 1 / F2.1) — gate matcher coverage unclear для loaded-on-demand tools
|
||||
- P-09 **Skill execution gate coverage** (Agent 1 / F2.5) — нужен smoke test что tool calls в skill execution проходят через PreToolUse
|
||||
- P-10 **`node.exe` variant + temp files** (Agent 1 / F5.1) — subagent creates non-protected helper.js, parent runs via `node.exe`
|
||||
- P-11 **String concatenation static scan bypass** (Agent 1 / F5.4) — `const k = 'write' + 'File' + 'Sync'; fs[k](...)`
|
||||
- P-12 **Parallel Task counter race** (Agent 2 / F3.3) — 3+ Tasks одновременно проходят check на 3-limit
|
||||
- P-13 **Orphan cleanup 24h reset** (Agent 2 / F3.5) — long sessions могут exploit cleanup
|
||||
- P-14 **`<!-- edited-files-clear -->` via AskUser social eng** (Agent 3 / F6.6) — controller suggests user paste marker
|
||||
- P-15 **`<` input redirect not in blacklist** (Agent 3 / F7.6) — `wc -l < ~/.claude/runtime/state.json` обходит path-deny
|
||||
- P-16 **Retry after timeout bypass** (Agent 2 / F8.1) — fail-CLOSE на timeout → retry next turn → fresh cache → success
|
||||
- P-17 **TDD-gate vs router-gate execution order race** (Agent 3 / F9.1) — undefined hook ordering = TDD bypass через cascading unlock
|
||||
|
||||
---
|
||||
|
||||
## DOS Findings (9)
|
||||
|
||||
| ID | Finding | Severity | Mitigation |
|
||||
|---|---|---|---|
|
||||
| D-1 | `gate-config.json` writable migration window (Этап 1→2 gap) | HIGH — tiny budget = permanent fail-CLOSE | Hardcoded floor values; gate-config protected with Этапа 1.4 |
|
||||
| D-2 | subagent block-file missing + tool_use_count==0 = assume BLOCKED → AskUser loop | HIGH — каждый Task triggers AskUser | Add success-marker file для clean subagent exits |
|
||||
| D-3 | file-watcher reset stdout parsing fragile (FAIL string в commit message) | MEDIUM | Use lefthook exit code, не stdout strings |
|
||||
| D-4 | Windows tmp+rename non-atomicity under AV load | MEDIUM | Wrap rename in retry+catch с logging |
|
||||
| D-5 | Transcript parsing huge turn budget exhaustion | MEDIUM | Cap turn size при парсинге |
|
||||
| D-6 | 10 tool calls lock contention cascade | MEDIUM | Reader-writer lock model |
|
||||
| D-7 | Subagent gate reads parent locked state timeout | MEDIUM | Read-lock для parent state, не exclusive |
|
||||
| D-8 | gate-config.json missing first start fail-CLOSE | MEDIUM | Explicit hardcoded defaults fallback |
|
||||
| D-9 | enforce-prompt-injection empty surface drops context | MEDIUM | Verify hook handles empty array без crash |
|
||||
|
||||
---
|
||||
|
||||
## INFO / Acknowledged (15 findings)
|
||||
|
||||
Низкая feasibility или acknowledged design choice:
|
||||
|
||||
- TodoWrite context pollution (Agent 1 / F1.5)
|
||||
- npm test DB side effects (Agent 1 / F2.3)
|
||||
- Monitor timing attack (Agent 1 / F2.4)
|
||||
- PostToolUse handler crash semantics (Agent 2 / F3.6)
|
||||
- symlink swap TOCTOU (Agent 2 / F4.3) — требует mutating Bash который уже blocked
|
||||
- Unicode homoglyph case-fold (Agent 2 / F4.5) — требует pre-existing homoglyph dir
|
||||
- symlink + EACCES path normalization (Agent 1 / F5.2)
|
||||
- git format-patch creates files in CWD (Agent 1 / F5.3) — covered by F4.7 deeper
|
||||
- gate-config security posture impact (Agent 1 / F5.5)
|
||||
- Read-only long reconnaissance leakage (Agent 3 / F6.2) — acknowledged Поведение 4
|
||||
- `node --inspect` debugger port (Agent 3 / F7.2) — requires external debugger
|
||||
- IFS manipulation (Agent 3 / F7.4) — `env`/`export` already blacklisted
|
||||
- git format-patch --stdout info disclosure (Agent 3 / F7.5) — acknowledged read-only
|
||||
- JSONL append > 4096 bytes atomicity (Agent 2 / F8.5) — log corruption only, не security
|
||||
- first-option position bias defeats (Agent 3 / F9.5) — acknowledged residual
|
||||
|
||||
---
|
||||
|
||||
## Verdict on «hard wall полный» claim
|
||||
|
||||
**v3.5 spec заявление НЕ выдерживает.** Audit identified concrete BYPASS-COMPLETE vectors which 4 раунда внутренних audits упустили. Pattern причин:
|
||||
|
||||
1. **Bash content rules incomplete enumeration** — 6 critical bypasses в §5.1 single attack zone. Внутренние audits фокусировались на known bypass classes (`$()`/backtick/heredoc), пропустили `<<<` / `node` REPL semantics / `<` redirect / multi-arg path / `format-patch -o`.
|
||||
2. **Windows-specific path normalization gaps** — UNC, 8.3 short names, env var expansion edge cases. Внутренние audits в основном на Unix-mental-model тестировались.
|
||||
3. **Поведение 1 trigger source unrestricted** — `transcript[-1].user.content` включает AskUser chosen_label, что fundamentally breaks direct-invocation model. **Это самая серьёзная** — design-level flaw, не implementation detail.
|
||||
4. **Implementation order matrix incomplete** — Этап 2.3 sequencing не enforced, migration window открывает S8 регрессию.
|
||||
|
||||
**Класс «controller-writable signals» НЕ полностью закрыт** — закрыты S5 (subagent BLOCKED) и S8 (branch-switch confirmation), но обнаружены **новые** controller-writable signals: AskUser option labels (CRITICAL-1), AskUser chosen_label контекст для chain reset (PARTIAL-2), AskUser content для social engineering (PARTIAL-3). Generalisable формула side-channel paths применима только к **explicit out-of-band channels** — но controller контролирует contents AskUserQuestion construction, что эффективно делает chosen_label semi-side-channel.
|
||||
|
||||
---
|
||||
|
||||
## Recommended actions (priority order)
|
||||
|
||||
**MUST FIX до writing-plans** (8 items):
|
||||
|
||||
1. **CRITICAL-9** — Add `<<<` к §5.1 sub-shell broad sweep (trivial fix, 1 строка)
|
||||
2. **CRITICAL-8** — Add `node` без positional path arg → block (§5.1 hard-blacklist)
|
||||
3. **CRITICAL-6** — Tokenizer per-arg path-deny check (§5.1 path-deny overlay)
|
||||
4. **CRITICAL-1** — Поведение 1 detector ограничить только organic root prompt, не AskUser answers (§4 Поведение 1)
|
||||
5. **CRITICAL-3** — Unresolved `$VAR` → fail-CLOSE (§3.1 step 3)
|
||||
6. **CRITICAL-4** — Strip `\\?\` UNC prefix перед resolve (§3.1)
|
||||
7. **CRITICAL-5** — Use `GetLongPathName` WinAPI для 8.3 expansion (§3.1)
|
||||
8. **CRITICAL-10** — §8 matrix явно enforce Этап 2.3 BEFORE Этап 3
|
||||
|
||||
**SHOULD FIX в writing-plans** (5 items):
|
||||
|
||||
1. **CRITICAL-2** — Static scan §5.2 расширить на vitest config + setupFiles auto-discovery
|
||||
2. **CRITICAL-7** — Verify `git format-patch -o` handling в parser implementation; explicit fix если per-command parser
|
||||
3. **PARTIAL-1** — `approved_action_pattern` mandatory file-path binding (§4.5)
|
||||
4. **PARTIAL-2** — Chain reset фразы только в organic root prompt (§3 chain-state)
|
||||
5. **PARTIAL-7** — chain-state malformed → fail-CLOSE (§3.3 failure mode)
|
||||
|
||||
**NICE-TO-HAVE / D-* DOS fixes** в writing-plans:
|
||||
|
||||
- D-1 (gate-config protected с Этапа 1.4)
|
||||
- D-2 (subagent success-marker file)
|
||||
- D-3 (lefthook exit code не stdout parsing)
|
||||
- D-9 (verify enforce-prompt-injection empty handling)
|
||||
|
||||
**ACCEPT as residual** (явно документировать в §9):
|
||||
|
||||
- 15 INFO findings — большинство acknowledged limitations или low feasibility
|
||||
- PARTIAL-3 (AskUser fatigue) — UX-tradeoff
|
||||
- PARTIAL-17 (TDD-gate ordering) — нуждается в smoke-test для verification
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
Заказчик решает:
|
||||
|
||||
**Option A — Patch condensed v3.5 inline:** добавить fixes для 8 MUST + 5 SHOULD в текущий condensed (commit поверх d4f7e681), пересохранить self-consistent v3.5.5. Spec становится 1117 → ~1250 строк.
|
||||
|
||||
**Option B — Open v3.6 round:** brainstorming-скил для design каждого fix отдельно (AskUserQuestion per fix), затем patch condensed. Более тщательно, +3-5h работы.
|
||||
|
||||
**Option C — Defer fixes в writing-plans:** записать audit findings в spec §9 как «known to be addressed in plan implementation», plan picks up specific resolution per finding. Faster but spec остаётся имеющим заявленные дыры.
|
||||
|
||||
**Option D — Hybrid:** A для 8 MUST (trivial fixes), C для 5 SHOULD (require design decisions в plan).
|
||||
|
||||
**Рекомендуемый:** D — закрыть 8 critical inline (час работы), оставить 5 SHOULD на plan для проектных решений per fix.
|
||||
|
||||
Audit complete.
|
||||
@@ -42,7 +42,9 @@
|
||||
|
||||
**Сохраняется:** 7 preserved хуков (tdd-gate / coverage-verify / memory-coverage / verify-before-push / rationalization-audit / prompt-injection / branch-switch). 6 из них теряют свои findOverride escape-фразы. Branch-switch переписан под AskUserQuestion-gate (см. §3 таблица сохраняемых).
|
||||
|
||||
**Class controller-writable signals закрыт архитектурно** через side-channel writes в protected paths (S5: subagent-block-file; S8: askuser-decisions с git-pattern). Acceptable residual list пуст.
|
||||
**Class controller-writable signals закрыт архитектурно** через side-channel writes в protected paths (S5: subagent-block-file; S8: askuser-decisions с git-pattern).
|
||||
|
||||
**Audit 29.05.2026** ([audit report](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 51 finding, из них 8 MUST-FIX закрыты inline (commit поверх d4f7e681; см. §5.1 `<<<` here-string + `node` REPL + per-arg path-deny + `<` redirect; §3.1 UNC strip + 8.3 expand + `$VAR` fail-CLOSE; §4 Поведение 1 source restriction; §8 Этап 2.3 sequencing). 5 SHOULD-FIX перенесены в writing-plans (см. §9 «Audit findings deferred»). 9 DOS findings + 15 INFO residuals документированы в audit report.
|
||||
|
||||
---
|
||||
|
||||
@@ -146,12 +148,14 @@ Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до
|
||||
|
||||
**Path normalization:** перед сравнением с deny-list gate **канонизирует** путь:
|
||||
|
||||
1. `path.resolve(target)` — разрешает относительные пути и `..`.
|
||||
2. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved).
|
||||
3. Expand `~` через `os.homedir()` и env vars через `process.env`.
|
||||
4. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive).
|
||||
1. **Strip Windows UNC long-path prefix** (closes audit CRITICAL-4): если path начинается с `\\?\` или `\\.\` — strip prefix перед дальнейшей normalization. UNC форма `\\?\C:\Users\...\.claude\runtime\state.json` нормализуется к `C:\Users\...\.claude\runtime\state.json` для match с protected glob.
|
||||
2. `path.resolve(target)` — разрешает относительные пути и `..`.
|
||||
3. **Expand Windows 8.3 short names** (closes audit CRITICAL-5): на Windows если path содержит `~N` segment (tilde+digit, например `ADMINI~1`), вызвать `GetLongPathName` WinAPI через native binding (`node-ffi-napi` или PowerShell `[System.IO.Path]::GetFullPath` fallback). Без раскрытия `C:\Users\ADMINI~1\.claude\runtime\state.json` не match protected list `C:\Users\Administrator\.claude\runtime\*`. Если WinAPI fails (file не существует, permission) → fail-CLOSE.
|
||||
4. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved).
|
||||
5. Expand `~` через `os.homedir()` и env vars через `process.env`. **Unresolved `$VAR` / `%VAR%` после expansion → fail-CLOSE** (closes audit CRITICAL-3: `~/.claude/runtime/$UNDEFINED/state.json` literal `$UNDEFINED` segment не match glob, must block). Allowed env vars whitelist: `HOME` / `USERPROFILE` / `APPDATA` / `LOCALAPPDATA` — все остальные `$VAR` references → fail-CLOSE.
|
||||
6. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive).
|
||||
|
||||
Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих.
|
||||
Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих. EACCES при realpathSync → fail-CLOSE (assume protected).
|
||||
|
||||
### 3.2.0. Pre-flight smoke-tests — REQUIRED BEFORE §3.2 IMPLEMENTATION
|
||||
|
||||
@@ -254,7 +258,7 @@ Budget: 0.5h × 3 smoke-tests + verification = **1.5h total**. Without these —
|
||||
| File missing | == 0 | **Assume BLOCKED** (defensive, gate-crash possible). Parent обязан AskUser |
|
||||
| File read FS error | любое | fail-CLOSE per §3.3 |
|
||||
|
||||
4. **Cleanup:** parent gate удаляет block-file после чтения. Background cleanup script (cron / lefthook): orphans > 24h → delete (защита от parent gate crash после read но до delete).
|
||||
1. **Cleanup:** parent gate удаляет block-file после чтения. Background cleanup script (cron / lefthook): orphans > 24h → delete (защита от parent gate crash после read но до delete).
|
||||
|
||||
**Anti-forgery (3 защитных слоя — почему controller не подделает):**
|
||||
|
||||
@@ -376,6 +380,8 @@ Gate определяет одно из 4 поведений на основе s
|
||||
- Tool-call НЕ матчит → **block** с reason: «заказчик указал X через direct invocation, текущий tool — другое».
|
||||
- Read-only baseline (Read/Grep/Glob/LS/TodoWrite/AskUserQuestion) разрешены всегда.
|
||||
|
||||
**Source restriction (closes audit CRITICAL-1):** detector патернов **проверяется ТОЛЬКО против organic root user prompt в turn** — первое user message турна. AskUserQuestion chosen_label / chosen_text НЕ проходят через Поведение 1 detector — обрабатываются эксклюзивно через §4.5 answer parser. Контроллер контролирует labels (option text создаётся controller'ом), без этой restriction он может сконструировать опцию `делай subagent-driven-development` (exact registry match) → user выбирает → label попадает в transcript как user content → Поведение 1 fake-triggers → allow любого mutating tool. Implementation: gate использует transcript metadata который различает `user_message_type: "prompt"` vs `user_message_type: "askuser_answer"`. Pattern match только на тип `prompt`.
|
||||
|
||||
**Stale registry — mandatory AskUser.**
|
||||
|
||||
Если префикс-pattern matched (например `делай foo-bar`), но `foo-bar` **не найден** в registry — gate **обязан** заблокировать любой mutating tool до **mandatory AskUserQuestion** с явным форматом: «Пользователь указал skill `foo-bar`, но он не найден в реестре (возможно опечатка или переименован). Router рекомендует `<rec_node>`. Что делать?» с опциями:
|
||||
@@ -556,7 +562,7 @@ Gate инспектирует **content** Bash-команды (string в `tool_i
|
||||
| `ls` / `pwd` / `wc` / `head` / `tail` / `file` / `stat` | без `>` redirect, **path-deny overlay на ВСЕ path-args** |
|
||||
| `grep` / `egrep` / `fgrep` | без `--exec`, без `-l` после `>`, **path-deny overlay на ВСЕ path-args** |
|
||||
| `cat` / `less` / `more` | без `>` redirect, **path-deny overlay на ВСЕ path-args** |
|
||||
| `node` | без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader`** (preload arbitrary module before main script) |
|
||||
| `node` | **mandatory positional `<script-path>` arg** (НЕ REPL mode); без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader`** (preload arbitrary module before main script), **без `-i` / `--interactive` / `--inspect*`** (REPL/debugger ports); pipe receiver `node` без positional path arg = block (closes audit CRITICAL-8: `echo 'code' \| node` arbitrary execution) |
|
||||
| `npx vitest run ...` / `npx vitest --version` | любые run-args, не `--reporter` с suspicious patterns |
|
||||
| `npm test` / `npm run test` / `npm run lint:*` (read-only scripts) | любые |
|
||||
| `php artisan` (read-only commands только: `list`, `route:list`, `migrate:status`) | по whitelist |
|
||||
@@ -574,7 +580,8 @@ Gate инспектирует **content** Bash-команды (string в `tool_i
|
||||
- `npm install` / `npm update` / `npm remove` / `yarn add` / `pnpm add`
|
||||
- `npx claude-*` — запуск Claude изнутри Claude
|
||||
- `curl -X POST/PUT/DELETE/PATCH` / `wget --post-data` — outbound mutations
|
||||
- Любые pipe-команды (`|`) где receiver — mutating command
|
||||
- Любые pipe-команды (`|`) где receiver — mutating command OR `node` без positional script path (REPL via stdin)
|
||||
- `<` (input redirect) к file — закрывает audit PARTIAL-15: `wc -l < ~/.claude/runtime/router-state.json` обход path-deny overlay через input redirect (whitelist reading command + `<` bypass)
|
||||
|
||||
**Sub-shell / heredoc broad sweep — команды содержащие любой из следующих токенов hard-blocked всегда (даже после unlock):**
|
||||
|
||||
@@ -582,6 +589,7 @@ Gate инспектирует **content** Bash-команды (string в `tool_i
|
||||
- `$(...)` — POSIX command substitution
|
||||
- `<(...)` / `>(...)` — process substitution
|
||||
- `<<` / `<<-` — heredoc
|
||||
- `<<<` — here-string (closes audit CRITICAL-9: `node <<< "fs.writeFileSync(...)"` arbitrary execution bypass)
|
||||
|
||||
Некоторые legitimate uses (`git log $(echo HEAD)`) ломаются. Принимаем как acceptable friction для закрытия sub-shell bypass attack surface.
|
||||
|
||||
@@ -617,7 +625,9 @@ const SKILL_BASH_ALLOW = {
|
||||
|
||||
Маппинг — **в коде gate'а, не в Skill определении**. Skill не может расширить свой allow-list (это снова controller-writable signal). Расширение mapping — только через `tools/router-gate-decide.mjs` Edit + tests + PR review (а файл — в §3.1 protected paths).
|
||||
|
||||
**Path-deny overlay (§3.1) — applied to ALL path-args:** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json` — `tail` whitelisted, но `~/.claude/runtime/*` в protected → block. Все reading commands проверяются единообразно (не только `cat`).
|
||||
**Path-deny overlay (§3.1) — applied to ALL path-args (per-arg validation):** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: UNC strip + 8.3 expand + resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json` — `tail` whitelisted, но `~/.claude/runtime/*` в protected → block.
|
||||
|
||||
**Tokenizer per-arg requirement (closes audit CRITICAL-6):** при tokenize Bash-команды извлекать НЕ только first token (command), но ВСЕ subsequent args. Каждый arg который parse'ится как path (relative or absolute, не option flag начинающийся с `-`) проходит через path-deny check independently. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — `cat` whitelisted, **два** path-args, каждый normalize + проверяется → второй args block'ируется → команда block с reason про второй args. Все reading commands проверяются единообразно (не только `cat`).
|
||||
|
||||
**File-watcher для script execution — session-scoped, NOT turn-scoped:** gate tracks `edited_files_this_session` в `~/.claude/runtime/edited-files-<sess>.json` (session-wide persistence). Если в session был Edit/Write на `*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` файл, последующие `node X`, `npx vitest run X`, `python X` блокируются если target path в session-list. Reset происходит **только** на:
|
||||
|
||||
@@ -760,12 +770,14 @@ Protected paths должны быть live **до** того как соотве
|
||||
| Этап | Protected-up-to | Защиты live |
|
||||
|---|---|---|
|
||||
| 1 (pure decision module) | settings.json только | Gate в shadow-mode (logs, не блокирует) |
|
||||
| 1.4 (path normalization) | + `~/.claude/runtime/*` + `tools/enforce-*.mjs` | Hard-deny на gate state-файлы |
|
||||
| 2 (удаление 5 хуков + vocab) | + `docs/registry/nodes.yaml` + `tools/registry-load.mjs` + `tools/enforce-hook-helpers.mjs` + `tools/subagent-prompt-prefix.mjs` + `package.json` / `composer.json` + `gate-config.json` explicit | Полный §3.1 list active **до** включения router-gate (subagent-prompt-prefix должен быть в protected ДО первой записи new-schema inheritance файла) |
|
||||
| 1.4 (path normalization) | + `~/.claude/runtime/*` (включая `gate-config.json` — early protection, closes audit DOS D-1) + `tools/enforce-*.mjs` | Hard-deny на gate state-файлы; gate-config.json защищён ДО Этапа 2 чтобы tiny-budget patch attack невозможна |
|
||||
| 2 (удаление 5 хуков + vocab) | + `docs/registry/nodes.yaml` + `tools/registry-load.mjs` + `tools/enforce-hook-helpers.mjs` + `tools/subagent-prompt-prefix.mjs` + `package.json` / `composer.json` | Полный §3.1 list active **до** включения router-gate (subagent-prompt-prefix должен быть в protected ДО первой записи new-schema inheritance файла) |
|
||||
| 2.1 (subagent inheritance) | (без новых) | §3.2.0 smoke-tests run by user; если pass — env-based реализация активируется |
|
||||
| 3 (settings.json reg) | (без новых) | Router-gate switch from shadow to enforce-mode |
|
||||
| 2.2 (subagent constraints + block-file) | (без новых) | §3.4 constraints + side-channel block-file активны |
|
||||
| **2.3 (branch-switch AskUser-gate)** | (без новых) | **MUST complete BEFORE Этап 3** (closes audit CRITICAL-10): `enforce-branch-switch.mjs` rewrite under §4.5 git-pattern + askuser-decisions consume — иначе во время Этапа 3 enforce-mode OLD hook ещё принимает `BRANCH-SWITCH-CONFIRMED` controller-writable marker → S8 closure НЕ активна → dangerous git ops unprotected. Альтернатива: Этап 3 settings.json регистрация включает обновлённый branch-switch hook одной транзакцией |
|
||||
| 3 (settings.json reg) | (без новых) | Router-gate switch from shadow to enforce-mode. **Pre-condition:** Этап 2.3 deployed (S8 closure live). Если 2.3 не готов → НЕ переходить в enforce-mode |
|
||||
|
||||
**Принцип:** каждый Этап **сначала** расширяет protected list, **потом** активирует защиту что на него ссылается. Между Этапами — gap-окно когда controller теоретически может Edit'нуть будущий protected file. Mitigation: Этапы 1.4 → 2 → 2.1 → 3 идут одной сессией (несколько часов), не дни — gap-окно short.
|
||||
**Принцип:** каждый Этап **сначала** расширяет protected list, **потом** активирует защиту что на него ссылается. Между Этапами — gap-окно когда controller теоретически может Edit'нуть будущий protected file. Mitigation: Этапы 1.4 → 2 → 2.1 → 2.2 → 2.3 → 3 идут одной сессией (несколько часов), не дни — gap-окно short.
|
||||
|
||||
### Риски миграции
|
||||
|
||||
@@ -782,6 +794,18 @@ Protected paths должны быть live **до** того как соотве
|
||||
- **Logging granularity** — все decision events в одном файле или раздельно (router-gate / askuser-decisions / gate-errors)? Сейчас разделено на 3 файла.
|
||||
- **Bash whitelist completeness** — список в §5.1 покрывает основные read-only operations, но возможно есть пропуски (например `tree`, `du`, `find` с right args). Расширяется итеративно из реальной работы.
|
||||
|
||||
### Audit findings deferred to writing-plans (5 SHOULD fixes)
|
||||
|
||||
29.05.2026 adversarial audit ([docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 5 findings которые требуют **design decisions per fix** и решаются в writing-plans фазе (не trivial inline patch):
|
||||
|
||||
1. **SHOULD-FIX-1 — vitest globalSetup cross-session blind spot** (audit CRITICAL-2): Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state. Open question для plan: scan recursion limit (1-level transitive imports follow vs full transitive).
|
||||
2. **SHOULD-FIX-2 — `git format-patch -o` handling verification** (audit CRITICAL-7): row label «git log / git show / git diff / git blame / git format-patch» с exclusion `-o <path>` может покрывать обе команды (row-wide application) или только первую (per-command implementation). Verify parser implementation в Этапе 1.1 (Bash content parser); если per-command — добавить explicit `git format-patch` row с `-o` exclusion И блокировкой write в protected paths.
|
||||
3. **SHOULD-FIX-3 — `approved_action_pattern` mandatory file-path binding** (audit PARTIAL-1): §4.5 answer parser сейчас разрешает class-level unlock (`approved_tool: "Edit"` без path). Plan: каждое `approved_specific_tool` interpretation обязано извлечь `target_path` из user choice и сохранить как `approved_action_pattern`. Gate при mutating tool сравнивает `tool_input.file_path` против pattern (exact или prefix match). Open question: glob-style patterns (`docs/**`) разрешены или только exact?
|
||||
4. **SHOULD-FIX-4 — Chain reset фразы только в organic root prompt** (audit PARTIAL-2): §3 chain-state TTL detection. AskUser chosen_label содержащий `новая задача` сейчас тригернёт reset. Plan: интегрировать с CRITICAL-1 fix (transcript metadata `user_message_type`) — chain reset check только на `user_message_type: "prompt"`. Open question: что делать если controller пишет AskUserQuestion option с явным `"начать новую задачу"` — block через §4.7 question quality detector или allow с warning?
|
||||
5. **SHOULD-FIX-5 — chain-state malformed → fail-CLOSE** (audit PARTIAL-7): §3.3 failure mode table сейчас даёт «chain-state malformed → reset+warn» (fail-OPEN). Symmetric fix с router-state semantics. Open question: какой recovery path если chain-state легитимно corrupted (например crash mid-write) — manual reset через recovery §6 уровень 3 или auto-reset с явной user confirmation через AskUser?
|
||||
|
||||
Audit также identified 9 DOS findings + 15 INFO/acknowledged residuals — детали в audit report. Plan ОБЯЗАН адресовать минимум 5 DOS findings: D-2 (subagent success-marker file), D-3 (lefthook exit code parsing), D-7 (subagent gate reader-writer lock), D-8 (gate-config defaults fallback), D-9 (enforce-prompt-injection empty handling).
|
||||
|
||||
### Acceptable residuals (acknowledged design choices, не security holes)
|
||||
|
||||
- **Max 2 AskUserQuestion per turn limit** (§4.5): chain'ы с many mutating tools (>2 шагов с Edit/Write/Bash per ход) НЕ помещаются в один turn — заказчик вынужден re-prompt'ить или делить задачу. Acceptable UX-tradeoff — это design choice §4.5 (anti-fatigue), не дыра. Multi-step chains реально работают через переspawning context per next prompt. Документировать в README/onboarding.
|
||||
|
||||
Reference in New Issue
Block a user