351 lines
38 KiB
Markdown
Executable File
351 lines
38 KiB
Markdown
Executable File
---
|
||
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]].
|