vetto 0.2.20

Daemon-less sandbox + security layer for AI coding agents (Landlock/Seatbelt, TUI statusline, post-session audit reports)
Documentation
# Process-Tree Containment + Guaranteed Cleanup — Architecture Proposal (Prompt 05)

Статус: proposal, НЕ production-код. Ничего из нижеописанного не реализовано.
Скоуп: `src/sandbox/{handle.rs,linux/proctrack.rs,macOS/pdeath_watch.rs,windows/job_object.rs}`
+ связанные модули. `src/verify_ng/` — чужой скоуп, не трогаем.

## 1. Current process lifecycle (как есть сейчас)

### Linux FULL (`src/sandbox/linux/mod.rs`: `spawn_full`, `child_full`, `child_b`)

```text
vetto (host ns, держит alive_w)
└─ S (USER+MOUNT+IPC+NET ns; alive_r; err pipe)
   ├─ R (relay, только allowlist/strict; вне pidns, вне Landlock)
   └─ B (PID 1 нового pidns; PDEATHSIG; приватный /proc; Landlock; seccomp)
      └─ C (agent; PDEATHSIG+getppid-check; seccomp; execve; пишет 'R')
```

Teardown FULL: `KillStrategy::PidNsPipe` (`src/sandbox/handle.rs:45-49`) —
drop `alive_w` → EOF на `alive_r` → B делает `kill(-1, SIGKILL)` внутри pidns,
затем reaps всех. Crash-resilience ядра: смерть vetto = EOF = тот же путь.
B также ловит смерть C (`waitpid(-1)`) и убивает ns. Зомби жнёт B как PID 1.
Оценка: Strong.

### Linux FS-ONLY / Seccomp (`spawn_fs_only`, `spawn_seccomp_only`)

Один fork, child = agent. `setpgid(0,0)` (или `setsid` в PTY), Landlock,
seccomp, PDEATHSIG + getppid-check. Teardown (`handle.rs:175-195`):
`kill(-pgid)` + `kill(pid)` + `proctrack::sweep_reparented` (только Linux,
`sweep=true`). Перед fork — `set_subreaper` (`src/multi/isolation.rs:154`),
после fork — `arm_exit_sweep` (atexit, т.к. `supervise` выходит через
`std::process::exit`, `Drop` не бежит; `proctrack.rs:41-70`). Стрипы 360°:
утечка сабреапера, выход из асьминхронного окна, выполнение вне peload-sweep.
Оценка: Partial/best-effort, честно задокументировано в коде.

### macOS (`src/sandbox/macos/mod.rs`, `pdeath_watch.rs`)

Один fork, `setpgid(0,0)`, Seatbelt, execve. Teardown:
`KillStrategy::ProcessGroup{pid,pgid,sweep:false}` — только `kill(-pgid)` +
`kill(pid)`. `sweep=false` явно. Плюс `pdeath_watch::spawn` из родителя:
kqueue NOTE_EXIT на vetto → SIGKILL агента (закрывает «vetto SIGKILLed»
окно; grandchildren НЕ покрыты). Оценка: Partial.

### Windows (`src/sandbox/windows/mod.rs`, `job_object.rs`)

`Experimental_CreateProcessInSandbox` с `CREATE_SUSPENDED` →
`create_kill_on_close_job` → `AssignProcessToJobObject` →
`ResumeThread`. Teardown: drop job (kill-on-close) + drop process handle
(`handle.rs:196-203`). `job_object.rs:104-106` — `KILL_ON_JOB_CLOSE |
DIE_ON_UNHANDLED_EXCEPTION`, breakaway явно не разрешён (комментарий).
`terminate()` вызывается из `Drop`, `wait_with_timeout` (`main.rs:1512`),
TUI-quit, verify_ng killer. Оценка: Strong при условии, что assignment
успел до ResumeThread (выполняется: suspended → assign → resume — правильно).

### Timeout/supervision (`src/main.rs`)

