История: v1 -> v2 (раунд 1 codex, 13 находок/8 блокеров) -> v3 (раунд 2,
10 новых) -> v4 (раунд 3, 5 TRUE-блокеров) -> v5 (раунд 4: ПРИНЯТ -
threat-model признан легитимным, закрыты 4 блокера-в-модели: legacy
incarnation fallback, approve FSM-гард, upstream-rejected cleanup, единый
предикат покрытия). Дизайн готов к коду. Ключевое
v4: threat-model ВЫРОВНЕН под системную модель claude-control (защита от
ОШИБОК/шума, НЕ от злонамеренного same-UID агента - принцип 1 плана; реальная
изоляция отложена СИСТЕМНО, не 7b-специфично); LLM-провайдер - доверенный
sink (как любой claude -p); candidate_id включает КОНТЕНТ (approve привязан
к просмотренному); durable RMW canon.yaml; commonpath-граница. Прежние v2/v3
правки в силе: project key sha1(realpath), durable incarnation-id, redaction
всех экспортных полей, disjoint+evidence валидатор, finite lifecycle.
Контракт stage-7 - design-2026-07-12-stage7-acceptor-role.md; канон -
feedback_canon_via_toolkit_log.
Статус реализации (2026-07-13): РЕАЛИЗОВАНО. Код: bin/claude-agent-harvest
(collect/propose/digest/approve/dismiss/reject/mark-applied), enabling-фикс Д0/Д2
в bin/claude-rc-agent (note->detail события, incarnation в control.json,
валидация spec.role), dispatch harvest в bin/claude-rc. Тесты:
tests/test-agent-harvest.sh (44 детерм + мок-LLM), enabling-ассерты в
tests/test-agent-cli.sh (80). Отклонение от дизайна: intended_root границы
Д11 = КОРЕНЬ ПРОЕКТА (не подкаталог - иначе симлинк подкаталога наружу проекта
тривиально прошёл бы проверку (1)); canon.yaml.upstream_pending entry = путь
брифа (уникален по cid, чистая идемпотентная очистка при upstream-rejected).
Adversarial код-раунд r5 (codex) + фиксы: 8 дефектов закрыто - (1) маскировка PEM-блока целиком + project path в брифе через mask(); (3) валидатор fail-closed на oversize (капы не срезают молча) + строго ОДИН top-level JSON-объект; (4) approve/reject сверяют candidate_id с хэшем контента (integrity при порче emitted); (5a) durable_append цикл до полной записи (POSIX short-write рвал ledger/emitted); (5b) reject чистит артефакты ДО смены статуса, fail-closed на битом canon, self-heal re-run; (5c) fsync каталога после unlink брифа; (6) canon RMW schema-валидация ВХОДА под canon.lock (не до)
- revalidate; (7b) incarnation hex32-guard в collector + validate_control; (8) run_llm fail-closed на returncode!=0; (9) install.sh ставит бинарник. Тесты дополнены (harvest 53). Планы/отчёты r5/r6 - docs/dev/adversarial-*-r5/r6.
Adversarial r6 (re-review) + остаток: 6/8 закрыто полностью, 3 остаточных пункта закрыты (harvest 58): reject fail-closed на fsync каталога брифа И на отсутствующем canon.yaml (die, статус не меняется, self-heal re-run); incarnation
- различаем ОТСУТСТВИЕ ключа (legacy fallback) от ПРИСУТСТВИЯ битого значения (агент пропускается, не переэмитит старые события), validate_control отвергает явный null; имя каталога агента валидируется NAME_RE + event_at через mask() (повреждённый registry-каталог token=SECRET не утечёт).
Adversarial r7 + 2 edge-case: durable-self-heal reject (fsync каталога брифа ВСЕГДА, даже когда файл уже удалён - иначе недолговечный unlink откатится) + incarnation: непарсибельный/отсутствующий control.json -> пропуск агента (None), НЕ legacy fallback (только dict без ключа -> agent_created). Тесты harvest 60. r8 - GO. Отчёты r5-r8 - docs/dev/adversarial-report-*.
Защита от ОШИБОК и низкокачественного сигнала, НЕ от злонамеренного агента. Это тот же системный контракт, что у всего claude-control (план, принцип 1: "границы v1 - от ошибок, не от злоумышленника; агент с Bash технически дотянется до чужих файлов; настоящая изоляция осознанно отложена"). 7b НЕ поднимает планку выше системной - иначе это ложная гарантия.
Что из этого следует (честно, без overclaim):
- Same-UID агент с Bash УЖЕ всевластен - может сам вызвать
harvest approve, писать вcanon.yaml,role/,rm -rfрепозиторий. Защищать 7b от ЭТОГО в v1 бессмысленно: это системная дыра, закрываемая только общей изоляцией (свой UID/контейнер на роль - отложено для ВСЕЙ системы, не для 7b). Поэтому 7b НЕ претендует на механическую защиту human gate от злонамеренного агента. - От чего 7b реально защищает: от ШУМА и низкокачественного сигнала, доезжающего до канона по ОШИБКЕ. Human gate с evidence - фильтр КАЧЕСТВА (спурьозная/слабая поправка -> человек видит сырьё -> отклоняет), не барьер против атак. Это ценно: без него мусорные поправки молча копятся в правила.
actor=operator- сигнал происхождения (обычно ставит операторский CLI), не крипто-доказательство. Consistent с системой: мы не аутентифицируем акторов в v1.- LLM-провайдер - доверенный разрешённый sink: candidate-pass шлёт
masked-ledger во внешний LLM ДО human review - ровно как каждый
claude -pв claude-control (приёмщик шлёт дифф, event-агент - payload). Отправка кода/данных провайдеру - принятый системный факт, не новый вектор 7b. Redaction (Д5) снижает утечку неумышленных секретов, не претендуя на анти-эксфильтрацию злонамеренного агента (тот и так шлёт что угодно куда угодно).
Итог: 7b проектируется как фильтр качества с человеком в петле в системной модели "ошибки, не злоумышленник". Механические детали ниже (маскировка, disjoint-валидация, no-follow, durable-записи) закрывают ОШИБКИ и порчу данных (крэш, симлинк по недосмотру, повреждённый файл), а не активную атаку same-UID агента - та вне scope v1 системно.
Операторские поправки (revise/reject) накапливаются per (проект, роль),
кластеризуются LLM'ом, предлагаются кандидатами - через дайджест, human
gate, канон. В role/ не пишем никогда (Д11). Источник v1 - только (a);
(b),(c) отложены (Д6).
- Verifiable delivery (билд, тестируемо): collector собирает посаженный корпус incarnation-safe с маскировкой; candidate-pass на корпусе с ДВУМЯ разными сутями даёт РОВНО два непересекающихся кластера, singletons не кластеризует, устойчив к перефразировке и к семантической инъекции (unknown/disjoint-валидация); идемпотентен между прогонами (candidate_id стабилен); approve атомарно-идемпотентен и не выходит за no-follow границу. Это проверяет РАБОТОСПОСОБНОСТЬ, не только plumbing.
- Outcome-метрика (эксплуатация, НЕ билд): реальный кластер доехал до канона и снял повтор. Не билд-тестируемо; копится во времени.
cmd_revise/cmd_accept_reject передают note в --detail control-cas.
append_event идёт под тем же flock и после durable_write(control.json) -
это НЕ атомарно (крэш между ними оставит control без события). Убираем
ложное "атомарно": контракт - best-effort аудит, harvester терпит
потерю (петля обучения, не бухгалтерия; пропущенная поправка = недобор
сигнала). События до Д0-фикса (без detail.note) - пропуск. Правка боевого
пути - только эти две команды.
- Collector (
claude-agent-harvest collect, детерминированный): events.jsonl всех агентов -> per-(проект,роль) ledger. Идемпотентен по incarnation-safe id, под per-key fcntl-flock, маскирует на экспорте. - Candidate-pass (
propose, bounded LLM §8.7 + валидация вывода Д7). - Digest + approve/reject/dismiss (человек): кандидаты с EVIDENCE -> дайджест; approve -> canon-протокол (Д9).
Запуск v1 - явная команда/cron, не автономный агент.
- Project key =
sha256(os.path.realpath(spec.project))[:16]- canonical (хэш РЕАЛЬНОГО абсолютного пути; symlink разрешён realpath'ом), не lossy slug (/srv/a-b/cи/srv/a/b-cв один slug - коллизия). sha256 (не sha1) - без криптотребования, но дешевле обосновать безколлизионность (codex-3: sha1 не инъективен). - Role: валидировать
spec.roleпри create тем же^[a-z][a-z0-9-]{0,30}[a-z0-9]$, что имя. Legacy-агенты (роль записана до валидации): collector при чтении проверяет роль тем же regex; невалидная -> агент ПРОПУСКАЕТСЯ (не в ledger), с log-строкой. Не ретро-ломаем. - Incarnation-id: control.json при create получает поле
"incarnation": "<secrets.token_hex(16)>"(CSPRNG, durable, уникален на инкарнацию). Пересозданный одноимённый агент = новый incarnation. Collector читает из ТЕКУЩЕГО control.json. Legacy-агенты этапов 1-7 БЕЗ поля (codex-4 блокер 1: create его не писал): collector НЕ падает и не требует поля - fallbackincarnation = control.get("incarnation") or <at первого события agent_created из events.jsonl>. Первоеagent_createddurable и уникально на инкарнацию (пересоздание -> новый agent_created с новымat), т.е. различает инкарнации и для legacy. Поле обязательно только для НОВЫХ агентов; чтение - опциональное с fallback. - Путь ledger на диске -
harvest/<project_key>/<role>/(project_key и role- валидированные/хэш, безопасны как компоненты пути).
id = sha256(json.dumps([project_key, role, incarnation, event_seq, masked_note], ensure_ascii=False))[:16].
incarnation(неevent_at) различает пересозданных одноимённых - секундная гранулярностьevent_atне годилась (два revise в секунду).masked_note(НЕ сырой) в хэше: сыройsha1(note)допускал словарную атаку на короткий секрет по ledger'у. Хэшируем ПОСЛЕ маскировки.- json.dumps списка (не
|-склейка): путь/текст может содержать|.
Threat-model: локальный events.jsonl - недоверенный вход, маскируем при ЧТЕНИИ в ledger (ledger - первый экспортный артефакт). Маскируется КАЖДОЕ поле, попадающее в ledger/digest/candidate/brief/emitted, включая:
- текст поправки (note);
- reject/dismiss
--reason(тоже операторский/недоверенный текст -> во внешний LLM и дайджест); - любые строки в brief (пишутся из уже-маскированного ledger). correction-id строится из masked-note (Д4) - не несёт сырья.
Паттерны: sk-…, gh[pousr]_…, AIza…, xox[baprs]-…, -----BEGIN,
Bearer …, (token|password|secret|api[_-]?key)=\S+, hex(>=32),
base64(>=40) -> ***. Агрессивно: потеря сигнала приемлема, утечка в
git-remote канона - нет. Остаточный риск (принят): секрет нестандартной
формы пробьёт маскировку. Смягчение: (1) human review видит кандидат перед
export; (2) источник - только короткий операторский текст (не транскрипт).
Не претендуем на полную DLP - claim'а "безопасный экспорт" в дизайне нет.
(b) транскрипт вырезан (fresh без session_id, handoff без границы миссии); (c) пост-accept diff отложен (нет надёжного трекинга ветки). v1 на (a).
Контракт приёмщика (пустой cwd no-follow, ВСЕ tools забанены, strict-mcp пустой, input-капы с явным отказом при переполнении, timeout SIGKILL-pgroup, no-clobber O_EXCL+link, fencing по (key, ledger-sha)).
Детерминированная валидация вывода (ПОСЛЕ LLM, reconciler-сторона) - кластер ОТБРАСЫВАЕТСЯ, если не выполнено ВСЁ:
correction_ids- непустое подмножество id поданного ledger (unknown -> отброс: инъекция "объедини реальные X,Y" мимо, т.к. проверяем membership, но НЕ семантику - семантику ловит human evidence, см. ниже);- кластеры disjoint: один id максимум в одном кластере (иначе весь вывод отброшен - LLM плохо разбил);
- >= MIN_CLUSTER (2) от >= 2 РАЗНЫХ incarnation ИЛИ agent - проверяется МЕХАНИЧЕСКИ по ledger-записям ids (не по слову LLM);
- ни один id не покрыт (единый предикат Д8: любой кандидат в {proposed, pending-upstream, applied, dismissed});
- поля bounded.
Семантическую честность группировки валидатор НЕ гарантирует (LLM мог слепить реальные, но несвязанные ids). Ловит - human gate с evidence: дайджест показывает рядом с кандидатом ПОЛНЫЕ masked-тексты всех его correction_ids. Человек видит, что суть реально общая, и отклоняет слепленное.
harvest/<key>/emitted.jsonl, append под key-flock. candidate_id = sha256(json.dumps([sorted(correction_ids), essence, why, how_to_apply])) -
включает КОНТЕНТ, не только ids (codex-3 блокер 2): человек одобряет
ИМЕННО просмотренный bundle. Тот же набор ids с другой essence (напр. после
upstream-rejected LLM переформулировал) = ДРУГОЙ candidate_id -> approve
старого id экспортирует ровно то, что было в дайджесте, не подменённый
payload. Кандидат в emitted.jsonl IMMUTABLE: propose НЕ перезаписывает
существующий candidate_id (append-only); повтор с тем же id - no-op.
approve дополнительно fail-closed сверяет переданный candidate_id с
существующей записью (нет записи -> отказ). Состояния и ПОКРЫТИЕ ids:
proposed- эмитировано, ждёт человека. Покрывает (non-terminal).pending-upstream- approve прошёл, кандидат в upstream-pending. Покрывает (non-terminal, но с ВЫХОДАМИ ниже).applied- ТЕРМИНАЛ: канон реально принял правило (человек ставитharvest mark-appliedпосле заноса toolkit-сессией, либо canon-sync детектит файл). Покрывает НАВСЕГДА - цель достигнута.dismissed- ТЕРМИНАЛ: человек на дайджесте сказал "не тянет на правило". Покрывает НАВСЕГДА (не долбить).dismiss --reason.upstream-rejected- канон-ревьюер отклонил при заносе. Переход идемпотентно ЧИСТИТ артефакты (codex-4 блокер 3): удаляет/архивирует brief изupstream-pendingИ убирает запись изcanon.yaml.upstream_pending(durable RMW, как approve) - иначе отклонённый кандидат продолжает числиться pending для canon-workflow. Только ПОСЛЕ чистки ids возвращаются в пул, essence+reason -> в промпт candidate-pass ("уже отклонено каноном").
Единый предикат покрытия (codex-4 блокер 4): id "покрыт" и исключён из
входа candidate-pass (Д7), если на него ссылается кандидат в состоянии
{proposed, pending-upstream, applied, dismissed} - т.е. ВСЕ, кроме
upstream-rejected. Только upstream-rejected возвращает ids в пул. Так
applied/dismissed покрывают НАВСЕГДА (терминалы), и их ids не всплывут
в новом кластере с другим набором. Валидатор Д7 и Д8 используют ОДИН этот
предикат (не рассинхронены).
Конечность: каждая поправка кончает в applied (стала правилом) или
dismissed (человек сказал нет) - оба покрывают навсегда. upstream-rejected
возвращает в пул с memory сути -> переформулированный повтор всплывёт
ограниченно, человек dismiss'ит -> терминал.
harvest approve <key> <candidate_id> fail-closed сверяет candidate_id с
записью emitted (нет -> отказ) И статус ∈ {proposed, pending-upstream}
(codex-4 блокер 2: только свежий proposed или recovery-повтор pending-
upstream; из dismissed/applied/upstream-rejected approve ОТКАЗЫВАЕТ -
иначе approve после dismiss создаст brief, а после applied воскресит
удалённое sync'ом). Затем 3 записи. Каждая independently
durable (codex-3 блокер 5) И идемпотентна -> re-run = crash-recovery:
- brief-файл
<project>/toolkit-log/upstream-pending/<key>-<cid>.md-durable_write(tmp O_EXCL + write-all + fsync + rename + fsync каталога), overwrite тем же контентом (детерминирован по candidate); - RMW
<project>/.claude/canon.yaml: parse+schema-validate старого -> add-if-absent вupstream_pending->durable_writeнового (полная цепь) -> re-parse-validate. Под.claude/.canon.lockflock (общий с /canon-sync). Повреждённый/невалидный старый canon.yaml -> approve ОТКАЗЫВАЕТ (не пишет поверх мусора), просит починить - не усугубляет. - emitted status ->
pending-upstream(durable append).
Крэш на любом шаге: durable_write гарантирует, что каждый файл либо старый
целый, либо новый целый (rename атомарен, fsync до/после) - НЕ обрезанный.
Повторный approve <cid> доделывает недостающее (проверяет brief, запись в
canon.yaml, статус - до-исполняет идемпотентно). Остаточный риск
(принят): три файла не образуют ОБЩУЮ транзакцию - между шагами наблюдаемо
brief без canon-записи; но каждый durable, re-run сходится, а применение
канона всё равно за человеком в toolkit-клоне. Общий ACID здесь
over-engineering для human-gated шага; per-file durability - обязателен и
обеспечен.
Per-key fcntl-flock harvest/<key>/.lock на collect И propose И approve
(fcntl, не bash flock - Mac). Чтение events.jsonl терпит рваный хвост
(json.loads построчно, пропуск). ledger/emitted append durable под flock.
Гонка approve/propose (codex-2 находка 8): approve и propose держат ОДИН
key-flock -> propose не прочитает ledger, пока approve меняет emitted, и
наоборот. Кандидат, ставший pending при approve, покрывает ids до
следующего propose (который уже под тем же локом увидит покрытие).
- LLM без write-тулов (Д7) - физически не пишет.
- Детерминированный код пишет в фиксированный allowlist корней. Перед
КАЖДОЙ записью в output-корень (
toolkit-log/upstream-pending,canon.yaml, дайджест): resolveos.path.realpath(target)и assert черезos.path.commonpath(не строковый префикс - codex-3 блокер 3:/a/agentsстрокой матчит/a/agents-foo): (1)commonpath([target, намеренный_корень]) == намеренный_корень; (2)commonpath([target, ~/.claude-control/agents]) != agents(target НЕ внутри реестра/role). Финальная записьO_NOFOLLOWна файле + tmp+rename. Symlinkupstream-pending -> agents/<name>/role/не даёт записи: realpath попадёт в agents/ -> assert падает.agents/<name>/role/как write-путь в коде ОТСУТСТВУЕТ вовсе. - Остаточный риск (принят, вне scope v1): статический realpath+O_NOFOLLOW
ловит симлинк-по-недосмотру (ошибка), но НЕ race-free против злонамеренной
подмены ancestor-каталога между assert и open (TOCTOU). Это защита от
АТАКИ same-UID процесса - вне threat-model v1 (см. Threat-model: агент и
так всевластен). Race-free
openat(O_DIRECTORY|O_NOFOLLOW)покомпонентно от trusted-fd - в фазу общей изоляции системы. - Инъекция через ledger не даёт записи: LLM без рук; его вывод -> валидация -> только emitted/digest.
Детерминированный (без сети):
- Д0:
revise --note X-> событие сdetail.note=X;reject --reason Y->detail.note=Y; accept-нота НЕ порождает correction. - collect: 3 агента одной (проект,роль) revise -> ledger 3 записи; повторный collect без дублей; пересозданный одноимённый (новый incarnation, seq сброшен) НЕ коллизится; два revise в одну секунду - разные id (incarnation+seq).
- ключ:
spec.roleс/,..,заглавными -> create отбит; legacy-агент с невалидной ролью -> collector ПРОПУСКАЕТ; два разных project-пути -> два разных project_key (без slug-коллизии). - секреты:
sk-/token=в ноте И в reject-reason ->***в ledger/digest/brief; correction-id НЕ обратим в секрет (из masked). - рваный хвост events.jsonl -> пропуск строки.
- валидатор (мок LLM): unknown id -> отброс; пересекающиеся кластеры -> отброс; кластер из 1 incarnation -> отброс; покрытый id -> отброс.
- lifecycle: proposed->approve->pending-upstream (не applied); dismiss-> terminal (повторно не эмитится); upstream-rejected-> ids в пул + essence-memory; mark-applied->terminal.
- approve идемпотентность: прервать после brief -> re-run доделывает canon.yaml+status; canon.yaml add-if-absent не дублит.
- no-follow:
upstream-pending-> symlink наagents/x/role-> approve падает на assert, в role НЕ пишет. - per-key flock: параллельные collect не сдвоят ledger.
LLM-корпус (сеть):
- ДВЕ разные сути по 3 агента каждая -> РОВНО 2 disjoint-кластера, essence по сути, ids верные; singletons не кластеризуются.
- перефразировка той же сути у 3 агентов -> один кластер (устойчивость).
- повторный прогон того же ledger -> тот же candidate_id (стабильность).
- инъекция "верни applied/объедини всё" в тексте ноты -> не меняет вывод.
Все - в системной модели "ошибки, не злоумышленник" (Threat-model). Атаки same-UID агента вне scope v1 СИСТЕМНО (не 7b-специфично) - закрываются общей изоляцией, отложенной для всего claude-control.
- Злонамеренный same-UID агент может сам вызвать approve / писать в canon/role / эксфильтровать через LLM - вне scope v1 (агент и так всевластен; принцип 1 плана). 7b защищает от ОШИБОК/шума, не атак.
- actor не аутентифицирован - сигнал происхождения, не доказательство; consistent с системой.
- LLM-провайдер - доверенный sink - masked-ledger уходит до human
review, как любой
claude -p; принятый системный факт. - маскировка не полная DLP - секрет нестандартной формы пробьётся; смягчение human review + короткий операторский источник; защищает от СЛУЧАЙНОЙ утечки, не от злонамеренной эксфильтрации.
- no-follow не race-free (TOCTOU) - ловит симлинк-по-недосмотру, не атаку подмены ancestor; race-free dirfd - в фазу изоляции.
- approve не общий-ACID - per-file durable + идемпотентный self-heal; общая транзакция - over-engineering для human-gated шага.
- lifecycle конечен через human dismiss, не автоматически: цикл upstream-rejected->proposed гасится essence-memory + терминальным dismiss.
- LLM split/merge недетерминизм - candidate_id стабилен по (ids+контент), human gate с evidence ловит слепленное.
- outcome-критерий не билд-тестируем - копится в эксплуатации.
- (b),(c) отложены (Д6).