Files
obsidian-vault/family/how-to/vault-git-sync.md
T

351 lines
38 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Vault Git Sync — Architecture & Scripts
updated: '2026-10-15'
type: tech
tags:
- vault
- git
- sync
- infra
- obsidian
---
# Vault Git Sync
> **✅ STATE 2026-09-26 (поправка Taiga, 09:50 UTC) — «раз в час» было ошибкой чтения: sync идёт каждые 5 минут.**
> Блок ниже («РАСХОЖДЕНИЕ РАСПИСАНИЯ», «задержка до 60 минут») построен на поле `schedule_display: "0 * * * *"`
> в `/opt/data/cron/jobs.json` — это **устаревшее поле**, диспетчер его не использует. Факты:
> - `jobs.json` → `schedule.expr = "*/5 * * * *"`, `display = "*/5 * * * *"` (устарело только `schedule_display`);
> - `executions.db`, job `29851c413903`, 1001 тик за 09-22 22:25 → 09-26 09:40 UTC: интервалы **985 × 5 мин**,
> 14 × 4 мин, 1 × 1 мин → **288,6 тика/сутки**, а не 24. Тик «раз в час» дал бы 98 записей за это окно;
> - `/tmp/vault-sync.log` — timestamped `Pushed ✓` каждые 5 минут.
>
> `hermes cron list` печатает то же устаревшее поле (`Schedule: 0 * * * *`), поэтому каденцию проверять
> по `executions.db` или `jobs.json:schedule.expr`, а не по выводу CLI. Задержка «до 60 минут» бумажная;
> реальная доставка до телефона — в пределах тика. Перезапись `schedule_display` через
> `hermes cron edit 29851c413903 --schedule '*/5 * * * *'` — на решение Алексея (косметика, на исполнение не влияет).
> **✅ STATE 2026-09-26 (later, RESOLVED) — ACL с `nas_users`+INHERIT применён на `obsidian-syncthing`, синк работает.**
> Оба дефекта закрыты:
> **(1) Битого remote** — `origin = /vault.git` переписан на полный путь (но **снова слетел** после
> `sync-vault.sh`: тот форсит `file:///vault.git` внутри контейнера — это правильно для `/vault`,
> но для хостового клона путь другой. Проверять `git remote -v` перед диагностикой прав).
> **(2) Права — решено перманентно:** `midclt filesystem.setacl` рекурсивно на
> `/mnt/RED_2TB/storage/obsidian-syncthing` с `gid=3000` (`nas_users`) и флагами
> `FILE_INHERIT`+`DIRECTORY_INHERIT` на **все** ACE. Было `trivial_acl: true` + 4051 объект root →
> стало `trivial_acl: false`, `group: 3000`, **0 объектов root** (все 4073 = `truenas_admin`).
> Группа `nas_users` (gid 3000) содержит `root`, `truenas_admin`, `nas`, `mallexxx`, `transmission` —
> поэтому и root-процесс Taiga, и Syncthing (uid 950) пишут в одну папку.
> **Наследование проверено ФАКТОМ:** `touch .inherit_test` → `truenas_admin:nas_users`,
> `mkdir .inherit_dir_test` → `truenas_admin:nas_users`. Артефакты после теста удалены.
> `git fetch origin main` → `Already up to date`; HEAD клона содержал коммит с Mac (`f2501dc`).
> **Бэкап ACL перед правкой:** `/home/truenas_admin/variantA_bk/acl-obsidian-syncthing-before-20260926-154827.json`.
> **Скрипт:** `acllib_obsidian_syncthing_fix.py` (см. раздел «ACL repair» → рецепт).
>
> **🔴 ГЛАВНЫЙ ПИТФОЛЛ ЭТОЙ ПРАВКИ: `midclt filesystem.getacl` отдаёт КЭШ.**
> Сразу после `setacl` он печатал старые значения (`trivial: True`, `gid: 950`, `NOINHERIT`),
> и скрипт рапортовал ложный «VERIFY FAILED». Реальное состояние видно только через
> **`nfs4xdr_getfacl`** и **`stat`**. Никогда не верить `midclt getacl` сразу после `setacl` —
> сверять `nfs4xdr_getfacl` (там `trivial_acl: false` и `fd-----` = FILE_INHERIT+DIRECTORY_INHERIT)
> плюс `stat -c '%A %U:%G'`.
>
> **Историческая путаница (для будущих сессий):** рекурсивный ACL с `nas_users` действительно
> ставился 2026-09-23, но на **`/mnt/RED_2TB/Edu`** (запрос Alex: «можешь просто группу под acl
> сделать с inherit чтобы весь edu был доступен группе включающей truenas_admin?»), **не** на
> `obsidian-syncthing`. Отсюда «я это уже чинил» — чинил, но другую папку. Искать прошлые сессии:
> `sqlite3 ~/.hermes/hermes-whale/state.db "SELECT session_id, substr(content,1,300) FROM messages WHERE content LIKE '%setfacl%' ORDER BY id DESC LIMIT 40;"`.
>
> Разовый `chown` лечит только уже существующие объекты; следующий pull от root возвращает дефект.
> Перманентно лечит `ACL`+`INHERIT` (наследование новыми объектами) либо запуск процесса **не от root**.
> Корень по-прежнему не устранён: контейнер `hermes-taiga` работает от root.
> Подробно: `family/documents/vault-sync/2026-09-26-syncthing-clone-root-objects-acl-confusion.md`.
>
> **🔴 STATE 2026-09-26 — РАСХОЖДЕНИЕ РАСПИСАНИЯ: Taiga-сервер синкается РАЗ В ЧАС, не 5 минут.**
> Job `sync-vault` внутри контейнера `hermes-taiga` (Hermes cron, id `29851c413903`) расписан
> **`0 * * * *`** — то есть раз в час, **хотя таблица ниже (и `family/how-to/obsidian-sync`)
> заявляет «*/5»**. Обнаружено при проверке «доехал ли док до телефона»: ноты, залитые с Mac
> в 14:32, в клоне `obsidian-syncthing` отсутствовали.
>
> **Следствие:** изменения с Eagle/Kraken доезжают до телефона с задержкой **до 60 минут**.
> Цепочка `bare repo → git-клон obsidian-syncthing → Syncthing → телефон` рвётся на **втором**
> шаге (`Syncthing здоров` ≠ `док доехал`).
>
> **Ручной триггер (проверено):**
> ```bash
> docker exec hermes-taiga sh -c 'cd /opt/data && HERMES_HOME=/opt/data \
> /opt/hermes/.venv/bin/hermes cron run 29851c413903'
> # → Triggered job: sync-vault … Ran now: succeeded.
> ```
> После триггера: клон обновлён, Syncthing `needFiles: 0`, телефон `completion: 100`.
>
> **Открытый вопрос к Alex: менять расписание на `*/5 * * * *` или оставить раз в час?**
>
> ⚠️ **Диагностика синка — только изнутри контейнера Syncthing:** порт `8384` слушает лишь
> в контейнере, `curl` с хоста TrueNAS даёт пустоту.
> ```bash
> docker exec syncthing sh -c 'K=$(grep -oE "<apikey>[^<]+" /var/syncthing/config/config.xml | sed "s|<apikey>||"); \
> curl -s -H "X-API-Key: $K" "http://localhost:8384/rest/db/status?folder=k5vyy-gzdgj"'
> ```
> Папка Syncthing: id `k5vyy-gzdgj` «Obsidian Vault», устройства `SM-S931B` (телефон) и `syncthing-taiga`.
> Готовый скрипт: `~/Developer/personal-os/tools/music-tags/check_syncthing2.sh`.
> **✅ STATE 2026-09-22 — рецидив ACL закрыт перманентно.** Дефект вернулся (35 root-каталогов),
> push с Mac падал. Починено: **236/236 каталогов `INHERIT` + `WRITE`** через
> `midclt filesystem.setacl` (read-modify-write + ожидание job). **Наследование подтверждено
> ФАКТОМ** — `mkdir objects/zzz` → новый ACL унаследован (`INHERITED:True`), `touch` внутрь OK.
> End-to-end push: `ba3780c..8b831a8 main -> main` ✅. Поставлен **cron-сторож** (каждые 2 ч):
> `~/scripts/vault-acl-watchdog.py` на TrueNAS — SILENT когда всё ок, чинит + докладывает когда
> сломано. Скрипты: `/Users/admin/Developer/vault-tools/`.
> 🔴 **Корень НЕ устранён:** контейнер `hermes-taiga` работает от root
> (`HERMES_ALLOW_ROOT_GATEWAY=1`, `User:` пусто) → `mcpvault /vault` пишет root-owned объекты.
> Перманентный фикс — `user: "950:950"` в `taiga` service
> `/mnt/RED_2TB/docker/hermes/docker-compose.yml` (риск: образ может требовать root для `/opt/data`).
> Сторож пока закрывает последствия. Подробно:
> `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
>
> **STATE 2026-09-21 — bare repo push починен (наследуемый ACL).** `git push nas` падал `Permission denied` на `objects/XX`: 28 каталогов были root-owned (создавал `mcpvault` **второго Hermes на TrueNAS, от root**), 13 каталогов без owner-`x`. Фикс: **полный dacl-REPLACE с `FILE_INHERIT`+`DIRECTORY_INHERIT`** через `midclt filesystem.setacl` — 291 каталог, 0 ошибок, без root-пароля. Push прошёл дважды, Mac=`nas`=`3b923d3`. ⚠️ Прямой write-тест `touch` в `objects/` **не выполнялся** → наследование подтвердит только следующий push root-инстанса. Подробно: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
>
> **STATE 2026-09-02 (позднее обновление):** Eagle/bare repo снова **синхронны** (ahead=0/behind=0, `2fab879`). Ранее (см. ниже) git-sync стоял из-за битых прав `storage/git` — **починено** полной dacl-заменой (`family/documents/vault-sync/2026-09-02-restore-privilege-scope.md` §RESOLVED).
>
> **STATE 2026-09-04:** починена потеря git identity в контейнере Taiga (слетела при рекреации после crashloop 09-02) — коммиты молча не проходили ~1.5 суток, изменения телефона не доезжали до bare. Фикс: `sync-vault.sh` теперь идемпотентно ставит `user.name "Taiga"` / `user.email "taiga@hermes"` при каждом запуске. Накопленное закоммичено (`2b45264`). Подробно: `family/documents/vault-sync/2026-09-04-git-identity-loss-silent-commit-failure.md`.
>
> **STATE 2026-09-26 — `pipefail` + `grep -q` молча отменял коммиты в `sync-vault-lib.sh`.** При большом
> числе изменений (492 файла ≈ 23 КБ вывода) `git status --porcelain | grep -q .` падал по `SIGPIPE`
> (grep выходил после первого совпадения, git дописывал в закрытый pipe), `set -o pipefail` объявлял
> pipeline упавшим (exit 141) → ветка `else` → **коммит не создавался вообще**, при этом тик печатал
> `Pushed ✓` ×3 и exit 0. Так застряли 491 mode-flip и правка телефона (`1-needs-decision.md`, 4 чекбокса
> `[x]`). Фикс: условие через `STATUS="$(git status --porcelain)"` + `[ -n "$STATUS" ]`, сообщение
> коммита — одним `awk` (первые 25 путей, дальше `(... +N more)`). ⚠️ `awk | head -25` в подстановке
> даёт тот же SIGPIPE → тик падает с exit 141, так делать нельзя. Проверено: коммит `12a7566e` (492 файла),
> три SHA равны, `git status` пуст. Разбор: `family/documents/vault-sync/2026-09-26-commit-skip-pipefail.md`.
>
> **⛔ НОВАЯ проблема 2026-09-02 (не решена):** оба **Taiga-клиента** (`/storage/obsidian` и `/storage/obsidian-syncthing`) **отстали на месяц** — HEAD `ceea848` (2026-08-01) vs bare repo `2fab879` (2026-09-02). Причина: контейнер `hermes-taiga` в **crash-loop** (16339 рестартов) — `uv run hermes gateway run` не может докачать депы (pytz/markdown-it-py) с PyPI, т.к. весь HTTP идёт через **мёртвый `socks5://vless-proxy:1080`**, а `UV_CACHE_DIR=/tmp/uv-cache` пустеет на каждом рестарте. Taiga 3-phase sync (крон внутри агента) не выполняется => телефон видит устаревший vault. Подробно: `family/documents/vault-sync/2026-09-02-hermes-taiga-crashloop.md`. Фикс НЕ применён (ждёт Alex).
> **Триггер cron на маке (Eagle) — это launchd, НЕ Hermes cron:** `~/Library/LaunchAgents/com.sync-vault.plist` → `/Users/admin/scripts/sync-vault.sh`, `StartInterval=300` (5 мин). Скрипт `sync-vault-lib.sh` намеренно глушит dead remote (`git fetch` fail → `exit 0`), поэтому launchd показывает `last exit code = 0` даже при фактически стоящем sync. Системный crontab на маке пуст.
Obsidian vault is a bare git repo on TrueNAS:
`/mnt/RED_2TB/storage/git/obsidian-vault.git`
Also mirrored in Gitea: `https://git.mallexxx.duckdns.org/git_admin/obsidian-vault`
Five clients sync to it: Eagle, Kraken, Taiga (vault), Taiga (syncthing/phone), iPad (Edhita Documents).
## Architecture
```
bare repo (obsidian-vault.git) — source of truth
↑↓ ↑↓ ↑↓ ↑↓
Eagle Kraken Taiga /vault Taiga /obsidian-syncthing
full clone sparse clone sparse clone full clone
~/obsidian ~/obsidian personal/family/ phone ↔ Syncthing
personal/family/ .obsidian/
.obsidian/
```
Taiga runs 3-phase sync (order matters):
1. **syncthing** → phone changes land in bare repo first
2. **vault** → agent (mcpvault) changes land in bare repo
3. **syncthing** → agent changes propagate to phone
## Sync Algorithm (same for all clients)
```bash
git add -A && git commit # commit local changes
git fetch origin main # get remote state
git merge origin/main --no-edit # 3-way merge (conflict → commit markers)
git push origin main # push back
```
No stash. No GIT_DIR/GIT_WORK_TREE workarounds.
## Scripts
| Host | Script | Type | Trigger |
|--------|-------------------------------|---------------|----------------------|
| Eagle | `~/scripts/sync-vault.sh` | full clone | **launchd** `com.sync-vault` (⚠️ НЕ Hermes cron) `StartInterval=300` |
| Kraken | `~/scripts/sync-vault.sh` | sparse clone | host crontab */5 |
| Taiga | `/opt/data/sync-vault.sh` | 3-phase orch. | Hermes cron **`*/5 * * * *`** — job `29851c413903`. ⚠️ `hermes cron list` и поле `schedule_display` в `jobs.json` показывают устаревшее `0 * * * *` (см. поправку в STATE 2026-09-26 выше); фактические тики — каждые 5 мин |
| iPad | `/var/mobile/Library/VaultSync/sync-vault.sh` | full working tree, initial shallow clone | **launchd** `local.ipad.obsidian-vault-sync`, mobile, StartInterval300 |
> **⭐ УТОЧНЕНО 2026-09-02 (Eagle/мак sync-механизм):** автосинка на маке запускается **НЕ Hermes cron, а launchd-агентом** `~/Library/LaunchAgents/com.sync-vault.plist`:
> - Программа: `/Users/admin/scripts/sync-vault.sh`, `StartInterval = 300` сек (5 мин)
> - `launchctl list` → активен, `runs = 3762`, `last exit code = 0`
> - ⚠️ **`sync-vault-lib.sh` умышленно глушит недоступность remote**: если `git fetch` падает → логирует `Fetch failed (...)` и `exit 0`. Поэтому launchd показывает `exit code 0`, даже когда sync фактически сломан (см. случай ahead 55 при сломанном bare repo после restore). Это **не** отсутствие cron и не сбой — диагностировать нужно по `git status` (ahead count), а не по exit-code агента.
> - Системный crontab и `/etc/crontab` на маке отсутствуют.
All clients share a common library: `sync-vault-lib.sh` (same dir as sync-vault.sh).
Source of truth for scripts: `~/Developer/vault-sync-test/scripts/` on Eagle.
## Taiga Container Mounts
| Container path | Host path | Purpose |
|----------------------|----------------------------------------------------|--------------------------|
| `/vault` | `/mnt/RED_2TB/storage/obsidian` | sparse clone (agent rw) |
| `/vault.git` | `/mnt/RED_2TB/storage/git/obsidian-vault.git` | bare repo (git remote) |
| `/obsidian-syncthing`| `/mnt/RED_2TB/storage/obsidian-syncthing` | full clone (phone sync) |
| `/opt/data` | `/mnt/RED_2TB/docker/hermes/config` | Hermes config + scripts |
`/vault` is a sparse clone with `remote = file:///vault.git`.
`/vault.git` is hidden from the agent (mcpvault points to `/vault`).
`/obsidian-syncthing` is also a clone with `remote = file:///vault.git`, но **его `.git/` сам синкается
Syncthing'ом с других устройств**, и `remote.origin.url` периодически перезаписывается чужим адресом —
хост-путём `/mnt/RED_2TB/storage/git/obsidian-vault.git` (2026-09-26 09:36) или Gitea-URL
`https://git.mallexxx.duckdns.org/git_admin/obsidian-vault.git` (версия с телефона). Внутри контейнера
не резолвится ни один: `git fetch origin main` → `fatal: … does not appear to be a git repository`,
`sync-vault-lib.sh` печатает `Fetch failed (origin offline?) — skipping merge/push` и выходит **с exit 0**,
поэтому фазы 1 и 3 **молча не выполняются** (правки телефона не доезжают до bare, правки агента — до телефона),
а cron рапортует «успех». С 2026-09-26 09:42 `sync-vault.sh` в начале каждого тика принудительно ставит
`origin = file:///vault.git` и добавляет `url."file:///vault.git".insteadOf` для обоих чужих адресов.
Разбор: `family/documents/vault-sync/2026-09-26-syncthing-clone-origin-url-flip.md`.
## Sparse Checkout Paths
Taiga and Kraken check out only:
```
personal/
family/
.obsidian/
```
`work/` and `wiki/` are never checked out on sparse clients — they never touch those paths.
## Deploy a Script Fix
```bash
# Edit on Eagle:
~/Developer/vault-sync-test/scripts/sync-vault-lib.sh # common algorithm
~/Developer/vault-sync-test/scripts/sync-vault.sh # Taiga 3-phase orchestrator
# Deploy to Taiga:
scp ~/Developer/vault-sync-test/scripts/sync-vault-lib.sh \
~/Developer/vault-sync-test/scripts/sync-vault.sh \
truenas_admin@mallexxx.duckdns.org:/mnt/RED_2TB/docker/hermes/config/
# Deploy to Kraken (via sudo):
cat ~/scripts/sync-vault-lib.sh | ssh kraken "sudo tee ~/scripts/sync-vault-lib.sh > /dev/null"
cat ~/scripts/sync-vault.sh | ssh kraken "sudo tee ~/scripts/sync-vault.sh > /dev/null"
```
## .gitignore
Plugin binaries excluded to avoid cross-device conflicts:
```
.obsidian/plugins/obsidian-git/main.js
.obsidian/plugins/obsidian-git/styles.css
.obsidian/plugins/obsidian-git/manifest.json
```
`data.json` (plugin config) is tracked — shared settings.
## Regression Tests
`~/Developer/vault-sync-test/test-sync.sh` — 16 scenarios (A–P):
covers Cyrillic filenames, moves, deletes, conflicts, work/ phantom-delete, 3-phase sync.
```bash
cd ~/Developer/vault-sync-test && bash test-sync.sh
```
## Pitfalls
- **`nfs4xdr_getfacl`, а НЕ `getfacl`, если проверяешь наличие расширенного ACL** (2026-09-26). POSIX-вид (`getfacl`, `ls -l`) показывает `user::rwx group::rwx` и **скрывает** отсутствие именованных ACE. Только `nfs4xdr_getfacl` печатает `trivial_acl: true` — что значит «кастомного ACL нет». Все диагностики прав на TrueNAS начинать с него.
- 🔴 **`midclt filesystem.getacl` СРАЗУ ПОСЛЕ `setacl` отдаёт КЭШ** (2026-09-26). Печатает старые значения (`trivial: True`, старый `gid`, `NOINHERIT`), из-за чего правка выглядит неприменённой. Реальное состояние — только `nfs4xdr_getfacl <path>` (`trivial_acl: false`, флаги `fd-----`) + `stat -c '%A %U:%G %n'` + фактический `touch` внутрь папки. Не откатывать и не повторять `setacl` по показаниям `getacl`.
- **`chgrp` / `chown` от `truenas_admin` → `Operation not permitted`** (2026-09-26). В NFSv4 `group@` — это POSIX-gid файла, а не ACE: без смены gid группа не получает доступа. Менять gid/uid только через `midclt filesystem.setacl` (root-контекст). TrueNAS `sudo` требует пароль (в отличие от Kraken, где `sudo -n` работает).
- **Разовый `chown` — лечение симптома, не фикс** (2026-09-26). Если root-процесс продолжает писать в папку, root-owned объекты вернутся на следующем же запуске. Перманентно лечит только `ACL`+`INHERIT` (наследование новыми объектами) либо запуск процесса **не от root**.
- **«Этот фикс уже делали» — проверить, что он на ТОЙ САМОЙ папке** (2026-09-26). Рекурсивный ACL с группой `nas_users` ставился 2026-09-23, но на `/mnt/RED_2TB/Edu` (запрос Alex про `Маскелиаде`). На `obsidian-syncthing` ACL не было никогда — отсюда ложное «я это уже чинил». Каждый «известный фикс» верифицировать фактом на целевом объекте, а не памятью. Искать прошлые сессии: `sqlite3 ~/.hermes/hermes-whale/state.db "SELECT session_id, substr(content,1,300) FROM messages WHERE content LIKE '%setfacl%' ORDER BY id DESC LIMIT 40;"`.
- **Клон может иметь битый `origin`** (2026-09-26). `obsidian-syncthing` имел `origin = /vault.git` (потерян префикс пути) → `fatal: does not appear to be a git repository`. Проверять `git remote -v` в клоне до диагностики прав — это независимый дефект, который выглядит как проблема доступа.
- **`sync-vault-lib.sh` глушит ошибки (`2>/dev/null` на fetch/merge)** → «Pushed ✓» / `exit 0` при фактически упавшем pull. Это уже второй раз, когда поломка синка была не видна в логе. Диагностировать по `git status`/`git log` в клоне, не по exit-code.
- `git add pathspec` fails with `fatal: pathspec did not match any files` when a directory doesn't exist on disk — use `git add -A` instead
- `git add -A` in sparse worktree still stages deletions of out-of-cone files tracked in index — need to unstage them (fixed in old script; irrelevant with proper clone)
- `core.quotePath=true` (default) escapes Cyrillic paths in `git ls-files` output — use `-c core.quotePath=false`
- ZFS on TrueNAS blocks `chmod` — `git init` fails from host; must run from inside Docker container or create `.git` structure manually
- **NFS4 `owner@ DENY READ_DATA` ACL breaks push (fake "corrupt" objects)** — see History 2026-09-02. Key: after a TrueNAS **pool restore**, a subset of loose git objects in the bare repo can carry an inverted NFS4 ACL `owner@ type=DENY READ_DATA=True`. In NFSv4 a DENY overlays ALLOW, so the owning uid (e.g. 950) can't mmap-read those objects → on receive, `git-receive-pack`/`index-pack` reports `loose object ... is corrupt` and rejects push, even though data is intact (`git fsck --full` under root is clean). Fix = **full dacl REPLACE** via `filesystem.setacl` (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode `40` (`r--------`) on loose objects vs normal `750`.
> ⚠️ **Don't reach for `stripacl`** (2026-09-21): it tears the ACL down to POSIX and does **not** remove a DENY in all cases. Always pass a complete `dacl` array — `setacl` treats it as a full replace.
- **`Permission denied` writing `objects/XX/` (NOT the corrupt-object case)** — see History 2026-09-21. Two defects: (a) dirs with owner `rw-` and **no `x`** (`drw-r-x---`), (b) `objects/XX/` dirs **owned by `root`** with `group@:r-x` — git runs as uid 950 → cannot write. Root cause is `mcpvault` (a **second Hermes on TrueNAS, `/opt/hermes`**, vault `/vault`) running **as root** and creating root-owned objects in the shared bare repo. Requires `midclt` (see below) — `chown`/`chmod` as `truenas_admin` give `Operation not permitted` because uid 950 isn't the owner, and TrueNAS `sudo` **needs a password** (unlike Kraken, where `sudo -n` works). Fix must include **`FILE_INHERIT`+`DIRECTORY_INHERIT`** or the defect returns on the next push by the root instance. Recipe: `personal/tech/truenas-nfs4-acl-and-arrmultiuser.md` §«Durable fix with INHERITANCE».
## ACL repair via `midclt` (no root password needed)
`truenas_admin` is FULL_ADMIN in midclt → can read/write ACLs directly.
```bash
# READ — path as a BARE STRING
midclt call filesystem.getacl /mnt/RED_2TB/storage/git/obsidian-vault.git
midclt call filesystem.listdir /mnt/RED_2TB/storage/git/obsidian-vault.git/objects
# WRITE — ONE json object with "path" INSIDE it (opposite convention!)
midclt call filesystem.setacl '{"path":"/x","acltype":"NFS4","dacl":[…],
"options":{"recursive":false,"traverse":true,"stripacl":false}}'
```
Pitfalls: `setacl`/`setperm` are **async** (return a job id) — poll `core.get_jobs` until `SUCCESS`/`FAILED`, or the immediate `WAITING` reads as a failure. `filesystem.setperm` refuses when a non-trivial ACL is present (`[EINVAL] … Option "stripacl" required`).
### ✅ Рабочий рецепт: группа + INHERIT на папку (проверено 2026-09-26)
Скрипт (`acllib_obsidian_syncthing_fix.py`, отработал на `/mnt/RED_2TB/storage/obsidian-syncthing`):
read-modify-write ACL — читает существующий dacl, **сохраняет `id` полей** (без них TrueNAS падает
`missing required keys: {'id'}`), переписывает `flags` в `FILE_INHERIT+DIRECTORY_INHERIT`,
ставит `gid` нужной группы, `recursive: true`, `stripacl: false`.
```python
payload = {
"path": TARGET,
"uid": 950, # truenas_admin — оставить владельцем
"gid": 3000, # nas_users — группа, в которой и root, и uid 950
"acltype": "NFS4",
"dacl": new_dacl, # read-modify-write, флаги INHERIT, id сохранены
"options": {"recursive": True, "traverse": True, "stripacl": False},
}
```
Запуск: `scp` скрипта на NAS → `ssh truenas_admin@mallexxx.duckdns.org "python3 /home/truenas_admin/<script>.py"`.
**Критические детали:**
- `stripacl: false` — `true` не убирает DENY и ломает наследование (проверено 2026-09-21).
- `gid` в payload при `recursive: true` **заодно переписывает владельца всех вложенных объектов**
(4051 root-объект → `truenas_admin` одной операцией).
- `group@` в NFSv4 — это POSIX-gid, а **не** ACE. `chgrp` от `truenas_admin` даёт `Operation not permitted`;
менять gid только через `midclt`.
- 🔴 **VERIFY через `midclt getacl` бесполезен — это кэш.** После `setacl` он печатает старые значения.
Проверять: `nfs4xdr_getfacl <path>` (`trivial_acl: false`, `fd-----`) + `stat -c '%A %U:%G %n'` +
фактический `touch` нового файла внутрь папки.
- **Бэкап перед правкой:** `midclt call filesystem.getacl <path> > /home/truenas_admin/variantA_bk/acl-<name>-before-$(date +%Y%m%d-%H%M%S).json`.
- **Приёмка:** `find <path> -type f -printf '%u\n' | sort | uniq -c` → 0 объектов от root;
`git fetch origin main` → `Already up to date`.
## History
- **2026-09-26 (ACL на `obsidian-syncthing` — закрыто)**: синк не доезжал до телефона. Два независимых дефекта: **(1)** `origin` клона = `/vault.git` (потерян префикс) → `fetch` падал; **(2)** 4051 из 4055 объектов принадлежали `root` — контейнер `hermes-taiga` работает от root (`Config.User` пусто, `id` → `uid=0`), он делает pull, git под uid 950 не мог писать (`insufficient permission for adding an object`). Пользователь потребовал поставить ACL с INHERIT на папку — **сделано**: `midclt filesystem.setacl` recursive, `gid=3000` (`nas_users`) + `FILE_INHERIT+DIRECTORY_INHERIT` на все ACE. Было `trivial_acl: true` → стало `false`, `group: 3000`, 0 root-объектов (4073 = `truenas_admin`). Наследование подтверждено `touch`/`mkdir` фактом. `git fetch` → `Already up to date`. Коммит с Mac `f2501dc` найден в истории клона. 🔴 `midclt getacl` после `setacl` отдаёт кэш (ложный «не применилось») — сверять `nfs4xdr_getfacl` + `stat`. Бэкап: `/home/truenas_admin/variantA_bk/acl-obsidian-syncthing-before-20260926-154827.json`.
- **2026-09-26**: клон `/obsidian-syncthing` потерял рабочий remote — Syncthing переписал `.git/config` (mtime 09:36:19, owner uid 950 ≠ наши root-записи) на хост-путь `/mnt/RED_2TB/storage/git/obsidian-vault.git`. Первый за всю историю лога `Fetch failed` (тик 09:40:31) → фазы 1 и 3 молча скипались при `exit 0`; `/obsidian-syncthing` отстал на 1 коммит Eagle (`66d4d626` vs bare `5c939ed8`). Фикс: `sync-vault.sh` форсит `origin = file:///vault.git` каждый тик + `insteadOf` для хост-пути и Gitea-URL; after-fix три SHA равны `3edbddf5`, статусы пусты, новых `Fetch failed` нет. Корень (`.git` клона синкается Syncthing'ом — отсюда же `.sync-conflict-*` внутри `.git`) — на решение владельца. Подробно: `family/documents/vault-sync/2026-09-26-syncthing-clone-origin-url-flip.md`.
- **2026-09-26 (поправка каденции)**: снято ложное «Taiga синкается раз в час». `hermes cron list` и `jobs.json:schedule_display` показывают `0 * * * *`, но `schedule.expr = "*/5 * * * *"` и `executions.db` дают 985 интервалов по 5 мин из 1001 тика (288,6/сутки). Диагностировать каденцию — только по `executions.db`/`schedule.expr`.
- **2026-09-21**: bare repo push `Permission denied` on `objects/XX`. 28 dirs root-owned (created by `mcpvault` of a **second Hermes on TrueNAS** running as root) + 13 dirs with owner `rw-` and no `x`. `chown`/`chmod` as `truenas_admin` → `Operation not permitted` (TrueNAS sudo needs a password, unlike Kraken). Fixed with a **full dacl REPLACE + `FILE_INHERIT`/`DIRECTORY_INHERIT`** via `midclt filesystem.setacl` — **291 dirs, 0 errors**, no root password. Two clean pushes after; Mac = `nas` = `3b923d3`. ⚠️ Direct write-test in `objects/` was NOT run. Full log: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
- **2026-06-20**: Kraken ACL fix. mcpvault (Hermes контейнер, uid 10000) не мог писать в существующие файлы vault из-за прав (EACCES). Подробно:
**Проблема 1 — права доступа:** файлы в vault создаются от uid 1000 (kraken через git на хосте), а mcpvault в контейнере работает от uid 10000. `patch_note` падал: `EACCES: permission denied`. `delete_note` + `write_note` срабатывали, но mcpvault при write перезаписывает файл целиком, переформатируя YAML фронтматер (убирает кавычки, меняет стиль) — шум в git-diff.
**Проблема 2 — экранирование backtick в patch_note:** mcpvault `patch_note` не может найти строку, содержащую тройные обратные кавычки (`` ``` ``). Пришлось якориться на соседнюю строку без backtick — например, на `\n\n## Заголовок` вместо `\`\`\`\n\n## Заголовок`.
**Решение — ACL ext4 (setfacl):**
```bash
sudo setfacl -R -m u:10000:rwx /srv/.../obsidian # mcpvault — write
sudo setfacl -R -d -m u:10000:rwx /srv/.../obsidian # default для новых файлов
sudo setfacl -R -m g:kraken:rwx /srv/.../obsidian # kraken (uid 1000) — write
sudo setfacl -R -d -m g:kraken:rwx /srv/.../obsidian # default для новых файлов
```
**Итоговая совместимость:** uid 10000 (mcpvault) и uid 1000 (kraken/git) могут читать и писать одни и те же файлы. `patch_note` работает без EACCES.
**Ограничение (mcpvault patch_note + backtick):** `patch_note` не может найти строку, содержащую тройные обратные кавычки (`` ``` ``). Пример: нужно было вставить новый раздел между closing code fence `\`\`\`` и заголовком `## Исправления (v0.0.92...`. Варианты `oldString`, которые **НЕ сработали**:
- `"}\n\n\`\`\`\n\n## Исправления (v0.0.92..."` — не найдено
- `"\`\`\`\n\n\n## Исправления (v0.0.92..."` — не найдено
- Любая комбинация пустых строк между `\`\`\`` и заголовком — не найдено
**Что сработало:** якориться на соседнюю строку **без** обратных кавычек. Вместо `\`\`\`\n\n## Исправления...` использовать `\n\n## Исправления (v0.0.92...` — замена пустой строки + заголовка, куда перед заголовком вставляется новый контент, а сам заголовок остаётся. `patch_note(oldString="\n\n## Исправления (v0.0.92...")` — найдено и отработало.
- **2026-05-23**: Taiga deleted 32+ wiki files. Root cause: `git add -A` before merge staged deletions of files outside sparse cone. Fixed with scoped `git add`.
- **2026-06-01**: "Керамика" lost from gift-ideas. Root cause: `git add $SPARSE_PATHS` failed silently when `.obsidian/` absent — patch+move not committed.
- **2026-06-02**: Full architecture migration. Taiga `/vault` converted from `GIT_DIR=bare GIT_WORK_TREE` workaround to proper sparse clone. All clients unified on same `commit→fetch→merge→push` algorithm. Sync interval: every 5 min.
## iPad iOS9.3.5 — подключён 2026-10-15
Прямой SSH Git клиент того же TrueNAS bare main, Git2.8.1 от Saurik, отдельный restricted Ed25519 key (только этот repo), persisted TrueNAS user settings. Vault в Documents Edhita/ObsidianVault; task работает от mobile каждые300с и при load. Проверены actual iPad→NAS push и NAS→iPad automatic pull на двух последовательных ticks. Native fixture tests повторены 2026-10-15: offline сохранение/retry, commit/push конфликтных маркеров с обеими версиями, получение другим clone, продолжение синка и возобновление прерванного merge. Script/last-success/last-result/error logs находятся вне Git vault; core.fileMode=false, auto gc отключён. При конфликте, как на Mac, client коммитит маркеры с пометкой `merge conflict — review needed` и пушит; ручное разрешение не блокирует последующие ticks. Скрипты прочих клиентов не менялись. Полные пути, source/evidence и обслуживание: [[personal/how-to/ipad-vault-sync]].