`wait_with_timeout` (`main.rs:1512-1536`): только `--tui=none`; TUI-режимы
таймаут игнорируют (предупреждение `main.rs:1216-1223`). Немедленный
`terminate()` по дедлайну, без graceful-фазы. `install_sigint_forwarder`
(`main.rs:1704`): SIGINT/SIGTERM → forward (FS-ONLY: всей группе). Ctrl+C в
TUI: `p` = pause, `q`+`y` = terminate. Daemon (`src/daemon/registry.rs:137`):
`stop_session` = `child.kill()` + `wait()` на vetto-child (не на дерево
напрямую; дерево чистит teardown самого vetto — ок для FULL/Job, best-effort
иначе). `vetto kill` (`src/cli/kill.rs:33-89`): SIGTERM → 2с → SIGKILL,
по группе с fallback на pid.

## 2. Threat model

Агент = произвольный код от uid пользователя внутри sandbox. Цели агента:
пережить termination boundary (double-fork, `setsid`, daemonize,
` KillStrategy::ProcessGroup`,
nohup/`&`, at/cron/systemd --user, сокет/файл как якорь, respawn по таймеру,
рождение ребёнка после смерти родителя, fork-bomb как DoS на sweep).
Граница доверия: ядро/ОС — доверены; vetto-родитель — доверен, но убиваем
(SIGKILL); всё внутри sandbox — враждебно. Non-goals (из threat-model.md):
враждебное ядро, root, другой uid, prompt-injection внутри разрешённого,
записи в разрешённый `$PROJECT`, side-channels.

Приоритет gaps по exploitability × impact:
1. FS-ONLY/macOS setsid-grandchild вне sweep-бюджета — детерминированный escape.
2. Supervisor-crash + delayed/respawn child (cron/at/systemd --user вне pidns;
   Job вне job; macOS вне группы).
3. Creation race (assign после первого исполнения) — частично есть на Windows,
   нет на Unix (нет раннего ownership вообще).
4. False-positive cleanup checks (ниже §12).
5. Resource-exhaustion как containment-атака (fork-bomb против sweep/observer).

## 3. Security invariants

- **I1 Termination closure.** Ни один процесс session не переживает
  termination boundary в неконтролируемом состоянии. Strong tier: доказывается
  ядром/Job; best-effort tier: утверждается только после успешной
  post-cleanup assertion (§10), иначе — FAIL с явным residual.
- **I2 Early ownership.** Владение потомком формируется до его первой
  исполняемой инструкции (suspended-create+assign; pidns-fork; seccomp при
  рождении невозможен — фиксировать как Unsupported и компенсировать).
- **I3 Descendant closure.** Владение транзитивно: ребёнок владельца —
  владелец. Механизм обязан покрывать внуков без их кооперации.
- **I4 Crash resilience.** Смерть супервизора в любой точке обязана вести к
  смерти дерева без кода супервизора (kernel/Job-alive-pipe/watchdog).
- **I5 Cleanup authority.** Единственный владелец teardown — держатель
  `SandboxHandle` (как уже в verify_ng/killer.rs: single-owner); `Drop` =
  terminate; путь `std::process::exit` обязан иметь atexit-эквивалент.
- **I6 No false-clean.** Cleanup успешен только при доказанном отсутствии
  session-owned descendants (pidns-empty / job-empty / orphan-scan-empty +
  cgroup-empty). Пустой `waitpid(root)` ≠ чистое дерево.
- **I7 Fail-closed degradation.** Потеря примитива (subreaper prctl, kqueue,
  cgroup mount) — громкий warn + понижение tier, никогда молча.

## 4. Process ownership model

Владелец = (session_id, механизм, граница). Граница Strong: pidns (Linux
FULL), Job (Windows). Граница best-effort: pgid + subreaper-adoption
(FS-ONLY), pgid + watchdog (macOS). Потомок принадлежит сессии, если создан
внутри границы после её установки. FS-ONLY/macOS: принадлежность потомка,
рождённого между fork и установкой фильтров, недоказуема — окно фиксируется
как residual, а не игнорируется. Передача владения (daemon `stop_session`,
`vetto kill`) — только через владельца handle, не через сырой pid (сегодня
`kill.rs` шлёт сигналы по pid — gap, см. §12 п.6).

