166 lines
12 KiB
Markdown
166 lines
12 KiB
Markdown
---
|
||
title: Vault Git Sync — Architecture & Scripts
|
||
updated: '2026-09-02'
|
||
type: tech
|
||
tags:
|
||
- vault
|
||
- git
|
||
- sync
|
||
- infra
|
||
- obsidian
|
||
---
|
||
|
||
# Vault Git Sync
|
||
|
||
> **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).
|
||
>
|
||
> **⛔ НОВАЯ проблема 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`
|
||
|
||
Four clients sync to it: Eagle, Kraken, Taiga (vault), Taiga (syncthing/phone).
|
||
|
||
## 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 |
|
||
|
||
> **⭐ УТОЧНЕНО 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`).
|
||
|
||
## 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
|
||
|
||
- `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 = `filesystem.setacl <repo> ... {stripacl:true}` (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode `40` (`r--------`) on loose objects vs normal `750`.
|
||
|
||
## History
|
||
|
||
- **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.
|