704 lines
24 KiB
Markdown
704 lines
24 KiB
Markdown
# Backup & Recovery
|
||
|
||
**Версия:** 0.1.0-draft
|
||
**Дата:** 2026-07-20
|
||
**Статус:** Draft
|
||
**Зависит от:** `SPECIFICATION.md` (§10 Data lifecycle, §7.2 Reliability), `docs/adapters/HERMES.md`
|
||
|
||
> Этот документ описывает процедуры бэкапа и восстановления конфигурации Hermes Agent в составе Agent Control Center. Реализация запрещена до G0/G1 и утверждения оператором.
|
||
|
||
## 1. Обзор
|
||
|
||
Система бэкапа ACC охватывает:
|
||
- Конфигурацию Hermes Agent (config.yaml, .env, профили)
|
||
- Пользовательские навыки (skills)
|
||
- Память (MEMORY.md, USER.md)
|
||
- Сессионные данные (опционально)
|
||
|
||
Бэкап НЕ включает:
|
||
- Секреты (API-ключи) — управляются отдельно через secret manager
|
||
- Бинарные артефакты и кэши (audio_cache, сессионные файлы)
|
||
- Логи (сохраняются согласно retention policy)
|
||
|
||
## 2. Структура конфигурации Hermes
|
||
|
||
### 2.1 Основные компоненты
|
||
|
||
```
|
||
~/.hermes/
|
||
├── config.yaml # Основная конфигурация
|
||
├── .env # Переменные окружения (API-ключи, секреты)
|
||
├── auth.json # OAuth-токены и credential pools
|
||
├── state.db # SQLite база сессий (state store)
|
||
├── profiles/ # Профили
|
||
│ ├── default/
|
||
│ │ ├── config.yaml
|
||
│ │ ├── .env
|
||
│ │ ├── skills/
|
||
│ │ └── memories/
|
||
│ ├── worker-code/
|
||
│ │ └── ...
|
||
│ ├── worker-fast/
|
||
│ │ └── ...
|
||
│ ├── worker-research/
|
||
│ │ └── ...
|
||
│ └── worker-review/
|
||
│ └── ...
|
||
├── skills/ # Пользовательские навыки
|
||
│ ├── *.md # SKILL.md файлы
|
||
│ └── .usage.json # Метаданные curator'а
|
||
├── sessions/ # Сессионные файлы (опционально)
|
||
├── logs/ # Логи (НЕ бэкапируются)
|
||
├── audio_cache/ # Аудиокэш (НЕ бэкапируется)
|
||
└── backups/ # Локальные снапшоты
|
||
├── latest/ # Последний снапшот
|
||
│ ├── config.yaml
|
||
│ ├── config.yaml.sig
|
||
│ └── manifest.json
|
||
└── archive/ # Архив снапшотов
|
||
└── 2026-07-20T14-00-00Z.tar.gz
|
||
```
|
||
|
||
### 2.2 Приоритеты бэкапа
|
||
|
||
| Компонент | Приоритет | RPO | Без секретов |
|
||
|---|---|---|---|
|
||
| `config.yaml` (все профили) | CRITICAL | 1 час | Да |
|
||
| `skills/` | HIGH | 6 часов | Да |
|
||
| `memories/` (MEMORY.md, USER.md) | HIGH | 6 часов | Да |
|
||
| `profiles/*/config.yaml` | HIGH | 1 час | Да |
|
||
| `cron/` конфигурации | MEDIUM | 24 часа | Да |
|
||
| `sessions/` | LOW | 24 часа | Нет (секреты в сессиях) |
|
||
| `state.db` | LOW | 24 часа | Нет |
|
||
|
||
## 3. Процедура снапшота
|
||
|
||
### 3.1 Скрипт: `scripts/update_backup.py`
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
update_backup.py — создание снапшота конфигурации Hermes Agent.
|
||
|
||
Использование:
|
||
python scripts/update_backup.py [--output DIR] [--exclude-secrets] [--dry-run]
|
||
|
||
Снапшот включает:
|
||
- config.yaml всех профилей
|
||
- .env (с redacted секретами при --exclude-secrets)
|
||
- skills/ (только SKILL.md файлы)
|
||
- memories/ (MEMORY.md, USER.md)
|
||
- cron/ конфигурации
|
||
|
||
Исключает:
|
||
- sessions/
|
||
- logs/
|
||
- audio_cache/
|
||
- state.db (опционально)
|
||
- auth.json (секреты)
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import shutil
|
||
import tarfile
|
||
import hashlib
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
||
BACKUP_DIR = HERMES_HOME / "backups"
|
||
LATEST_DIR = BACKUP_DIR / "latest"
|
||
ARCHIVE_DIR = BACKUP_DIR / "archive"
|
||
|
||
# Директории для бэкапа
|
||
INCLUDE_DIRS = [
|
||
HERMES_HOME / "skills",
|
||
HERMES_HOME / "profiles",
|
||
]
|
||
|
||
# Файлы для бэкапа
|
||
INCLUDE_FILES = [
|
||
HERMES_HOME / "config.yaml",
|
||
HERMES_HOME / ".env",
|
||
]
|
||
|
||
# Директории для исключения
|
||
EXCLUDE_PATTERNS = [
|
||
"**/sessions/",
|
||
"**/logs/",
|
||
"**/audio_cache/",
|
||
"**/state.db",
|
||
"**/auth.json",
|
||
"**/__pycache__/",
|
||
"**/*.pyc",
|
||
]
|
||
|
||
def create_snapshot(output_dir: Path, exclude_secrets: bool = False):
|
||
"""Создать снапшот конфигурации."""
|
||
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%SZ")
|
||
snapshot_dir = output_dir / timestamp
|
||
|
||
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
manifest = {
|
||
"version": "0.1.0",
|
||
"created_at": timestamp,
|
||
"hermes_home": str(HERMES_HOME),
|
||
"files": [],
|
||
"exclude_secrets": exclude_secrets,
|
||
}
|
||
|
||
# Копирование конфигурационных файлов
|
||
for src in INCLUDE_FILES:
|
||
if src.exists():
|
||
dst = snapshot_dir / src.relative_to(HERMES_HOME)
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(src, dst)
|
||
|
||
# Redact секреты в .env если нужно
|
||
if exclude_secrets and dst.name == ".env":
|
||
redact_env_secrets(dst)
|
||
|
||
file_hash = hash_file(dst)
|
||
manifest["files"].append({
|
||
"path": str(src.relative_to(HERMES_HOME)),
|
||
"hash": file_hash,
|
||
"size": dst.stat().st_size,
|
||
})
|
||
|
||
# Копирование директорий с исключениями
|
||
for src_dir in INCLUDE_DIRS:
|
||
if src_dir.exists() and src_dir.is_dir():
|
||
_copy_with_exclusions(
|
||
src_dir,
|
||
snapshot_dir / src_dir.relative_to(HERMES_HOME),
|
||
manifest,
|
||
)
|
||
|
||
# Сохранение манифеста
|
||
manifest_path = snapshot_dir / "manifest.json"
|
||
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
|
||
|
||
# Обновление симлинка latest
|
||
if LATEST_DIR.exists():
|
||
if LATEST_DIR.is_symlink():
|
||
LATEST_DIR.unlink()
|
||
else:
|
||
shutil.rmtree(LATEST_DIR)
|
||
os.symlink(snapshot_dir, LATEST_DIR, target_is_directory=True)
|
||
|
||
# Создание архива
|
||
archive_path = ARCHIVE_DIR / f"{timestamp}.tar.gz"
|
||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with tarfile.open(archive_path, "w:gz") as tar:
|
||
tar.add(snapshot_dir, arcname=timestamp)
|
||
|
||
print(f"Snapshot created: {snapshot_dir}")
|
||
print(f"Archive: {archive_path}")
|
||
print(f"Manifest: {len(manifest['files'])} files")
|
||
|
||
# Очистка старых архивов (> 30 дней)
|
||
cleanup_old_archives(days=30)
|
||
|
||
def redact_env_secrets(env_path: Path):
|
||
"""Заменить значения секретов на REDACTED."""
|
||
content = env_path.read_text()
|
||
redacted_lines = []
|
||
for line in content.split("\n"):
|
||
if "=" in line and not line.startswith("#"):
|
||
key, _ = line.split("=", 1)
|
||
if any(s in key.upper() for s in ["KEY", "TOKEN", "SECRET", "PASSWORD"]):
|
||
redacted_lines.append(f"{key}=REDACTED")
|
||
else:
|
||
redacted_lines.append(line)
|
||
else:
|
||
redacted_lines.append(line)
|
||
env_path.write_text("\n".join(redacted_lines))
|
||
|
||
def hash_file(path: Path) -> str:
|
||
"""SHA-256 хеш файла."""
|
||
sha = hashlib.sha256()
|
||
with open(path, "rb") as f:
|
||
for chunk in iter(lambda: f.read(8192), b""):
|
||
sha.update(chunk)
|
||
return sha.hexdigest()
|
||
|
||
def cleanup_old_archives(days: int = 30):
|
||
"""Удалить архивы старше N дней."""
|
||
cutoff = datetime.now(timezone.utc).timestamp() - days * 86400
|
||
for archive in ARCHIVE_DIR.glob("*.tar.gz"):
|
||
if archive.stat().st_mtime < cutoff:
|
||
archive.unlink()
|
||
print(f"Removed old archive: {archive.name}")
|
||
|
||
|
||
def _copy_with_exclusions(src: Path, dst: Path, manifest: dict):
|
||
"""Копировать директорию с исключениями."""
|
||
from fnmatch import fnmatch
|
||
|
||
for root, dirs, files in os.walk(src):
|
||
root_path = Path(root)
|
||
|
||
# Пропустить исключённые директории
|
||
dirs[:] = [
|
||
d for d in dirs
|
||
if not any(fnmatch(str(root_path / d), pat) for pat in EXCLUDE_PATTERNS)
|
||
]
|
||
|
||
for fname in files:
|
||
fpath = root_path / fname
|
||
if any(fnmatch(str(fpath), pat) for pat in EXCLUDE_PATTERNS):
|
||
continue
|
||
|
||
rel = fpath.relative_to(HERMES_HOME)
|
||
dest = dst / rel.relative_to(src)
|
||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(fpath, dest)
|
||
|
||
file_hash = hash_file(dest)
|
||
manifest["files"].append({
|
||
"path": str(rel),
|
||
"hash": file_hash,
|
||
"size": dest.stat().st_size,
|
||
})
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import argparse
|
||
parser = argparse.ArgumentParser(description="Hermes config snapshot")
|
||
parser.add_argument("--output", type=Path, default=BACKUP_DIR,
|
||
help="Output directory")
|
||
parser.add_argument("--exclude-secrets", action="store_true",
|
||
help="Redact secrets in .env files")
|
||
parser.add_argument("--dry-run", action="store_true",
|
||
help="Show what would be backed up, without copying")
|
||
args = parser.parse_args()
|
||
|
||
if args.dry_run:
|
||
print("Files to backup:")
|
||
for f in INCLUDE_FILES:
|
||
if f.exists():
|
||
print(f" {f}")
|
||
for d in INCLUDE_DIRS:
|
||
if d.exists():
|
||
print(f" {d}/")
|
||
else:
|
||
create_snapshot(args.output, exclude_secrets=args.exclude_secrets)
|
||
```
|
||
|
||
### 3.2 Ручной снапшот
|
||
|
||
```bash
|
||
# Стандартный снапшот
|
||
python scripts/update_backup.py
|
||
|
||
# Снапшот с redacted секретами (для хранения в Git)
|
||
python scripts/update_backup.py --exclude-secrets
|
||
|
||
# Предварительный просмотр
|
||
python scripts/update_backup.py --dry-run
|
||
|
||
# В указанную директорию
|
||
python scripts/update_backup.py --output /mnt/backups/hermes/
|
||
```
|
||
|
||
### 3.3 Автоматизация через cron
|
||
|
||
```bash
|
||
# Добавить задание в crontab
|
||
# Ежечасный снапшот с исключением секретов
|
||
0 * * * * cd ~/acc-local && python scripts/update_backup.py --exclude-secrets --output ~/.hermes/backups/
|
||
|
||
# Ежедневный полный снапшот
|
||
0 2 * * * cd ~/acc-local && python scripts/update_backup.py --output ~/.hermes/backups/
|
||
```
|
||
|
||
## 4. Процедура восстановления
|
||
|
||
### 4.1 Скрипт: `scripts/restore_backup.py`
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""
|
||
restore_backup.py — восстановление конфигурации Hermes Agent из снапшота.
|
||
|
||
Использование:
|
||
python scripts/restore_backup.py --snapshot DIR
|
||
python scripts/restore_backup.py --latest
|
||
python scripts/restore_backup.py --list
|
||
python scripts/restore_backup.py --verify DIR
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import shutil
|
||
import hashlib
|
||
from pathlib import Path
|
||
from datetime import datetime, timezone
|
||
|
||
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
||
BACKUP_DIR = HERMES_HOME / "backups"
|
||
LATEST_DIR = BACKUP_DIR / "latest"
|
||
ARCHIVE_DIR = BACKUP_DIR / "archive"
|
||
RESTORE_DIR = HERMES_HOME / "restore_tmp"
|
||
|
||
|
||
def list_snapshots():
|
||
"""Показать доступные снапшоты."""
|
||
if not ARCHIVE_DIR.exists():
|
||
print("No archives found.")
|
||
return
|
||
|
||
archives = sorted(ARCHIVE_DIR.glob("*.tar.gz"), reverse=True)
|
||
for i, arc in enumerate(archives):
|
||
size_mb = arc.stat().st_size / (1024 * 1024)
|
||
mtime = datetime.fromtimestamp(arc.stat().st_mtime)
|
||
print(f" [{i}] {arc.name} {size_mb:.1f} MB {mtime.isoformat()}")
|
||
|
||
|
||
def verify_snapshot(snapshot_dir: Path):
|
||
"""Проверить целостность снапшота."""
|
||
manifest_path = snapshot_dir / "manifest.json"
|
||
if not manifest_path.exists():
|
||
print("ERROR: manifest.json not found")
|
||
return False
|
||
|
||
manifest = json.loads(manifest_path.read_text())
|
||
all_ok = True
|
||
|
||
for fspec in manifest.get("files", []):
|
||
fpath = snapshot_dir / fspec["path"]
|
||
if not fpath.exists():
|
||
print(f" MISSING: {fspec['path']}")
|
||
all_ok = False
|
||
continue
|
||
|
||
actual_hash = hashlib.sha256(fpath.read_bytes()).hexdigest()
|
||
if actual_hash != fspec["hash"]:
|
||
print(f" HASH MISMATCH: {fspec['path']}")
|
||
all_ok = False
|
||
|
||
if all_ok:
|
||
print(f"VERIFIED: {len(manifest['files'])} files OK")
|
||
return all_ok
|
||
|
||
|
||
def restore_snapshot(snapshot_dir: Path, dry_run: bool = False):
|
||
"""Восстановить конфигурацию из снапшота."""
|
||
manifest_path = snapshot_dir / "manifest.json"
|
||
manifest = json.loads(manifest_path.read_text())
|
||
|
||
print(f"Restoring from: {snapshot_dir}")
|
||
print(f"Created: {manifest.get('created_at', 'unknown')}")
|
||
print(f"Files: {len(manifest['files'])}")
|
||
print(f"{'[DRY RUN] ' if dry_run else ''}")
|
||
|
||
conflicts = []
|
||
|
||
for fspec in manifest.get("files", []):
|
||
src = snapshot_dir / fspec["path"]
|
||
dst = HERMES_HOME / fspec["path"]
|
||
|
||
if not src.exists():
|
||
print(f" SKIP (missing): {fspec['path']}")
|
||
continue
|
||
|
||
# Проверить конфликт
|
||
if dst.exists() and not dry_run:
|
||
dst_hash = hashlib.sha256(dst.read_bytes()).hexdigest()
|
||
if dst_hash != fspec["hash"]:
|
||
conflicts.append(fspec["path"])
|
||
|
||
if dry_run:
|
||
action = "CONFLICT" if dst.exists() else "CREATE"
|
||
print(f" [{action}] {fspec['path']}")
|
||
else:
|
||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(src, dst)
|
||
print(f" RESTORED: {fspec['path']}")
|
||
|
||
if conflicts:
|
||
print(f"\nWARNING: {len(conflicts)} files had local modifications (overwritten):")
|
||
for p in conflicts:
|
||
print(f" - {p}")
|
||
|
||
if dry_run:
|
||
print(f"\nDry run complete. {len(manifest['files'])} files would be restored.")
|
||
|
||
print("Restore complete.")
|
||
print("NOTE: .env files were backed up with redacted secrets.")
|
||
print(" Manually restore API keys and secrets after restore.")
|
||
|
||
|
||
def restore_from_archive(archive_path: Path):
|
||
"""Восстановить из tar.gz архива."""
|
||
import tarfile
|
||
|
||
extract_dir = RESTORE_DIR / archive_path.stem
|
||
extract_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
with tarfile.open(archive_path, "r:gz") as tar:
|
||
tar.extractall(extract_dir)
|
||
|
||
# Найти директорию снапшота внутри архива
|
||
snapshots = list(extract_dir.glob("*/manifest.json"))
|
||
if not snapshots:
|
||
print("ERROR: No manifest.json found in archive")
|
||
return
|
||
|
||
snapshot_dir = snapshots[0].parent
|
||
restore_snapshot(snapshot_dir)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import argparse
|
||
parser = argparse.ArgumentParser(description="Hermes config restore")
|
||
parser.add_argument("--snapshot", type=Path,
|
||
help="Snapshot directory to restore from")
|
||
parser.add_argument("--latest", action="store_true",
|
||
help="Restore from latest snapshot")
|
||
parser.add_argument("--list", action="store_true",
|
||
help="List available snapshots")
|
||
parser.add_argument("--verify", type=Path,
|
||
help="Verify snapshot integrity")
|
||
parser.add_argument("--dry-run", action="store_true",
|
||
help="Show what would be restored")
|
||
args = parser.parse_args()
|
||
|
||
if args.list:
|
||
list_snapshots()
|
||
elif args.verify:
|
||
verify_snapshot(Path(args.verify))
|
||
elif args.latest:
|
||
if not LATEST_DIR.exists():
|
||
print("ERROR: No latest snapshot found")
|
||
exit(1)
|
||
if args.dry_run:
|
||
restore_snapshot(LATEST_DIR, dry_run=True)
|
||
else:
|
||
verify_snapshot(LATEST_DIR)
|
||
restore_snapshot(LATEST_DIR)
|
||
elif args.snapshot:
|
||
snapshot = Path(args.snapshot)
|
||
if snapshot.suffix == ".gz":
|
||
restore_from_archive(snapshot)
|
||
else:
|
||
if args.dry_run:
|
||
restore_snapshot(snapshot, dry_run=True)
|
||
else:
|
||
verify_snapshot(snapshot)
|
||
restore_snapshot(snapshot)
|
||
else:
|
||
parser.print_help()
|
||
```
|
||
|
||
### 4.2 Ручное восстановление
|
||
|
||
```bash
|
||
# Список доступных снапшотов
|
||
python scripts/restore_backup.py --list
|
||
|
||
# Восстановить последний снапшот
|
||
python scripts/restore_backup.py --latest
|
||
|
||
# Восстановить конкретный снапшот
|
||
python scripts/restore_backup.py --snapshot ~/.hermes/backups/2026-07-20T14-00-00Z
|
||
|
||
# Восстановить из архива
|
||
python scripts/restore_backup.py --snapshot ~/.hermes/backups/archive/2026-07-20T14-00-00Z.tar.gz
|
||
|
||
# Проверить целостность без восстановления
|
||
python scripts/restore_backup.py --verify ~/.hermes/backups/latest
|
||
|
||
# Предварительный просмотр
|
||
python scripts/restore_backup.py --latest --dry-run
|
||
```
|
||
|
||
## 5. Git-репозиторий как хранилище
|
||
|
||
### 5.1 Структура репозитория `ochenstarik-ui/hermes-config-backup`
|
||
|
||
```
|
||
hermes-config-backup/
|
||
├── README.md
|
||
├── config/
|
||
│ ├── default.yaml # Без секретов
|
||
│ ├── worker-code.yaml
|
||
│ ├── worker-fast.yaml
|
||
│ ├── worker-research.yaml
|
||
│ └── worker-review.yaml
|
||
├── skills/ # Пользовательские навыки
|
||
│ └── *.md
|
||
├── memories/ # Память (без чувствительных данных)
|
||
│ ├── MEMORY.md
|
||
│ └── USER.md
|
||
├── scripts/
|
||
│ ├── update_backup.py
|
||
│ ├── restore_backup.py
|
||
│ └── sync_to_git.sh
|
||
└── .gitignore
|
||
```
|
||
|
||
### 5.2 `.gitignore`
|
||
|
||
```gitignore
|
||
# Исключить файлы с секретами
|
||
.env
|
||
*.env
|
||
auth.json
|
||
credentials/
|
||
secrets/
|
||
|
||
# Исключить чувствительные данные сессий
|
||
sessions/
|
||
state.db
|
||
|
||
# Исключить временные файлы
|
||
*.pyc
|
||
__pycache__/
|
||
backups/archive/
|
||
backups/latest/
|
||
```
|
||
|
||
### 5.3 Синхронизация: `scripts/sync_to_git.sh`
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# sync_to_git.sh — синхронизация снапшота в Git-репозиторий
|
||
|
||
set -euo pipefail
|
||
|
||
REPO_DIR="$HOME/hermes-config-backup"
|
||
SNAPSHOT_DIR="$HOME/.hermes/backups/latest"
|
||
|
||
cd "$REPO_DIR"
|
||
|
||
# Копировать файлы из снапшота (без секретов)
|
||
if [ -d "$SNAPSHOT_DIR" ]; then
|
||
# config.yaml профилей
|
||
cp "$SNAPSHOT_DIR/config.yaml" config/default.yaml 2>/dev/null || true
|
||
cp "$SNAPSHOT_DIR/profiles/worker-code/config.yaml" config/worker-code.yaml 2>/dev/null || true
|
||
cp "$SNAPSHOT_DIR/profiles/worker-fast/config.yaml" config/worker-fast.yaml 2>/dev/null || true
|
||
cp "$SNAPSHOT_DIR/profiles/worker-research/config.yaml" config/worker-research.yaml 2>/dev/null || true
|
||
cp "$SNAPSHOT_DIR/profiles/worker-review/config.yaml" config/worker-review.yaml 2>/dev/null || true
|
||
|
||
# Навыки
|
||
rm -rf skills/
|
||
cp -r "$SNAPSHOT_DIR/skills/" skills/ 2>/dev/null || true
|
||
|
||
# Память
|
||
mkdir -p memories/
|
||
cp "$SNAPSHOT_DIR/profiles/default/memories/MEMORY.md" memories/ 2>/dev/null || true
|
||
cp "$SNAPSHOT_DIR/profiles/default/memories/USER.md" memories/ 2>/dev/null || true
|
||
fi
|
||
|
||
# Commit и push
|
||
git add -A
|
||
if git diff --cached --quiet; then
|
||
echo "No changes to commit."
|
||
else
|
||
git commit -m "backup: $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||
git push origin main
|
||
echo "Backup synced to GitHub."
|
||
fi
|
||
```
|
||
|
||
## 6. Исключение секретов
|
||
|
||
| Компонент | Статус секретов | Действие при бэкапе |
|
||
|---|---|---|
|
||
| `config.yaml` | Может содержать `api_key` references | Проверить на прямые ключи; использовать `${ENV_VAR}` синтаксис |
|
||
| `.env` | Содержит API-ключи | REDACT при `--exclude-secrets`; NEVER commit в Git |
|
||
| `auth.json` | OAuth-токены | Исключён из бэкапа полностью |
|
||
| `state.db` | Может содержать ключи в сессиях | Исключён из бэкапа |
|
||
| `skills/` | Обычно без секретов | Бэкапируется; проверить на embedded ключи |
|
||
| `memories/` | Может содержать косвенные ссылки | Бэкапируется; проверить на чувствительные данные |
|
||
|
||
### 6.1 Проверка на секреты перед commit
|
||
|
||
```bash
|
||
# Проверить снапшот на наличие паттернов секретов
|
||
grep -rE "(sk-|api_key|token|secret|password|-----BEGIN)" ~/.hermes/backups/latest/ \
|
||
--exclude-dir=sessions --exclude="*.pyc" && echo "WARNING: Potential secrets found!"
|
||
```
|
||
|
||
## 7. Процедуры
|
||
|
||
### 7.1 Плановый бэкап (ежедневно)
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# daily_backup.sh
|
||
|
||
echo "=== Daily backup $(date) ==="
|
||
|
||
# 1. Создать снапшот
|
||
python scripts/update_backup.py --exclude-secrets
|
||
echo "Snapshot created."
|
||
|
||
# 2. Проверить целостность
|
||
python scripts/restore_backup.py --verify ~/.hermes/backups/latest
|
||
echo "Integrity verified."
|
||
|
||
# 3. Синхронизировать в Git
|
||
bash scripts/sync_to_git.sh
|
||
echo "Git sync complete."
|
||
|
||
# 4. Очистить старые архивы (> 30 дней)
|
||
find ~/.hermes/backups/archive/ -name "*.tar.gz" -mtime +30 -delete
|
||
echo "Old archives cleaned."
|
||
```
|
||
|
||
### 7.2 Восстановление после сбоя
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# disaster_recovery.sh
|
||
|
||
echo "=== Disaster Recovery $(date) ==="
|
||
echo "WARNING: This will overwrite current Hermes configuration!"
|
||
read -p "Continue? (yes/no): " confirm
|
||
[[ "$confirm" != "yes" ]] && exit 0
|
||
|
||
# 1. Создать аварийный снапшот текущего состояния
|
||
python scripts/update_backup.py --output ~/.hermes/backups/pre_recovery
|
||
echo "Pre-recovery snapshot created."
|
||
|
||
# 2. Восстановить из последнего снапшота
|
||
python scripts/restore_backup.py --latest
|
||
echo "Config restored."
|
||
|
||
# 3. Проверить конфигурацию
|
||
hermes config check
|
||
echo "Config check complete."
|
||
|
||
# 4. Проверить health
|
||
hermes doctor
|
||
echo "Health check complete."
|
||
|
||
# 5. Перезапустить gateway
|
||
hermes gateway restart
|
||
echo "Gateway restarted."
|
||
|
||
echo "=== Recovery complete ==="
|
||
echo "NOTE: API keys (.env) were redacted in backup."
|
||
echo " Manually restore API keys if needed."
|
||
```
|
||
|
||
## 8. RPO и RTO
|
||
|
||
| Сценарий | RPO | RTO | Процедура |
|
||
|---|---|---|---|
|
||
| Повреждение `config.yaml` | ≤ 1 час | ≤ 5 мин | `restore_backup.py --latest` |
|
||
| Потеря навыков | ≤ 6 часов | ≤ 10 мин | `restore_backup.py --latest` |
|
||
| Полная потеря `~/.hermes/` | ≤ 1 час (config) / ≤ 24 часа (sessions) | ≤ 30 мин | `disaster_recovery.sh` |
|
||
| Миграция на новый сервер | ≤ 1 час | ≤ 1 час | Клонировать Git-репозиторий + восстановить секреты |
|
||
|
||
## 9. Ссылки
|
||
|
||
- [HERMES.md](../adapters/HERMES.md) — спецификация Hermes-адаптера (конфигурация)
|
||
- [MONITORING.md](MONITORING.md) — мониторинг и алерты (включая ALT-BKP-001)
|
||
- [CREDENTIAL_POOLS.md](../patterns/CREDENTIAL_POOLS.md) — управление ключами
|
||
- [SPECIFICATION.md](../SPECIFICATION.md) §10 — Data lifecycle
|
||
- [SPECIFICATION.md](../SPECIFICATION.md) §7.2 — Reliability, consistency and recovery
|