## 5. Process creation protocol

Требуемый порядок (target state):
1. Создать границу ДО первой инструкции: pidns-fork / suspended+assign /
   pgid+subreaper-arm.
2. Запретить исполнение до готовности границы: Windows `CREATE_SUSPENDED`   Strong (уже есть); Unix fork не даёт suspended — компенсировать
   PDEATHSIG+getppid-check ДО unshare/mount (уже) + fail-closed setup pipe
   с таймаутом (уже, 30с) + никогда не возвращать handle до байта 'R' (уже).
3. Агент исполняется только после irreversible фильтров (seccomp/Landlock/
   Seatbelt) — уже.
4. Handle возвращается только после readiness; любой сбой — kill_and_reap,
   не leak.

## 6. Linux design

**Strict (FULL) — Strong, уже есть, держать:**
pidns + B(PID 1, reaper, alive-pipe) + PDEATHSIG + kill(-1) + cgroup.kill.
Добавить (proposal): pidfd на B/S для точного ожидания без PID-reuse
(`pidfd_open` + `poll`), `cgroup.kill` как второй kill-путь в
`CgroupHandle::cleanup` уже частично есть — поднять до обязательного шага
state machine; `SO_PEERCRED`-проверки не нужны.

**FS-ONLY/Seccomp — Partial, потолок без pidns:**
subreaper + kill(-pgid) + bounded sweep — текущий дизайн верный, но:
(a) sweep-бюджет 2с и один проход — поднять до многоитерационного с
эскалацией и финальной assertion (§10); (b) subreaper-fail сегодня только
warn — добавить явное понижение assurance в отчёт/doctor; (c) cgroup
(`pids.max` + `cgroup.kill`/`cgroup.procs` freeze) — поднять из
best-effort-опции в обязательный второй рубеж, когда иерархия доступна;
(d) `RLIMIT_NPROC` уже ставится pre-exec и наследуется — держать как
anti-fork-bomb рубеж, задокументировать связку со sweep-бюджетом.
Запрет `setsid/setpgid/daemon` через seccomp — **Unsupported**: ломает
штатные рантаймы (shell job-control, setsid в PTY-пути самого vetto),
фильтр неотличит «плохой» setsid от легитимного; containment обязан быть
механическим ( adoption+sweep), а не запретительным. Фиксируем как осознанный
отказ.

**Примитивы:** strict = pidns + alive-pipe + B-reaper + pidfd + cgroup.kill
(+ seccomp/Landlock как FS/нет-изоляция, не proctree). Optional = subreaper
(нужен только вне pidns), orphan-scan (верификация, не убийство Strong).

## 7. Windows design

Текущий suspended→assign→resume — правильный ответ на creation race; держать
как инвариант с регресс-тестом. Дополнительно (proposal):
- nested jobs: агент не может вложить своё job с `BREAKAWAY_OK`, т.к. мы не
  ставим `JOB_OBJECT_LIMIT_BREAKAWAY_OK` и члены job не могут выйти без
  этого флага — держать + явно assert в probe; `kill_contract()` уже фиксирует
  контракт — расширить его проверкой в `probe_job_object`.
- handle inheritance: `inheritHandles=FALSE` уже; держать; captured-stdio путь
  (`handle.rs:26-29`) открывать только с явным HANDLE-list, иначе fail-closed.
- restricted token/AppContainer: ownership не дают, но сужают «чем внук может
  стать» (no admin, low integrity) — держать как defense-in-depth, не как
  cleanup-механизм.
- limits: `ActiveProcessLimit` + `JobMemoryLimit` (уже в
  `create_kill_on_close_job`) — обязательный anti-fork-bomb рубеж; добавить
  post-cleanup assertion через Job accounting (запрос состояния job после
  close-handle невозможен — assertion делать ДО drop: перечислить процессы
  job, TerminateJobObject при остатке, затем close).
