docs(vault-sync): acl fix closed 236/236 INHERIT+WRITE, watchdog cron, root cause hermes-taiga

This commit is contained in:
Alexey Martemyanov committed 2026-09-22 20:09:38 +06:00
1 parent 8b831a85a1
commit afbf895e2e
2 files changed
+154 -8

No files matched your search

@@ -15,7 +15,100 @@ namespace: family
> 🔴 **РЕЦИДИВ 2026-09-22** — дефект вернулся (35 root-каталогов), наследование не защитило.
> Плюс важный урок: **собирать dacl «с нуля» нельзя** — TrueNAS требует поле `id` в
> каждой ACE, работает только read-modify-write. См. раздел «РЕЦИДИВ 2026-09-22» ниже.
> **Статус: ОТКРЫТ** (синк сломан, фикс v2 готов но не применён).
> ✅ **ПОЧИНЕНО 2026-09-22 (закрыто).** 236/236 каталогов: `INHERIT` + `WRITE`. Push прошёл
> end-to-end (`ba3780c..8b831a8 main -> main`). Установлен cron-сторож (каждые 2 ч).
> См. раздел «✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22» в конце.
## ✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22 (закрыто)
**Результат:**
```
INHERIT = 236/236 (все objects/XX)
WRITE = 236/236 (write-тест в каждый каталог)
root-owned каталогов: 35 — владелец root, но write-тест OK=35 FAIL=0 (группа 950 имеет права)
git push с Mac: ba3780c..8b831a8 main -> main ✅
```
### 🔴 Главный урок: `setacl` асинхронный — надо ЖДАТЬ job
Провал нескольких прогонов был из-за **гонки**: скрипт вызывал `setacl`, сразу читал
`getacl` и видел **старые** флаги (`NOINHERIT`) → ложный `fail`. На самом деле job
выполнялся через 1–3 сек и флаги применялись.
```python
out = midclt("filesystem.setacl", arg) # → job id
jid = int(out)
while time.time() < deadline:
j = json.loads(midclt("core.get_jobs", f'[["id","=",{jid}]]'))
if j[0]["state"] in ("SUCCESS","FAILED","ABORTED"): break
time.sleep(1)
# ТОЛЬКО ТЕПЕРЬ читать getacl
```
**Признак гонки:** `fixed=0`, `already=N`, `fail=M` — setacl «прошёл», флаги «не изменились».
### Рабочая структура ACL (после фикса)
```json
{"tag":"owner@", "id":-1, "perms":{"BASIC":"FULL_CONTROL"}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"}
{"tag":"group@", "id":-1, "perms":{"READ_DATA":true,"WRITE_DATA":true,...}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"}
{"tag":"everyone@","id":-1, "perms":{"READ_DATA":true,"WRITE_DATA":true,...}, "flags":{"BASIC":"INHERIT"}, "type":"ALLOW"}
```
🔴 **Флаги задаются как `{"BASIC":"INHERIT"}`, НЕ как булев dict**
(`{"FILE_INHERIT":True,"DIRECTORY_INHERIT":True}`). Первая попытка с булевым dict
отклонена: `[EINVAL] ... missing required keys(s): {'id'}` — потому что при неверном
формате TrueNAS пересобирает ACE и теряет `id`. Замена `NOINHERIT` → `INHERIT` в
существующем ACE работает.
🔴 **`getacl` возвращает `uid:0, gid:950` для root-owned каталогов, но `perms.BASIC` с
`FULL_CONTROL` — и запись от uid 950 проходит через `group@` (gid 950).** Владелец root
сам по себе не блокирует.
### Перманентность — проверена ФАКТОМ
```bash
mkdir objects/zzt # новый каталог
midclt call filesystem.getacl .../objects/zzt
# → owner@/group@/everyone@: FILE_INHERIT:True, DIRECTORY_INHERIT:True, INHERITED:True ✅
touch objects/zzt/.wt # → OK ✅
```
**Новый `objects/XX` наследует права автоматически.** Баг не вернётся даже при
root-владельце, потому что ACL родителя уже с INHERIT.
### Сторож от рецидива
```
~/scripts/vault-acl-watchdog.py (на TrueNAS)
cron: 0 */2 * * * /usr/bin/python3 ~/scripts/vault-acl-watchdog.py >> ~/scripts/vault-acl-watchdog.log 2>&1
```
Поведение: **SILENT если всё ок**; если нашёл сломанный каталог — чинит (setacl + ожидание
job) и печатает отчёт. Запуск от `truenas_admin` (uid 950) — `midclt` ему доступен как
FULL_ADMIN, root-пароль не нужен.
### 🔴 Осталось (корень НЕ устранён)
Контейнер `hermes-taiga` (второй Hermes на TrueNAS) работает **от root**:
```
f3f36f1eb745 hermes-taiga:latest
└─ uv run hermes gateway run (root)
└─ mcpvault /vault (root) → создаёт root-owned objects/XX
compose: /mnt/RED_2TB/docker/hermes/docker-compose.yml
env: HERMES_ALLOW_ROOT_GATEWAY=1 ← явное разрешение root
User: (пусто) = root
mounts: /mnt/RED_2TB/storage/git/obsidian-vault.git → /vault.git
```
**Перманентное устранение корня:** добавить `user: "950:950"` в `taiga` service этого
compose → mcpvault пишет от 950 → root-объектов нет вообще.
**Риск:** контейнер может не стартовать, если образ требует root для `/opt/data`.
Требует перезапуска `hermes-taiga` (второй инстанс отключится на ~минуту).
**Не сделано** — чужой контейнер, нужно явное решение Alex. Сторож пока закрывает
последствия автоматически.
## Симптом
@@ -151,12 +244,9 @@ setacl({"path": path, "dacl": dacl,
## Remaining (открытые вопросы)
0. 🔴 **2026-09-22 РЕЦИДИВ НЕ УСТРАНЁН.** Синк сломан, push с Mac падает. Кол-во root-каталогов
вернулось (35). Фикс v2 (`/tmp/fix_vault_acl2.py` на Mac, read-modify-write) **написан, но не
залит и не запущен** — Alex: «сначала доки, потом чинить синк».
**Порядок возобновления:** `scp` скрипта на TrueNAS → запуск → проверка `touch` в
`objects/XX` → `git push nas main`. Скрипт v1 (`fix_vault_acl_inherit.py`) **нерабочий**,
удалить/не использовать (`[EINVAL] … missing required keys(s): {'id'}`).
0. ~~**2026-09-22 РЕЦИДИВ НЕ УСТРАНЁН.**~~ → **УСТРАНЁН 2026-09-22**, см. раздел
«✅ ФИНАЛЬНЫЙ ФИКС 2026-09-22» выше. Скрипт v1 (`fix_vault_acl_inherit.py`) — нерабочий
(булев dict флагов без `id`), заменён на read-modify-write + ожидание job.
1. ~~**Наследование не проверено на практике**~~ — **проверено 2026-09-21 15:05 UTC** (read-only с Taiga), см. раздел «Верификация наследования» ниже. Наследование работает **на тот момент**; 2026-09-22 показал, что от рецидива оно НЕ спасает.
2. **`mcpvault` продолжает работать от root** — пока он root, дефект может вернуться. Профилактика: перезапустить его под uid 950.
3. **`cleanup` не видит `/media/movies`** — на Кра́кене удаляются только Radarr-файлы; просмотренное лежит в `/media/movies`, диск будет расти. Отдельная задача (см. `family/plans/watchlist-disk-overflow-fix.md`).
@@ -133,10 +133,66 @@ touch /mnt/RED_2TB/storage/git/obsidian-vault.git/objects/80/.wrtest && echo WRI
```
`WRITE OK` → push will succeed. Confirmed: `f9a5c2b..21505ec main -> main`, then a clean `git pull` on Kraken.
### Durable fix with INHERITANCE (2026-09-21, applied — 291 dirs)
### Durable fix with INHERITANCE (2026-09-21 — 291 dirs, 2026-09-22 — 236 dirs)
The minimal recipe above (add perms to existing `group@` ACE) restores write for *current* dirs, **but the bug returns**: the next push by the root-owned `mcpvault` creates a NEW `objects/XX/` dir whose ACL is inherited from the *parent*, and if the parent's `everyone@`/`group@` lacks `WRITE_DATA` + inherit flags, the new dir is root-owned and read-only again → push fails again.
🔴 **RE-CUR 2026-09-22.** Дефект вернулся (35 root-каталогов). Итог фикса: **236/236 каталогов
с `INHERIT` + `WRITE`**, push прошёл end-to-end. Порядок работ и уроки — в
`family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md` §«✅ ФИНАЛЬНЫЙ ФИКС».
**Working fix = read-modify-write, НЕ сборка dacl с нуля:**
```python
# perms granted to owner@ AND group@ AND everyone@:
# READ_DATA WRITE_DATA APPEND_DATA EXECUTE DELETE_CHILD
# READ_ATTRIBUTES WRITE_ATTRIBUTES READ_NAMED_ATTRS WRITE_NAMED_ATTRS
# READ_ACL (and WRITE_ACL WRITE_OWNER DELETE for owner@)
# flags задаются КАК СТРОКА, не булев dict:
# ace["flags"] = {"BASIC": "INHERIT"} ← FILE_INHERIT + DIRECTORY_INHERIT
# call: midclt call filesystem.setacl '<one json with path + dacl + options>'
# options = {"recursive": False, "traverse": True, "stripacl": False}
```
🔴 **Четыре питфолла, стоившие времени:**
1. **Собирать `dacl` «с нуля» НЕЛЬЗЯ.** TrueNAS требует поле `id` в каждой ACE:
`[EINVAL] filesystem_acl.dacl.0: ACL entry is missing required keys(s): {'id'}`.
Работает только **read-modify-write**: `getacl` → править `perms`/`flags` → `setacl`.
2. **Флаги — `{"BASIC":"INHERIT"}`, НЕ `{"FILE_INHERIT":True,"DIRECTORY_INHERIT":True}`.**
Булев dict отклоняется той же ошибкой про `id` (при неверном формате TrueNAS
пересобирает ACE и теряет поле).
3. **`setacl` АСИНХРОННЫЙ — надо ЖДАТЬ job до `SUCCESS`.** Проверка `getacl` сразу после
вызова видит СТАРЫЕ флаги → ложный `fail`. Признак гонки: `fixed=0, already=N, fail=M`.
```python
jid = int(midclt("filesystem.setacl", arg))
while deadline:
j = json.loads(midclt("core.get_jobs", f'[["id","=",{jid}]]'))
if j[0]["state"] in ("SUCCESS","FAILED","ABORTED"): break
time.sleep(1)
```
4. **Владелец `root` сам по себе НЕ блокирует запись.** `getacl` даёт `uid:0, gid:950`,
запись от uid 950 идёт через `group@` (gid 950). `write-тест` в 35 root-каталогов:
OK=35 FAIL=0.
**Верификация (обязательна, делай до retry push):**
```bash
touch <repo>/objects/XX/.wrtest && echo WRITE_OK # у КАЖДОГО каталога
# + тест наследования: mkdir objects/zzz → getacl → должен быть INHERITED:True → touch внутрь
# + end-to-end: git clone <repo> && cd && echo x > f && git add f && git commit && git push
```
**Сторож (поставлен 2026-09-22):**
```
~/scripts/vault-acl-watchdog.py (TrueNAS, от truenas_admin)
cron: 0 */2 * * * python3 ~/scripts/vault-acl-watchdog.py >> ~/scripts/vault-acl-watchdog.log 2>&1
```
SILENT если всё ок; чинит + докладывает если сломано.
⚠️ **Корень НЕ устранён:** `mcpvault` контейнера `hermes-taiga`
(`/mnt/RED_2TB/docker/hermes/docker-compose.yml`, `HERMES_ALLOW_ROOT_GATEWAY=1`, `User:` пусто)
работает от root. Перманентный фикс — добавить `user: "950:950"` в `taiga` service.
Working fix = **full dacl replacement** (not per-ACE edit) with **inheritance flags ON**, applied to every dir:
```python