"""Simbiot Harness — единая карта персистентных путей (SIMBIOT_HOME). t-261004113351-2858c1 (мультитенантность): память и знания инстанса — данные, а не код. Репозиторий = чистый код; всё, что живёт дольше процесса (ES-база, RAG-индекс знаний, входящие каналы, выжимки, секреты, доки инстанса), размещается в одном каталоге-хоме. SIMBIOT_HOME (env) — корень данных инстанса. Задан → структура: /db/harness.db — ES-база (события, проекции, delivery-леджер) /rag/rag.db — RAG-индекс знаний (векторные чанки) /env/.env — секреты инстанса (токены, ключи роутера) /inbox/ — входящие каналов (portal/state, legacy outbox) /digests/ — файлы-выжимки уровней (summary_ref) /audit/ — ключи крипто-аудита (ed25519) /docs/ — проектные документы инстанса (aum/balances.json) /voice/ — кэш голосовых (если используется ASR) /run/ — runtime-штампы каналов (channel_.json) Не задан (legacy) → всё по-старому из корня репо: harness/harness.db, .env, docs/, out/, .audit/ — обратная совместимость существующих установок и systemd-юнитов без правки окружения. Приоритет источников (12-фактор): точечная env-переменная (SIMBIOT_DB, SIMBIOT_RAG_DB, SIMBIOT_AUDIT_KEY_DIR, ...) сильнее схемы SIMBIOT_HOME, SIMBIOT_HOME сильнее legacy-дефолта. """ import os _HARNESS_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.dirname(_HARNESS_DIR) _HOME = os.environ.get("SIMBIOT_HOME", "").strip() IS_ISOLATED = bool(_HOME) def home() -> str: """Корень данных инстанса (SIMBIOT_HOME или корень репо — legacy).""" return _HOME or REPO_ROOT # ── ES-база: события/проекции/идемпотентная доставка ──────────────────────── def db_path() -> str: env = os.environ.get("SIMBIOT_DB", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "db", "harness.db") return os.path.join(_HARNESS_DIR, "harness.db") # legacy: как было # ── .env: секреты инстанса ────────────────────────────────────────────────── def env_path() -> str: if IS_ISOLATED: return os.path.join(_HOME, "env", ".env") return os.path.join(REPO_ROOT, ".env") # ── RAG-индекс знаний ─────────────────────────────────────────────────────── def rag_db() -> str: env = os.environ.get("SIMBIOT_RAG_DB", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "rag", "rag.db") return "/root/rag/rag.db" # legacy (общая база кооп-хоста, как было) # ── выжимки уровней (файлы summary_ref) ───────────────────────────────────── def digests_dir() -> str: if IS_ISOLATED: return os.path.join(_HOME, "digests") return os.path.join(REPO_ROOT, "out") # legacy out/ # ── проектные документы инстанса ──────────────────────────────────────────── def docs_dir() -> str: if IS_ISOLATED: return os.path.join(_HOME, "docs") return os.path.join(REPO_ROOT, "docs") # ── ключи крипто-аудита ───────────────────────────────────────────────────── def audit_key_dir() -> str: env = os.environ.get("SIMBIOT_AUDIT_KEY_DIR", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "audit") return os.path.join(REPO_ROOT, ".audit") # ── входящие каналов: state + legacy-каталоги файлопередачи ───────────────── def channel_state_file() -> str: if IS_ISOLATED: return os.path.join(_HOME, "inbox", ".channel-last") return os.path.join(REPO_ROOT, "portal", "inbox", ".channel-last") def inbox_pending_dir() -> str: if IS_ISOLATED: return os.path.join(_HOME, "inbox", "pending") return os.path.join(REPO_ROOT, "portal", "inbox", "pending") def inbox_outbox_dir() -> str: if IS_ISOLATED: return os.path.join(_HOME, "inbox", "outbox") return os.path.join(REPO_ROOT, "portal", "inbox", "outbox") # ── входящие файлы каналов (t-261005135853): documents/photo на диск ──────── def inbox_files_dir() -> str: """Каталог входящих файлов (documents/photo из TG). Голосовые живут в voice/ (кэш ASR), а документы/фото — данные уровня инбокса: выписки, сканы, артефакты Алекса. Не задаём SIMBIOT_HOME (legacy) → /var/lib/simbiot-harness/inbox/files — тот же корень, где голосовые (voice_dir legacy), рядом с inbox/ каналов портала. """ env = os.environ.get("SIMBIOT_INBOX_FILES_DIR", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "inbox", "files") return "/var/lib/simbiot-harness/inbox/files" # ── runtime-штампы каналов для дашборда ───────────────────────────────────── def stamp_dir() -> str: if IS_ISOLATED: return os.path.join(_HOME, "run") return REPO_ROOT # legacy: /harness/channel_.json # ── кэш голосовых ─────────────────────────────────────────────────────────── def voice_dir() -> str: env = os.environ.get("SIMBIOT_VOICE_DIR", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "voice") return "/var/lib/simbiot-harness/voice" # legacy # ── токен агентского MCP портала (t-261010093054) ────────────────────────── def portal_mcp_token_path() -> str: """Путь к токену MCP-агента портала (см. harness/portal_mcp.py). Данные инстанса, не код: secrets/ в SIMBIOT_HOME, legacy — файл .simbiot-portal-token в корне репо (выдан при подключении Simbiot как MCP-агента кооператива, 02.10). """ env = os.environ.get("SIMBIOT_PORTAL_MCP_TOKEN_FILE", "").strip() if env: return env if IS_ISOLATED: return os.path.join(_HOME, "secrets", "portal-mcp-token") return os.path.join(REPO_ROOT, ".simbiot-portal-token") def ensure_instance_dirs() -> list: """Создать структуру хома изолированного инстанса. Идемпотентно. В legacy-режиме ничего не создаёт (там всё уже лежит как лежало). Возвращает список созданных каталогов. """ if not IS_ISOLATED: return [] made = [] for sub in ("db", "rag", "env", "inbox", "digests", "audit", "docs", "voice", "run", "secrets"): p = os.path.join(_HOME, sub) if not os.path.isdir(p): os.makedirs(p, exist_ok=True) made.append(p) return made