- race окно между return experimental API и assign: покрыто suspended; если API
  вернул уже-running процесс — считать нарушением контракта, Terminate+fail.

## 8. macOS native design

Потолок платформы без pidns/subreaper: pgid + watchdog. Proposal:
- держать `pdeath_watch` (родительский fork, kqueue+poll-fallback) — Strong
  для корня, Partial для дерева;
- добавить subreaper-аналога НЕТ (Darwin не даёт) — фиксировать Unsupported;
- добавить orphan-scan assertion после `terminate()` (перебор `sysctl
  KERN_PROC` по ppid-цепочке от root pid + kill остатка + повтор) как
  portable-аналог sweep с тем же бюджетом; сегодня `sweep=false` и никакой
  проверки — главный macOS-gap;
- Seatbelt наследование держать (потомки в профиле), но не выдавать за
  cleanup;
- launchd/`open -b`/cron-escape: агент внутри Seatbelt не может положить
  persistent unit вне разрешённых путей записи — держать связку «FS-запрет =
  persistence-запрет», задокументировать.
- Endpoint Security: только notify/observe, не AUTH-allow/deny (уже честно);
  не предлагать как cleanup-механизм без entitlement — Unsupported.

## 9. VM design (Linux VM backend / win-sandbox)

VM даёт Strong teardown бесплатно (уничтожение инстанса). Требования к
дизайну: (a) termination boundary = граница VM, не внутренний pid; (b) агент
никогда не держит handle наружу VM; (c) снапшот/откат не воскрешает «убитое»
дерево как running; (d) host-side watchdog на гипервизор-объект на случай
смерти vetto; (e) exfiltration через shared folders — вне proctree, но
cleanup обязан закрывать гостевые mount-ручки до teardown. win-sandbox
модуль уже генерирует .wsb — добавить в него kill-контракт (текущий файл —
только спек, без lifecycle).

## 10. Cleanup state machine (target)

```text
EXIT_REQUEST ──► GRACEFUL (SIGTERM/job-soft, T_grace≈2с; FS-ONLY: SIGTERM группе)
   │ alive?            │ exited → VERIFY
   ▼                   ▼ timeout
ESCALATE (SIGKILL / kill(-1) / TerminateJobObject / drop-job; cgroup.kill+freeze)
   │                   ▼
   └──────────────► VERIFY (descendant-proof: pidns-empty / job-empty /
                   │         orphan-scan-empty + cgroup.procs-empty)
                   ├── CLEAN → post-cleanup assertion → SUCCESS
                   └── RESIDUE ──► RETRY-SWEEP (bounded, ≤N, backoff) ──► CLEAN?
                                     │ fail → FAIL-CLOSED: FAIL + residual report
                                     │ (pids, cmdlines, tier, механизм), non-zero exit,
                                     │ запись в registry, никаких «успех по waitpid»
```

Правила: graceful — только там, где есть кому ловить (Unix SIGTERM; Job —
TerminateJobObject сразу, graceful опционален); `waitpid(root)` — необходимое,
но не достаточное условие; каждый переход с дедлайном; общий бюджет teardown
ограничен (sweep-DoS через fork-bomb обязан упираться в `pids.max`+бюджет, а
не в бесконечность); сегодняшнего graceful вообще нет (`terminate()` =
сразу SIGKILL) — это честно для adversarial, но §17 вводит опциональный
GRACEFUL как настраиваемый, default = immediate-SIGKILL.

## 11. Crash resilience

| Сценарий | FULL | FS-ONLY | macOS | Windows Job |
|---|---|---|---|---|
| vetto SIGKILL | alive-pipe EOF → B kill(-1) — Strong | atexit НЕ бежит; остаётся kill(-pgid)? нет — **gap**: только PDEATHSIG корня; внуки-сироты вне subreaper-окна выживают | watchdog kqueue ловит — корень; внуки — gap | job kill-on-close — Strong |
| vetto SIGTERM | forwarder → нормальный teardown | то же | то же | teardown через handler |
| B/supervisor crash (внутри) | kernel reaps pidns — Strong | N/A (нет B) | watchdog двусторонний (следит и агента) | job держит |
| host reboot/power | всё умирает | всё умирает | всё умирает | всё умирает |
| daemon crash (`stop_session` недожал) | дерево уже убито teardown vetto-child | best-effort остаток | best-effort остаток | job держит |

Главный crash-gap: **FS-ONLY + SIGKILL(vetto)**: `arm_exit_sweep` (atexit) не
исполняется, `terminate()` не вызывается, subreaper-сироты остаются, т.к.
усыновивший их родитель (vetto) мёртв и sweep некому бежать; PDEATHSIG покрывает
только прямого ребёнка. Закрытие: pidfd+subreaper в daemon-стороже ИЛИ
документированный downgrade assurance + `vetto kill --hung` как внешний
sweeper (уже есть сканер, но он шлёт сигналы по pid, не sweep сирот —
предлагается расширить, §17).

## 12. Race analysis + false-positive cleanup checks

R1. fork→PDEATHSIG окно: закрыто getppid-check (везде) + alive-pipe в FULL.
R2. CreateProcess→assign: закрыто suspended (Windows). Unix-аналога нет —
    компенсируется §5 п.2.
R3. kill(-pgid)→setsid внука: reparent+sweep; окно = sweep-бюджет (residual).
R4. waitpid(root)→рождение ребёнка после смерти родителя (delayed child,
    `sleep 1 && evil &`): sweep ловит только усыновлённых к моменту скана;
    каскад глубже бюджета — escape. Лечится VERIFY-циклом (§10), не одним
    проходом.
R5. PID reuse: `kill(pid)`/`/proc/pid` после reap может бить по чужому;
    sweep исключает root_pid, но нерутовые пиды переиспользуемы между сканами.
    Лечится pidfd (`pidfd_open` + `pidfd_send_signal`) и стартовым временем
    из `/proc` (как уже в visibility `LifecycleToken`) — proposal.
R6. `vetto kill <pid>` по группе `-pid` при неверном pgid бьёт по чужой
    группе (fail-open наружу!). Предложение: kill только через registry +
    верификация `start_time`/cmdline перед сигналом.
R7. Daemon `stop_session`: `child.kill()` убивает vetto-child; если это был
    SIGKILL при FS-ONLY — см. §11. Предложение: graceful SIGTERM vetto-child
    с таймаутом вместо kill, чтобы его teardown успел отработать.

False-positive checks (супервизор думает «чисто», а сущность жива):
F1. `waitpid(root)==exited` при живых detached-внуках (главный кейс промпта).
F2. `kill(-pgid)==ESRCH` — группы уже нет, а setsid-внуки живы.
F3. `pgrep -f marker` пуст — процесс переименовался (`exec -a`, argv[0]
    перезапись) или cmdline недоступен (зомби/kernel-thread вид).
F4. orphan-scan пуст, т.к. ребёнок ещё не переродился (родитель-зомби не
    подождён) — scan до `root_settled` обязателен (в `sweep_reparented` уже
    есть `root_settled` — держать).
F5. cgroup.procs пуст, т.к. процесс в D-state (uninterruptible) — kill
    отложен ядром; VERIFY обязан отличать «пусто» от «D-state остаток».
F6. Job handle закрыт → «дерево убито», но nested-breakaway процесс вне job
    (если флаг когда-либо ослабят) — assertion ДО close обязательна.
F7. macOS `kill(agent)==ESRCH`, а внук в другой группе жив — сегодня вообще
    не проверяется.
F8. Redteam `test_setsid_escape` (`redteam.rs:85-129`): проверяет ФЛАГ
    subreaper/pidns, а не факт смерти потомка — self-attested Pass, классический
    false-positive. Заменить execution-пробой (§15).

## 13. Resource containment (связка с proctree)

CPU/mem (`limits.rs` RLIMIT_* + `cgroup.rs` cpu.max/memory.max) — наследуются,
снижают blast radius, но не убивают дерево. `pids.max` (cgroup) +
`RLIMIT_NPROC` (уже pre-exec) — прямой anti-fork-bomb рубеж для sweep-бюджета:
предложение сделать `pids.max` обязательным, когда cgroup доступен
(сегодня опционален и молча None). Open files (`RLIMIT_NOFILE`), fsize —
наследуются; сокеты — seccomp-семейства + netns/Job/AppContainer; IPC —
разделённые IPC-ns (FULL) vs общий host IPC (FS-ONLY/macOS — shm как
covert-канал между «убитыми» и живыми, учесть в VERIFY). FD-exfiltration:
передача дескрипторов наружу дерева (SCM_RIGHTS, pidfd_getfd — уже blocked)
не воскрешает процесс, но удерживает пайпы открытыми → ложное «дерево висит»
(EOF не приходит); B уже закрывает stdio-копии (`close_stdio_fds`), watchdog
чистит все fd — держать паттерн везде.

## 14. Verification strategy

Уровни: unit (парсинг `/proc`, BPF-офсеты — уже есть) → execution-probes
(реальный setsid-внук, см. `linux_orphans.rs:100` — уже execution, держать как
образец) → adversarial suite (§15) → post-cleanup assertion в проде (§10) →
`doctor --probe` отражает assurance-tier честно. Наблюдение
(visibility-poller, audit, ETW, FSEvents) — только evidence, никогда вход
VERIFY-решения (уже принцип threat-model.md:65-70 — держать).

## 15. Adversarial tests (минимум промпта → файлы)

Каждый кейс: spawn → teardown (graceful + SIGKILL-vetto) → assertion
«ноль session-owned». Матрица tier × кейс: FULL/Job = must-pass; FS-ONLY/macOS
= must-report (pass либо FAIL+residual, молчаливый pass запрещён).
Кейсы: double-fork; daemonize (`daemon()`); setsid; background shell
(`sh -c 'sleep 9999 &'`); spawn-loop/fork-bomb (под `pids.max`); delayed
child (`(sleep 3; evil)&` при teardown на t=0); child-after-parent-exit;
socket-open-then-detach (файл/сокет-якорь + setsid); files-kept-open
(EOF-hang проверка); respawn-loop (`while true; do evil; done` + watcher).
Плюс: exec-rename evasion (F3), PID-reuse окно (R5), TUI-timeout-ignore
проверка. Исполнение вне дерева запретов: at/cron/systemd --user только там,
где FS позволяет (expected-residual, не flake).

## 16. Repo/module changes (каждое изменение → файлы)

| # | Изменение | Файлы/модули | Tier | Класс |
|---|---|---|---|---|
| 1 | Cleanup state machine: graceful→escalate→verify→assert (`CleanupReport`) | `src/sandbox/handle.rs` (расширить `terminate`), новый `src/sandbox/cleanup.rs`, `src/main.rs` (`wait_with_timeout`), `src/tui/{full,statusline}.rs` | все | Partial→Strong-verify |
| 2 | Orphan-scan VERIFY-цикл + residual-отчёт вместо одного sweep | `src/sandbox/linux/proctrack.rs` (расширить `sweep_reparented`), `src/sandbox/cleanup.rs` | FS-ONLY/Seccomp | Partial (потолок) |
| 3 | pidfd-ожидание/сигналы против PID-reuse | `src/sandbox/linux/proctrack.rs`, `src/sandbox/linux/mod.rs` (хранить pidfd в handle) | Linux | Strong |
| 4 | cgroup.kill+freeze как обязательный второй рубеж + `pids.max` default при доступной иерархии | `src/sandbox/linux/cgroup.rs`, `src/sandbox/handle.rs`, `src/sandbox/linux/mod.rs` | Linux | Partial→Stronger |
| 5 | macOS orphan-scan assertion после terminate | `src/sandbox/macos/mod.rs`, новый `src/sandbox/macos/orphan_scan.rs`, `src/sandbox/handle.rs` (`sweep:true` для macOS с новым сканером) | macOS | Partial (потолок) |
| 6 | Windows assertion ДО drop job (перечень процессов, Terminate при остатке) + nested-breakaway assert в probe | `src/sandbox/windows/mod.rs`, `src/sandbox/windows/job_object.rs`, `src/sandbox/handle.rs` | Windows | Strong |
| 7 | Crash-path: graceful SIGTERM daemon-child вместо kill; внешний sweeper для FS-ONLY-сирот | `src/daemon/registry.rs` (`stop_session`), `src/cli/kill.rs` (registry-верификация перед сигналом) | все | Partial |
| 8 | HANDLE-list captured-stdio контракт (иначе fail-closed) | `src/sandbox/handle.rs` (`StdioMode`), `src/sandbox/windows/mod.rs` | Windows | Strong |
| 9 | Adversarial suite (§15) + замена self-attested redteam-пробы execution-тестом | `tests/integration/linux_proctree_adversarial.rs` (новый), `tests/integration/macos_proctree.rs` (новый), `tests/integration/windows_proctree.rs` (новый), `src/redteam.rs` (`test_setsid_escape`), `tests/integration/linux_orphans.rs` (расширить) | все | Test |
| 10 | `doctor --probe` assurance-репорт (tier × механизм × residual) + SECURITY.md sync | `src/doctor/probe.rs`, `SECURITY.md`, `docs/platform-backends.md`, `ARCHITECTURE.md` | все | Docs |
| 11 | VM kill-контракт в спеке + host-watchdog | `src/sandbox/windows/windows_sandbox.rs`, docs | VM | Strong |
| 12 | Graceful-флаг (default immediate-SIGKILL) + TUI-timeout паритет | `src/main.rs`, `src/config.rs`, `src/tui/*` | все | UX/compat |

Не менять молча: `KillStrategy` варианты, `SWEEP_BUDGET_MS`, `sweep:false` на
macOS, daemon stop-семантика, redteam-статусы — всё через этот proposal.

## 17. Migration plan

Фаза A (верификация без смены поведения): п.9-тесты + п.10-docs + п.8-контракт;
adversarial suite в CI как must-report (не блокирует). Фаза B (state machine):
п.1+п.2+п.5 за флагом, default старое поведение; включить VERIFY-assertion с
репортом, graceful default off. Фаза C (ужесточение): п.3 pidfd, п.4 cgroup
default, п.6 windows-assertion, п.7 daemon-graceful; adversarial становится
must-pass на Strong tier. Фаза D: п.11 VM-контракт, п.12 graceful-флаг/TUI
паритет. Каждая фаза: `docs/proctree-proposal.md` ← статус, SECURITY.md sync,
версионный бамп +0.0.1 по AGENTS.md-политике отдельным коммитом.

## 18. Security limitations (честные остатки)

- FS-ONLY/macOS: detached grandchild при SIGKILL(vetto) или вне бюджета —
  выживает;入学 Landlock/seccomp наследуются, но живость — нет. Unsupported
  без pidns/Job: запретить setsid, получить crash-proof sweep.
- Любой tier: respawn через внешнюю сущность (cron/systemd/host-процесс того
  же uid вне границы, daemon-registry) — вне proctree-границы по определению;
  закрывается FS-политикой (некуда записать unit), не kill-логикой.
- D-state процессы: SIGKILL отложен ядром; VERIFY отличает, но не ускоряет.
- PID-reuse между наивными сканами — до внедрения pidfd (п.3).
- Наблюдение (audit/poller/ETW/FSEvents) — evidence only, пропуск ≠ разрешение.
- Prompt-injection внутри разрешённого, записи в `$PROJECT`, side-channels,
  враждебное ядро/root — out of scope (threat-model.md).