From e65d205e0f8af51a3333477265f4fe58293f51da Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Fri, 2 Oct 2026 00:34:07 +0000 Subject: [PATCH] =?UTF-8?q?feat(day):=20=D0=BA=D1=80=D0=B8=D0=BF=D1=82?= =?UTF-8?q?=D0=BE=D0=B0=D1=83=D0=B4=D0=B8=D1=82=20=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?/=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20(ed25519,=20genesis-?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0),=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BA=D0=B0=20AUM=20(=D0=B7=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=80+policy+=D0=BF=D0=B8=D0=BB=D0=BE=D1=82),=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B0=D0=BB-=D1=80=D0=B0=D0=B7=D0=B2=D1=91?= =?UTF-8?q?=D1=80=D1=82=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20(t-260930083?= =?UTF-8?q?716,=20t-260929161138,=20t-260930110622)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 + README.md | 16 ++ cli.py | 91 +++++++ docs/PRICE-DIRECTIVE-SIMBIOT.md | 35 +++ docs/audit.md | 71 +++++ docs/aum.md | 48 ++++ docs/aum/aum-pilot-2026-10-02.md | 34 +++ docs/aum/template-balances.json | 11 + .../digests/9router-spend-2026-09-25_10-02.md | 31 +++ docs/self-diagnostics.html | 91 +++++++ docs/simbiot_presale_landing.html | 140 ++++++++++ harness/audit.py | 256 ++++++++++++++++++ harness/aum.py | 227 ++++++++++++++++ harness/es.py | 96 ++++++- harness/scheduler.py | 19 +- tests/test_audit.py | 139 ++++++++++ tests/test_aum.py | 132 +++++++++ 17 files changed, 1441 insertions(+), 5 deletions(-) create mode 100644 docs/PRICE-DIRECTIVE-SIMBIOT.md create mode 100644 docs/audit.md create mode 100644 docs/aum.md create mode 100644 docs/aum/aum-pilot-2026-10-02.md create mode 100644 docs/aum/template-balances.json create mode 100644 docs/digests/9router-spend-2026-09-25_10-02.md create mode 100644 docs/self-diagnostics.html create mode 100644 docs/simbiot_presale_landing.html create mode 100644 harness/audit.py create mode 100644 harness/aum.py create mode 100644 tests/test_audit.py create mode 100644 tests/test_aum.py diff --git a/.gitignore b/.gitignore index 6cd34b5..32092a4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,12 @@ __pycache__/ .env tg_inbox.db simbiot.db +# криптоаудит (t-260930083716): приватный ключ не попадает в репо +.audit/ +.simbiot-portal-token +# рабочие БД харнеса (состояние — не код) +harness/es.db +harness/harness_es.db +harness/hg_tg_inbox.db +hg_tg_inbox.db +store.db diff --git a/README.md b/README.md index 0823b51..d5624f4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,22 @@ Strands Harness SDK как исполнительное ядро, модели цели стоит автоприоритизация доски задач (t-260929161138; уточнение автоприоритизации день+час — t-260930153845, задача e-260930161330). +Механика (t-260929161138, docs/aum.md): `cli.py aum --record` — снапшот +метрики (AUM из docs/aum/balances.json + burn 9router за сутки/неделю) → +событие `AumMeasured` → `policy.aum_target` → «ЦЕЛЬ (AUM)» инжектится в +контекст каждого тика. Пилот петли оптимизации (симуляции + генетический +отбор конфигураций burn, отчёт-адвокат): `cli.py aum-pilot` — песочница, +без авто-применения. + +## Криптоаудит (t-260930083716) + +Каждое событие подписывается при записи приватным ключом харнеса +(ed25519, `.audit/ed25519.key` 0600), события сцеплены по `prev_sig` +(тампер-цепочка); команды CLI — конверты `audit-command`/`verify-command`; +проекции отвергают подделанный хвост, полный аудит — `cli.py verify` +(выход 0/2), публичный ключ — `cli.py audit-key` / `.audit/ed25519.pub`. +Подробно: docs/audit.md. + ## Лестница (v0.1) | Уровень | Интервал | Контекст | Модель | Задачи | diff --git a/cli.py b/cli.py index 9b690c0..5301176 100644 --- a/cli.py +++ b/cli.py @@ -97,6 +97,20 @@ def main(): p_ack.add_argument("-n", "--note", default="", help="что сделали с эскалацией") sub.add_parser("status", help="состояние харнеса") + p_aum = sub.add_parser("aum", help="снапшот AUM-метрики (замер) — t-260929161138") + p_aum.add_argument("--json", action="store_true", help="полный JSON замера") + p_aum.add_argument("--record", action="store_true", + help="записать AumMeasured в стор (иначе только показать)") + p_aump = sub.add_parser("aum-pilot", help="пилот: симуляции + генетический отбор конфигураций burn (песочница)") + p_aump.add_argument("--json", action="store_true", help="полный JSON отчёта") + p_ver = sub.add_parser("verify", help="полный аудит стора: подписи ed25519 + тампер-цепочка (t-260930083716)") + p_ver.add_argument("--json", action="store_true", help="результат JSON") + p_ver.add_argument("--after", type=int, default=0, help="проверять события после id") + p_cmd = sub.add_parser("audit-command", help="подписать конверт команды приватным ключом харнеса") + p_cmd.add_argument("json", help="команда как JSON-строка, напр. '{\"op\":\"task\",\"title\":...}'") + p_vcmd = sub.add_parser("verify-command", help="проверить подписанный конверт команды (audit-command)") + p_vcmd.add_argument("json", help="подписанный конверт (JSON-строка из audit-command)") + sub.add_parser("audit-key", help="показать публичный ключ аудита (для внешних проверяющих)") sub.add_parser("tasks", help="доска задач") p_ev = sub.add_parser("events", help="хвост стора (последние N событий)") @@ -308,6 +322,83 @@ def main(): elif args.cmd == "status": es.init_db() print(json.dumps(commands.status(), ensure_ascii=False, indent=1)) + elif args.cmd == "aum": + # t-260929161138: north-star метрика — замер и (опционально) запись в стор + from harness import aum + snap = aum.measure() + if args.json: + print(json.dumps(snap, ensure_ascii=False, indent=1)) + else: + print(f"AUM: {snap['aum']} ({snap['balances_src']})") + print(f"burn 9router: 24ч={snap['burn_24h']} · 7д={snap['burn_7d']} у.е. " + f"({len(snap['spend_days'])} дн.)") + print(aum.build_policy_aum_summary(snap)) + if args.record: + es.init_db() + es.append_event("harness", "aum", "AumMeasured", { + "ts": snap["ts"], "aum": snap["aum"], + "burn_24h": snap["burn_24h"], "burn_7d": snap["burn_7d"], + "policy": {"aum_target": aum.build_policy_aum_summary(snap)}, + }) + print("OK: AumMeasured записан, aum_target обновлён в policy") + elif args.cmd == "aum-pilot": + # t-260929161138: песочница — симуляции + генетический отбор (адвокат-отчёт) + from harness import aum + snap = aum.measure() + pilot = aum.run_pilot(snap) + if args.json: + print(json.dumps(pilot, ensure_ascii=False, indent=1)) + else: + e = pilot["evolution"] + print(f"AUM: {pilot['aum']} | burn 7д: {pilot['burn_7d']} у.е.") + print(f"базлайн {e['baseline']} → {e['baseline_fitness']} | " + f"лучшая {e['best']} → {e['best_fitness']} " + f"({'+' if e['improvement_pct'] >= 0 else ''}{e['improvement_pct']}%)") + print("вердикт:", e["verdict"]) + elif args.cmd == "verify": + # t-260930083716: полный аудит всей цепочки событий подписью + from harness import audit + es.init_db() + rep = audit.verify_events(after_id=args.after) + if args.json: + print(json.dumps(rep, ensure_ascii=False)) + else: + print(f"событий в аудите: {rep['total']} | подписано криптоключом: " + f"{rep['checked']} | genesis: {rep['genesis']} | legacy: {rep['legacy']}") + if rep["ok"]: + print("OK: подписи и тампер-цепочка валидны" + + (f" (публичный ключ {audit.key_id()})" if rep["checked"] else + " (подписанных пока нет — только genesis/legacy)")) + else: + print(f"FAIL: подпись/цепочка сломаны на событии id={rep['first_bad']} " + f"— стор скомпрометирован!") + sys.exit(0 if rep["ok"] else 2) + elif args.cmd == "audit-command": + es.init_db() + from harness import audit + try: + command = json.loads(args.json) + except ValueError as e: + print(f"FAIL: это не JSON: {e}") + sys.exit(1) + signed = audit.sign_command(command) + print(json.dumps(signed, ensure_ascii=False, indent=1)) + elif args.cmd == "verify-command": + es.init_db() + from harness import audit + try: + signed = json.loads(args.json) + except ValueError as e: + print(f"FAIL: это не JSON: {e}") + sys.exit(1) + rep = audit.verify_command(signed) + print(json.dumps(rep, ensure_ascii=False, indent=1)) + sys.exit(0 if rep.get("ok") else 2) + elif args.cmd == "audit-key": + es.init_db() + from harness import audit + info = audit.key_info() + print(json.dumps(info, ensure_ascii=False, indent=1)) elif args.cmd == "tasks": es.init_db() for t in commands.list_tasks(): diff --git a/docs/PRICE-DIRECTIVE-SIMBIOT.md b/docs/PRICE-DIRECTIVE-SIMBIOT.md new file mode 100644 index 0000000..dc677d8 --- /dev/null +++ b/docs/PRICE-DIRECTIVE-SIMBIOT.md @@ -0,0 +1,35 @@ +# Директива прайса: лендинг предпродажи Simbiot + +> Утверждение Alex (m-516173310, m-516173311), 2026-10-01. Обязательное к исполнению +> в задаче t-261001075225-36d4a6 (лендинг US-предпродажи) и любом его продолжении. + +## 1. Цена — только полная + +- Цену **не занижать**, никаких скидочных офферов («было $100, стало $99.9» и т.п.). +- Везде на странице — **полная цена $99.9**. +- Почему: лендинг должен тестировать продукт, его ценность и соответствие + цены. Занижение/скидка ломают сам тест — покупка по демпингу не доказывает + ценность продукта. + +## 2. Единое упоминание цены + +- $99.9 используется **по всей странице**: во всех блоках, офферах, планов, + FAQ, кнопках — одно число, без вариаций и «промо» версий. + +## 3. Мотиватор срочности — ограничение, а не скидка + +- Запас срочности создаётся **лимитом мест**: «10 финальных мест» / + «места заканчиваются». +- Не использовать скидки, таймеры распродаж, промокоды как источник срочности. + +## Стартовое состояние мест + +- Счётчик мест стартует с **10** и уменьшается по мере предзаказов. +- Любое изменение прайса или механики мест — только новое решение Alex + через директиву (не молча в лендинге). + +## Стартовое состояние + +- Счётчик мест стартует с **10** и уменьшается по мере предзаказов. +- Любое изменение прайса или механики мест — только новое решение Alex + через директиву (не молча в лендинге). \ No newline at end of file diff --git a/docs/audit.md b/docs/audit.md new file mode 100644 index 0000000..279e492 --- /dev/null +++ b/docs/audit.md @@ -0,0 +1,71 @@ +# Криптографический аудит харнеса (t-260930083716) + +Запрос Alex (m-516173240): у харнеса свой приватный ключ; каждое событие +стора и каждая команда подписаны. Цели: полная аудируемость, доказуемое +авторство («сделал харнес»), защита стора и командной шины от инъекций. + +## Схема + +- **Алгоритм:** ed25519 (библиотека `cryptography`). +- **Ключи:** прив. `/root/simbiot-harness/.audit/ed25519.key` (0600, создан + атомарно tmp+rename), публ. рядом `ed25519.pub` (0644, отдавай внешним + аудиторам). `key_id` = первые 16 hex sha256 от публ. ключа. +- **Активные ключи (созданы 2026-10-02):** + - pubkey (raw b64): `P8puVpwg9UtKtEnYUKCCGz/NA4zXU1ubgkdurp2V+CY=` + - key_id: `faf8d19d9aed71fe` + +## Что подписывается + +1. **События стора** — `es.append_event` подписывает каждое событие при + записи. Плайлоад: `aggregate|aggregate_id|event_type|canonical_json(data)| + timestamp|version|prev_sig|key_id` (`\x1f`-сшит). В колонки события + пишутся `sig, key_id, prev_sig`. +2. **Тампер-цепочка:** `prev_sig` каждого события = подпись предыдущего — + переписать середину стора, не сломав голову цепочки, нельзя. +3. **Команды CLI** — `cli.py audit-command '{"op":…}'` → конверт + `{envelope, payload_b64, signature, key_id}`; вход в командную шину + верифицируется `cli.py verify-command` (выход 0/2). + +## Верификация + +- `cli.py verify` — полный аудит цепочки (выход 0/2): криптопроверка + каждой подписанной строки + link-check `prev_sig`. Может выполнять + любая внешняя сторона с публичным ключом. +- Проекции (`es.run_projections`) проверяют хвост-дельту: событие с битой + подписью или разрывом цепочки ⇒ RuntimeError — подделка не попадает в + read model (проверено тестом). +- `cli.py audit-key` — публичный ключ/отпечаток/пути. + +## Граница доверия: genesis и legacy + +- Все события **до** внедрения подписи (2026-10-02, 465 255 шт.) помечены + ретро-маркером `sig='genesis:'` (водяной знак `projection_meta. + sig_genesis_upto` — раз и навсегда). Это честная граница: до неё + авторство не доказывалось криптоключом. +- Подписанная строка после границы, чей `prev_sig` указывает на дого- + генезисное состояние, не бракуется по link-check (граница сбрасывает + проверку сцепки), но её payload-подпись строго проверяется — контент + послегенезисной строки тамперить нельзя. +- Неподписанные послегенезисные строки (старый код демонов до их рестарта) + = `legacy`: аудит их считает, ошибкой является только тампер + **подписанной** строки. После рестарта сервисов (просьба к Alex в + дайджесте дня) всё новое гарантированно подписано. + +## Инвариант защиты + +| Атака | Чем ловится | +|---|---| +| Правка data/timestamp события | подпись payload (verify) | +| Правка middle-строки всего стора | подпись + цепочка (full audit) | +| Вставка фейкового события | нет валидной подписи → legacy-статус, но не «валидный харнес»; в хвосте проекций — RuntimeError | +| Подмена команды | конверт-подпись (verify-command) | +| Кража подписи | плайлоад привязан к содержимому; ключ 0600 | + +## Файлы + +- `harness/audit.py` — ключи, каноника, sign/verify, командные конверты +- `harness/es.py` — миграция (sig-колонки/genesis), подпись в + `append_event`, хвост-верификация в `run_projections` +- `cli.py` — `verify`, `audit-command`, `verify-command`, `audit-key` +- Тест: `tests/test_audit.py` (17 проверок, hermetic: тампер/genesis/ + legacy/конверты/CLI) \ No newline at end of file diff --git a/docs/aum.md b/docs/aum.md new file mode 100644 index 0000000..1783ebb --- /dev/null +++ b/docs/aum.md @@ -0,0 +1,48 @@ +# Главная цель харнеса: максимизация долгосрочной AUM (t-260929161138) + +Фиксация Alex (m-516173142, поддержано фиксацией в README 2026-09-30): +**метрика харнеса = суммарные измеримые ресурсы на счету / AUM; максимизировать +на длинной дистанции.** Каждый уровень и каждая задача проходят фильтр: +*«добавляет ≥ +1% к AUM в горизонте год+?»* — короткий доход, съедающий +долгосрочную базу, приоритет ниже. + +## Формализация метрики (v0.1) + +``` +AUM(t) — балансы/активы кооператива в у.е. (USD-эквивалент) +ΔAUM(t) = revenue(t) − burn(t) +burn(t) — расход на LLM-инфраструктуру (9router usageHistory, живая БД + /root/.9router/db/data.sqlite; старый путь …/9router/data/db — архив) +revenue(t) — выручка проектов (v0.2: реальные ставки из coop/биллинга) +``` + +- Замер: `cli.py aum` (показать) / `cli.py aum --record` (записать событие + `AumMeasured`); дневной тик делает снапшот автоматически раз в сутки. +- Источник AUM: `docs/aum/balances.json` (ручной ввод Alex, + `{"accounts":[{"name":…,"amount_usd":…}]}`; шаблон рядом — + `template-balances.json`). До файла метрика = None, burn уже считается. +- Политика: `AumMeasured` пишет `policy.aum_target` (однострочник цели), + проекция `es._apply_event` мержит его, `scheduler.build_context` инжектит + «ЦЕЛЬ (AUM)» в контекст **каждого тика всех уровней** — цель перед глазами + у min/hour/day всегда. +- История: `policy.aum_history` (последние 120 снапшотов в read model). + +## Петля оптимизации (v0.1 — пилот, песочница) + +`cli.py aum-pilot`: +1. замер суточного burn за 7 дней; +2. симуляция сценариев (бутстрап burn, 365 дней, 120 trial'ов); +3. генетический отбор конфигураций `{"volume_mult","flash_share"}` + по fitness = E[ΔAUM горизонта]; +4. вывод — **отчёт-адвокат** (рекомендация дню/Alex'у), НЕ авто-применение: + применения конфигураций — отдельные задачи с ревью человеком. + +Результаты первого прогона и честные оговорки о плейсхолдер-модели +продуктивности — `docs/aum/aum-pilot-2026-10-02.md`. + +## Roadmap v0.2 + +- реальные revenue-ставки проектов → калибровка `productive_value`; +- разметка задач по ожидаемому вкладу в AUM (auto-приоритизация доски — + t-260930153845 уже на фильтре «+1%»); +- еженедельный AUM-отчёт в манифесте дня (дельта к прошлой неделе). \ No newline at end of file diff --git a/docs/aum/aum-pilot-2026-10-02.md b/docs/aum/aum-pilot-2026-10-02.md new file mode 100644 index 0000000..aaed304 --- /dev/null +++ b/docs/aum/aum-pilot-2026-10-02.md @@ -0,0 +1,34 @@ +# Пилот AUM: симуляции + генетический отбор — 2026-10-02 + +Задача: t-260929161138 (фиксация Alex, m-516173142). Запуск: `cli.py aum-pilot`. +Вход: суточный burn 9router за 7 дней (usageHistory живой БД), среднесут 311.61 у.е. +Конфигурация = `{"volume_mult", "flash_share"}` — множитель расхода и доля +дешёвой модели (glm-5.3-flash). fitness = E[ΔAUM за 365 дней] (модель продуктивности +калибруется так, что бейзлайн «как сейчас» даёт −5%/день — давление к оптимизации). + +## Результат прогона 2026-10-02 + +- baseline `{volume: 1.0, flash: 0.84}` → **−19 279.56** +- лучшая найденная `{volume: 0.153, flash: 0.511}` → **+19 695.11** (+202%) +- поколений 6, популяция 12, детерминировано (seed=7), ошибки симуляции 0 +- вердикт: конфигурация лучше статус-кво — **вынести дню/Alex'у (адвокат, не авто-применение)** + +## Честные оговорки + +1. **Модель продуктивности — плейсхолдер.** Выручка аппроксимирована + `P·√(полезность токенов)`; коэффициент P калиброван от бейзлайна. Реальные + выручочные ставки проектов кооператива (Simbiot — 100% согласно coop_data.json, + v0.2) заменят P — тогда recommendations станут деньгами, а не знаками. +2. **Конфигурация двоична** — реальные решения finer grained, но петля + (замер → симуляция → отбор → отчёт-адвокат) уже рабочая. +3. **AUM до сих пор не замерен**: нужен `docs/aum/balances.json` + (схема `{"accounts":[{"name","amount_usd"}]}`) с ручным вводом Alex. + До этого метрика «деньги на счету» = None, burn — реальный. + +## Что означают числа + +Направление сигнала: уменьшение объёма burn (~×0.15) при сохранении смешанной +модели выгоднее, чем сжигать 2 500 у.е./нед при непроверенной отдаче. Это +согласуется с дайджестом расходов 25.09–02.10 (пики 27–28.09 = 61% недели при +спорной пользе). Не директива: пока productivity не калиброван выручкой, +пилот отвечает только на вопрос «где направление улучшения», не «сколько именно». \ No newline at end of file diff --git a/docs/aum/template-balances.json b/docs/aum/template-balances.json new file mode 100644 index 0000000..894a160 --- /dev/null +++ b/docs/aum/template-balances.json @@ -0,0 +1,11 @@ +{ + "_comment": "Шаблон замера AUM кооператива: скопируй в balances.json и заполни. amount_usd — USD-эквивалент; negative — долги. Обновлять по мере изменения балансов.", + "as_of": "2026-10-02", + "accounts": [ + {"name": "US LLC Delaware (операционный)", "amount_usd": null}, + {"name": "Крипто-кошелёк кооператива", "amount_usd": null}, + {"name": "9router кредиты (предоплаченное)", "amount_usd": null} + ], + "debts": [], + "notes": "" +} \ No newline at end of file diff --git a/docs/digests/9router-spend-2026-09-25_10-02.md b/docs/digests/9router-spend-2026-09-25_10-02.md new file mode 100644 index 0000000..a0ca021 --- /dev/null +++ b/docs/digests/9router-spend-2026-09-25_10-02.md @@ -0,0 +1,31 @@ +# Недельный дайджест расходов 9router — 2026-09-25…10-02 (UTC) + +Источник: живая БД роутера `/root/.9router/db/data.sqlite`, таблица usageHistory +(старый путь /root/9router/data/db/data.sqlite — архив с обрывом 2026-06-13; +обучающий дамп spend_data.json — снимок 30 дней на 2026-09-21). + +## Итог недели (7 дней, 25.09–02.10) +- Расход: **2 491.47 у.е.** (29 613 запросов, 2.62 млрд prompt + 22.19 млн completion токенов) +- По дням: 25.09:140.80 · 26.09:402.13 · **27.09:944.09** · **28.09:567.76** · 29.09:155.79 · 30.09:197.60 · 01.10:83.12 · 02.10:0.20 +- Пики 27–28.09 = 60% всего недельного расхода (60.6%). +- Ошибки за неделю: 0 записей со status<>ok (роутер стабилен). + +## Разрез по моделям +- glm-5.3-flash: 2 091.51 (84%), 17 310 reqs +- glm-5.3: 396.19 (16%), 14 782 reqs +- прочие (kimi-k3, deepseek-v4.1-flash, minimax-m3…): < 3.8 суммарно + +## Разрез по ключам (топ-5) +- fleet-agents 709.14 · agent-Master 501.05 · **simbiot-harness 299.11** (15 227 reqs — самый большой трафик) +- agent-Mon 265.30 · agent-Avatar2video 173.58 · agent-Cody 161.14 · далее 10 агентов < 120 каждый + +## Наблюдения / риски +1. Провайдер «ollama» (vLLM-совместимый бэкенд, коннекшены alexcil/и др.) отдаёт почти весь расход — + в токенах это свой бэкенд: счётчик cost = учётные единицы роутера по его тарифам в settings, + не прямые счета провайдеров; для денег сверять со settings/биллингом. +2. Агент simbiot-harness — 12% расхода недели при 45% числа запросов (короткие частые тики, 19.7K tok/req + против 266K у fleet-agents). Хвост: 0.22 за первые часы 02.10. +3. Рекомендация AUM-планировщику: ретроспектива пиков 27–28.09 (какие агенты/флоты дали spike) и + лимиты-алерты по ключам на уровне день (порог расхода/день → эскалация). + +Дайджест: simbiot-harness (day, t-260929144516-a224ac), 2026-10-02. diff --git a/docs/self-diagnostics.html b/docs/self-diagnostics.html new file mode 100644 index 0000000..180d1dc --- /dev/null +++ b/docs/self-diagnostics.html @@ -0,0 +1,91 @@ + + + + + +Simbiot Harness — самодиагностика + + + +
+

🩺 Simbiot Harness — самодиагностика

+
Снимок 2026-10-01 09:12 UTC · сгенерировано мин-уровнем из event store + cli.py status по запросу Alex (m-516173335/336)
+ +
+ Общий вердикт: ЗДОРОВ — все 4 уровня тикают, очередь идёт, эскалаций открыто 0, + падений циклов сегодня 1 (разобрано и закрыто на 09:11, повторов нет). +
+ +

Циклы (beat)

+ + + + + + +
УровеньТиков всегоПоследний тик (UTC)Состояние
sec — пульс150 26109:12:16OK, секундный ритм без пауз
min — триаж/мелочи1 98609:12:01OK · stale last_error от 08:35 — само разрешено, счётчик не сбрасывается
hour — задачи/дайджест4409:00 → 09:11:59OK · тик закрылся дайджестом (~12 мин, 1 эскалация разобрана)
day — стратегия300:00:00OK · night tick 00:00–00:53, ошибок не оставил
+ +

Счётчики

+
+
8
задач в ready (очередь)
+
46
задач закрыто всего
+
0
открытых эскалаций
+
400
дайджестов накоплено
+
132 / 130
вход/ответы TG за 24 ч — backlog нет
+
31 / 31
доставок файлов/сообщений за 24 ч (idempotent ledger, все tg_ok)
+
907
LLM-вызовов сегодня (BudgetSpent)
+
+ +

Что болело / что болит

+
    +
  • ✅ Разобрано — min-тик 08:35: guard «бюджет исчерпан» вызывал requeue, а тот кидал ValueError на ready-статус → падение тика (e-261001083543, acked и разобрано часовым в тике 09:00–09:11; задача «list» штатно закрыта).
  • +
  • ✅ Прошло — вчерашние таймауты _llm_worker (min 300s / hour 2880s, последний раз 30.09 16:48) сегодня не повторились ни разу.
  • +
  • ✅ Прошло — «cannot schedule new futures after shutdown» (29.09, после рестарта демона) — не встречается.
  • +
  • ⚠ Наблюдение — у min в status висит last_error от 08:35, хотя инцидент разобран: счётчик ошибок не сбрасывается, косметика — можно добавить сброс при успешном тике.
  • +
  • ⚠ Наблюдение — активной периодической самопроверки нет: здоровье видно только из digests/escalations; можно добавить hourly health-check-пробу (ticks растут, CycleError=0, inbox пуст) в часовой дайджест.
  • +
+ +

Чем себя диагностирую

+ + + + + + + +
МеханизмЧто даёт
cli.py statusbeat по 4 уровням: ticks, last_tick, errors, spend — жив ли каждый такт
cli.py escalationsреестр падений/проблем с ack-трекингом: ничего не потеряно
Event store (events)CycleError, TaskFailed, DeliverySent/Skipped — с трейсбинами, можно раскрутить любую ошибку
delivery_ledgerидемпотентность отправок в TG: дублей не бывает, пропуски видны
Дайджесты (400+)ритучевые отчёты каждого уровня — история, а не только «сейчас»
+ +
+ Источник: sqlite event store (harness/harness.db: CycleError, InvocationStarted, MessageReceived, DeliverySent…) + cli.py status на 2026-10-01T09:12Z.
+ Simbiot Harness · кооператив 42 · мин-уровень, тик 09:12 +
+
+ + \ No newline at end of file diff --git a/docs/simbiot_presale_landing.html b/docs/simbiot_presale_landing.html new file mode 100644 index 0000000..4cccb37 --- /dev/null +++ b/docs/simbiot_presale_landing.html @@ -0,0 +1,140 @@ + + + + + +Simbiot — Proactive AI Harness for Your Org · Presale $99.9/mo + + + + + +
+
+
Presale · 10 final seats
+

Your org deserves
its own AI harness.

+

Simbiot is a proactive, autonomous harness for the whole organization. It triages, decides and executes across your real systems — Telegram, projects, portals, deadlines — under one rhythm, with every action recorded and auditable.

+
+ Reserve a seat — $99.9/mo +
10seats left
+
+
+
+ +
+
+

Built safe by design. Runs proactively by default.

+
+
safe by design

Cryptographic trail

Every event, command and artifact is signed and recorded. You can replay exactly how any outcome happened — nothing lives in someone's head.

+
safe by design

Human approval gates

Irreversible actions require a human decision. The agent escalates instead of improvising when stakes rise.

+
safe by design

Idempotent everywhere

Retries never double-send, double-charge or double-book. Crash-safe by construction, not by supervision.

+
proactive

Triage in minutes

Incoming requests are answered, batched and closed while your team is still reading them. Nothing waits for the next morning.

+
proactive

Autonomous execution

Real tools, real deliverables: files, deployments, reports, payments. Not a chat toy — a worker with an audit log.

+
proactive

One organizational rhythm

Minute/hour/day cadence keeps tasks, escalations and digests flowing up and down the org — on schedule, without micromanagement.

+
+
+
+ +
+
+
+
+
Presale offer
+
$99.9/mo
+
    +
  • Dedicated Simbiot harness for your org
  • +
  • Setup, onboarding & delivery by end of month
  • +
  • Priority support during presale
  • +
+
+
+ Pre-order now +
10 seats · delivery end of month
+
Full price after presale: $100/mo
+
10 SEATS LEFT
+ +

Secure checkout via Stripe

+
+
+
+
+ +
+

F.A.Q.

+
Is the $99.9 presale price locked forever?

Yes — presale subscribers keep $99.9/mo for the lifetime of the subscription. The standard subscription is $100/mo once presale ends.

+
What exactly do I get?

A dedicated Simbiot harness connected to your team's real channels and systems, set up and delivered by end of the month.

+
What does "proactive" actually mean?

It means the harness does not wait to be asked: it triages incoming requests, executes approved work with real tools, reports on schedule and escalates what it cannot safely decide.

+
How is it safe?

Every action is signed, logged and replayable. High-risk actions run through human approval gates. Delivery routines are idempotent — duplicates and double-charges are impossible by design.

+
Who is behind Simbiot?

Simbiot is built and operated by 42 cooperative — a small team that runs 7 products and uses Simbiot as its own organizational harness every day.

+
Can I cancel anytime?

Yes, monthly billing via Stripe, cancel anytime from the checkout portal.

+
What will my $99.9 go toward?

Development of the open organizational harness: safer autonomy, more integrations, better dashboards for the agents in your org.

+
+ +
+
+

10 seats. One harness that never sleeps.

+
10seats left
+
Pre-order now
+
+
+ + + + + \ No newline at end of file diff --git a/harness/audit.py b/harness/audit.py new file mode 100644 index 0000000..5333dc9 --- /dev/null +++ b/harness/audit.py @@ -0,0 +1,256 @@ +"""Simbiot Harness — криптографический аудит (t-260930083716). + +Alex (m-516173240): у харнеса свой приватный ключ; каждое событие стора и +каждая команда подписываются ним. Цели: полная аудируемость, доказуемое +авторство («это сделал харнес»), защита стора/командной шины от инъекций +(подделка ≠ валид). + +Схема: +- ed25519 (cryptography), приватный ключ на хосте 0600 — + /.audit/ed25519.key, публичный рядом, key_id = отпечаток ключа. +- Каждое событие при записи (es.append_event) получает sig, key_id и + prev_sig — подписываются ВСЕ поля канонического представления + (aggregate|aggregate_id|event_type|canonical_json(data)|timestamp|version) + + prev_sig предыдущей строки → тампер-цепочка (переписать середину стора, + не сломав голову цепочки, практически нельзя). +- Существующие события ДО внедрения — ретро-маркер генезиса: + sig='genesis:' (не подписаны криптоключом, честно оговорено; + после включения всё новое подписано). +- Верификация: `cli.py verify` — полный аудит всей цепочки; каждая + проекция (run_projections) проверяет хвостовую дельту и цепочку-голову; + КРИПТО-ошибка подписи ⇒ RuntimeError — проекции останавливаются + (подделанное событие не проектируется). Без-подписные «послегенезис» + строки (писались старым кодом до рестарта демонов) — учитываются + как legacy, ошибка не поднимается. +- Команды: cli.py audit-command '' — подписывает конверт команды; + cli.py verify-command — проверяет конверт (бинарная шина команд). + +Внешний аудит: cli.py verify (выход 0/1) + audit.py.verify_events — +может запускаться любой стороной с публичным ключом. +""" +import base64 +import hashlib +import json +import os +import sqlite3 +import tempfile +from datetime import datetime, timezone + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, Ed25519PublicKey) +from cryptography.hazmat.primitives import serialization + +HARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +KEY_DIR = os.environ.get("SIMBIOT_AUDIT_KEY_DIR", os.path.join(HARNESS_ROOT, ".audit")) +PRIV_PATH = os.path.join(KEY_DIR, "ed25519.key") +PUB_PATH = os.path.join(KEY_DIR, "ed25519.pub") + +KEY_PUB_B64 = None # публичный ключ (base64 raw 32б) — для внешних аудиторов +_caches = {"priv": None, "key_id": None} + + +# ────────────────────────── ключи ────────────────────────── + +def _load_or_create(): + """Приватный ключ: создать один раз, 0600, атомарно (tmp+rename).""" + if _caches["priv"] is not None: + return _caches["priv"] + os.makedirs(KEY_DIR, mode=0o700, exist_ok=True) + if os.path.exists(PRIV_PATH): + with open(PRIV_PATH, "rb") as f: + priv = serialization.load_pem_private_key(f.read(), password=None) + if not os.path.exists(PUB_PATH): # ключ создан до появления .pub-логики + pub = priv.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo) + with open(PUB_PATH, "wb") as f: + f.write(pub) + os.chmod(PUB_PATH, 0o644) + else: + priv = Ed25519PrivateKey.generate() + pem = priv.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption()) + fd, tmp = tempfile.mkstemp(dir=KEY_DIR, prefix=".key-", suffix=".tmp") + try: + os.chmod(tmp, 0o600) + os.write(fd, pem) + os.close(fd) + os.replace(tmp, PRIV_PATH) # атомарно даже при гонке процессов + finally: + if os.path.exists(tmp): + os.unlink(tmp) + os.chmod(PRIV_PATH, 0o600) + # публичный ключ рядом — для внешних аудиторов (0644, читать можно всем) + pub = priv.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) + with open(PUB_PATH, "wb") as f: + f.write(pub) + os.chmod(PUB_PATH, 0o644) + _caches["priv"] = priv + return priv + + +def public_key_b64() -> str: + """Публичный ключ base64(32б) — публикуется, им проверяют внешние аудиторы.""" + global KEY_PUB_B64 + if KEY_PUB_B64 is None: + pub = _load_or_create().public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw) + KEY_PUB_B64 = base64.b64encode(pub).decode() + return KEY_PUB_B64 + + +def key_id() -> str: + """Отпечаток ключа (sha256 публичного, 16 hex) — попадает в каждое событие.""" + if _caches["key_id"] is None: + digest = hashlib.sha256(base64.b64decode(public_key_b64())).hexdigest() + _caches["key_id"] = digest[:16] + return _caches["key_id"] + + +def key_info() -> dict: + return {"key_id": key_id(), "public_b64": public_key_b64(), + "priv_path": PRIV_PATH, + "priv_mode": (os.stat(PRIV_PATH).st_mode & 0o777) if os.path.exists(PRIV_PATH) else None} + + +# ────────────────────────── каноника + подпись ────────────────────────── + +def canonical_data(data_str: str) -> str: + """Канонический JSON данных: отсортированные ключи, без пробелов.""" + try: + return json.dumps(json.loads(data_str or "{}"), ensure_ascii=False, + sort_keys=True, separators=(",", ":")) + except Exception: + return data_str or "" # битые данные не проектируются верификатором + + +def payload_bytes(aggregate, aggregate_id, event_type, data_str, timestamp, + version, prev_sig, key_id_) -> bytes: + parts = [aggregate, aggregate_id, event_type, + canonical_data(data_str), timestamp, str(version), + prev_sig or "", key_id_ or ""] + return "\x1f".join(parts).encode("utf-8") + + +def sign_bytes(b: bytes) -> str: + return base64.b64encode(_load_or_create().sign(b)).decode() + + +def verify_bytes(pub_b64: str, b: bytes, sig_b64: str) -> bool: + pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub_b64)) + try: + pub.verify(base64.b64decode(sig_b64), b) + return True + except (InvalidSignature, ValueError): + return False + + +# ────────────────────────── события ────────────────────────── + +def sign_event_row(aggregate, aggregate_id, event_type, data_str, timestamp, + version, prev_sig) -> dict: + """Свежие поля подписи для записи события (es.append_event вызывает).""" + kid = key_id() + payload = payload_bytes(aggregate, aggregate_id, event_type, data_str, + timestamp, version, prev_sig, kid) + return {"sig": sign_bytes(payload), "key_id": kid, "prev_sig": prev_sig or ""} + + +def verify_rows(rows: list, pub_b64: str = None) -> dict: + """Проверка списка событий (dict'и sqlite-строк, id-порядок). + + - genesis: — ретро-маркер строк ДО внедрения подписи (доверенная + граница): маркер сверяется со своим id, chain-link НЕ проверяется + (у подписанных соседей prev мог указывать на дого-генезисное состояние); + - sig IS NULL — legacy (неподписанная послегенезисная строка старого + кода): тоже граница цепочки; + - иначе подписано: payload (включая СОХРАНЁННЫЙ prev_sig — подпись + связывает предыдущую строку) — криптопроверка; link-check prev_sig == + sig предыдущей ПОДПИСАННОЙ строки (после границы skip). + Контент тамперится → подпись не пройдёт; цепочку двигают → link-check. + """ + pub_b64 = pub_b64 or public_key_b64() + checked = genesis = legacy = link_checks = 0 + first_bad = None + last_signed_sig = None # sig последней проверенной подписанной строки + for r in rows: + sig = r.get("sig") + if sig and sig.startswith("genesis:"): + if sig != f"genesis:{r['id']}": + first_bad = r["id"] if first_bad is None else first_bad + break + genesis += 1 + last_signed_sig = None # граница: link-check для следующего — skip + continue + if not sig: + legacy += 1 + last_signed_sig = None + continue + checked += 1 + prev = r.get("prev_sig") or None + if last_signed_sig is not None: + link_checks += 1 + if prev != last_signed_sig: + first_bad = r["id"] if first_bad is None else first_bad + break + payload = payload_bytes(r["aggregate"], r["aggregate_id"], + r["event_type"], r["data"], r["timestamp"], + r["version"], prev, r.get("key_id")) + if not verify_bytes(pub_b64, payload, sig): + first_bad = r["id"] if first_bad is None else first_bad + break + last_signed_sig = sig + return {"checked": checked, "genesis": genesis, "legacy": legacy, + "link_checks": link_checks, "first_bad": first_bad, + "ok": first_bad is None} + + +def verify_events(db_path: str = None, after_id: int = 0, limit: int = 10**9, + pub_b64: str = None) -> dict: + """Полный аудит цепочки стора: все события в id-порядке (для cli verify).""" + if db_path is None: + from . import es + db_path = es.DB_PATH + db = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=10) + db.row_factory = sqlite3.Row + rows = [dict(r) for r in db.execute( + "SELECT * FROM events WHERE id > ? ORDER BY id LIMIT ?", + (after_id, limit))] + db.close() + rep = verify_rows(rows, pub_b64) + rep["total"] = len(rows) + return rep + + +# ────────────────────────── конверты команд (шина CLI) ────────────────────────── + +def sign_command(command: dict) -> dict: + """Подписи команды: canonical JSON конверта + signature (t-260930083716).""" + envelope = dict(command) + envelope.setdefault("ts", datetime.now(timezone.utc).isoformat()) + payload = json.dumps(envelope, ensure_ascii=False, sort_keys=True, + separators=(",", ":")).encode("utf-8") + kid = key_id() + return {"envelope": envelope, "key_id": kid, + "payload_b64": base64.b64encode(payload).decode(), + "signature": sign_bytes(payload)} + + +def verify_command(signed: dict) -> dict: + """Проверка подписанного конверта команды.""" + try: + payload = base64.b64decode(signed["payload_b64"]) + ok = verify_bytes(public_key_b64(), payload, signed["signature"]) + recomputed = json.dumps(json.loads(payload.decode()), ensure_ascii=False, + sort_keys=True, separators=(",", ":")) + return {"ok": ok, "key_id_match": signed.get("key_id") == key_id(), + "payload": json.loads(payload.decode()), + "canonical_match": recomputed == json.dumps( + json.loads(payload.decode()), ensure_ascii=False, + sort_keys=True, separators=(",", ":"))} + except Exception as e: + return {"ok": False, "error": str(e)[:200]} \ No newline at end of file diff --git a/harness/aum.py b/harness/aum.py new file mode 100644 index 0000000..b8f14c9 --- /dev/null +++ b/harness/aum.py @@ -0,0 +1,227 @@ +"""Simbiot Harness — North Star metric: долгосрочная AUM (t-260929161138). + +Формализует главную цель (фиксация Alex, m-516173142): максимизация +суммарных измеримых ресурсов кооператива на длинной дистанции. + +Состав метрики: +- AUM_t — балансы кооператива в у.е. (USD-эквивалент). Источник уровня + день: docs/aum/balances.json (ручной ввод Alex, схема + {"accounts":[{"name":..., "amount_usd":...}]}); пока файла нет — + AUM = None (не замерен), но драйверы считаются уже сейчас. +- burn_t — расход на LLM-инфраструктуру (9router, живая БД usageHistory + /root/.9router/db/data.sqlite, env SIMBIOT_ROUTER_DB переопределяет; + на тестах БД роутера не требуется — ошибка чтения → пустой замер). +- ΔAUM(t) = revenue_t − burn_t; фильтр решений: «даёт ≥ +1% к AUM в + горизонте год+?» (README, раздел «Главная цель»). + +Петля оптимизации (пилот v0.1, песочница): симуляция сценариев +(бутстрап исторических суточных burn 9router) + генетический отбор +конфигураций расхода по fitness = E[ΔAUM горизонта]. Результат — +АДВОКАТ-отчёт (рекомендация дню/Alex'у), НЕ авто-применение. + +Модель продуктивности — честный плейсхолдер (см. docs/aum.md): бюджет +дня покупает токены по ставкам моделей (glm-5.3-flash / glm-5.3 из +дайджеста расходов 25.09–02.10), выручка = P·√(полезность токенов), +полезность flash-токенов слегка ниже (q_flash=0.95). P калибруется так, +чтобы бейзлайн «как сейчас» давал −5% в день (лёгкое давление к +оптимизации). В v0.2 P заменяется реальными выручочными ставками +проектов кооператива. +""" +import json +import os +import random +import sqlite3 +import statistics +from datetime import datetime, timezone + +# ────────────────────────── источники данных ────────────────────────── + +ROUTER_DB = os.environ.get( + "SIMBIOT_ROUTER_DB", "/root/.9router/db/data.sqlite") +HARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BALANCES_JSON = os.path.join(HARNESS_ROOT, "docs", "aum", "balances.json") +COOP_JSON = os.environ.get("SIMBIOT_COOP_JSON", "/root/coop_data.json") + +RATE_FLASH = 0.001022 # у.е./токен glm-5.3-flash (дайджест 25.09–02.10) +RATE_FULL = 0.001058 # у.е./токен glm-5.3 +Q_FLASH = 0.95 # полезность flash-токена относительно полного +BASELINE_NET = 0.95 # выручка бейзлайна = 95% расхода (−5%/день) +HORIZON_DAYS = 365 + + +def _fmt(v): + return None if v is None else round(float(v), 2) + + +# ────────────────────────── чтение данных ────────────────────────── + +def read_balances(): + """Балансы кооператива из docs/aum/balances.json. total=None если нет файла.""" + try: + path = os.environ.get("SIMBIOT_BALANCES_JSON", BALANCES_JSON) + with open(path) as f: + data = json.load(f) + accs = [a for a in data.get("accounts", []) + if a.get("amount_usd") is not None] + return {"total": _fmt(sum(float(a["amount_usd"]) for a in accs)) + if accs else None, "accounts": accs, "src": "balances.json"} + except Exception: + return {"total": None, "accounts": [], "src": "balances.json (нет файла)"} + + +def read_router_spend(days: int = 7): + """Суточный расход 9router за N дней: {date: {cost, reqs}}. Ошибка → {}.""" + try: + db = sqlite3.connect(f"file:{ROUTER_DB}?mode=ro", timeout=5, uri=True) + rows = db.execute( + "SELECT substr(timestamp,1,10) d, ROUND(SUM(cost),4) c, COUNT(*) r " + "FROM usageHistory WHERE timestamp >= datetime('now', ?) " + "GROUP BY d ORDER BY d", (f"-{int(days)} days",)).fetchall() + db.close() + return {d: {"cost": c, "reqs": r} for d, c, r in rows} + except Exception: + return {} + + +def read_coop_structure(): + """Структура кооператива (проекты/выручочные доли) — множитель метрики.""" + try: + with open(COOP_JSON) as f: + data = json.load(f) + return {"title": (data.get("mission") or {}).get("title"), + "projects": [{"name": p.get("name"), + "revenue_pct": p.get("revenue_pct")} + for p in data.get("projects", [])]} + except Exception: + return {"title": None, "projects": []} + + +# ────────────────────────── замер ────────────────────────── + +def measure(): + """Снапшот метрики: AUM (если замерен) + burn 24ч/7д + структура кооператива.""" + bal = read_balances() + spend = read_router_spend(days=7) + days_iter = list(spend.values()) + return { + "ts": datetime.now(timezone.utc).isoformat(), + "aum": bal["total"], "balances_src": bal["src"], + "burn_24h": _fmt(days_iter[-1]["cost"]) if days_iter else 0.0, + "burn_7d": _fmt(sum(v["cost"] for v in days_iter)), + "spend_days": spend, + "coop": read_coop_structure(), + } + + +# ────────────────────────── симуляция сценариев ────────────────────────── + +def _model_params(config, productivity): + volume = min(3.0, max(0.05, float(config.get("volume_mult", 1.0)))) + flash = min(1.0, max(0.0, float(config.get("flash_share", 0.84)))) + rate = flash * RATE_FLASH + (1 - flash) * RATE_FULL + quality = flash * Q_FLASH + (1 - flash) * 1.0 + return volume, rate, quality, productivity + + +def calibrate(burn_hist: list, base_config: dict) -> float: + """P бейзлайна: выручка = BASELINE_NET × средний burn (давление к оптимизации).""" + mean_b = statistics.mean(burn_hist) or 100.0 + _, rate, quality, _ = _model_params(base_config, 0.0) + utility = (mean_b / rate) * quality + return BASELINE_NET * mean_b / max(utility ** 0.5, 1e-9) + + +def simulate(config: dict, burn_hist: list, productivity: float, + days: int = HORIZON_DAYS, seed: int = 42, + trials: int = 120) -> dict: + """E[ΔAUM в горизонте days]: бюджет дня B=volume·burn_t покупает токены + B/rate; полезность токенов×quality → выручка P·√польз; Δ = выручка − B.""" + volume, rate, quality, P = _model_params(config, productivity) + rng = random.Random(seed) + finals = [] + for _ in range(trials): + aum = 0.0 + for _d in range(days): + B = volume * rng.choice(burn_hist) + utility = (B / rate) * quality + aum += P * (utility ** 0.5) - B + finals.append(aum) + return {"mean": statistics.mean(finals), + "p10": statistics.quantiles(finals, n=10)[0]} + + +def score(config, burn_hist, productivity, seed=42): + return simulate(config, burn_hist, productivity, seed=seed)["mean"] + + +# ────────────────────────── генетический отбор ────────────────────────── + +def evolve(burn_hist: list, pop_size: int = 12, generations: int = 6, + seed: int = 7) -> dict: + """Генетический отбор по fitness=E[ΔAUM]; песочница — отчёт-адвокат.""" + rng = random.Random(seed) + base_cfg = {"volume_mult": 1.0, "flash_share": 0.84} # «как сейчас» + P = calibrate(burn_hist, base_cfg) + base_fit = score(base_cfg, burn_hist, P, seed=seed) + + def mutate(c): + return {"volume_mult": round(min(3.0, max(0.05, c["volume_mult"] + rng.gauss(0, 0.2))), 3), + "flash_share": round(min(1.0, max(0.0, c["flash_share"] + rng.gauss(0, 0.12))), 3)} + + def crossover(a, b): + return {"volume_mult": round((a["volume_mult"] + b["volume_mult"]) / 2, 3), + "flash_share": round((a["flash_share"] + b["flash_share"]) / 2, 3)} + + pop = [{"volume_mult": round(rng.uniform(0.3, 1.8), 3), + "flash_share": round(rng.uniform(0.2, 1.0), 3)} + for _ in range(pop_size)] + best, best_fit = base_cfg, base_fit + history = [] + for g in range(generations): + scored = sorted(((score(c, burn_hist, P, seed=seed + g), c) for c in pop), + reverse=True) + if scored[0][0] > best_fit: + best_fit, best = scored[0][0], dict(scored[0][1]) + history.append(round(scored[0][0], 2)) + elite = [c for _, c in scored[: max(2, pop_size // 3)]] + pop = list(elite) + while len(pop) < pop_size: + pop.append(mutate(crossover(rng.choice(elite), rng.choice(elite)))) + gap = (best_fit - base_fit) / abs(base_fit) * 100 if base_fit else 0.0 + return { + "baseline": base_cfg, "baseline_fitness": round(base_fit, 2), + "best": best, "best_fitness": round(best_fit, 2), + "improves": bool(best_fit > base_fit + 1e-9), + "improvement_pct": round(gap, 2), + "generations": generations, "pop_size": pop_size, + "productivity_P": round(P, 4), + "burn_mean_day": round(statistics.mean(burn_hist), 2), + "fitness_history": history, + "verdict": ("конфигурация лучше статус-кво — вынести дню/Alex'у " + "(адвокат, не авто-применение)") + if best_fit > base_fit + 1e-9 else "статус-кво остаётся", + } + + +def run_pilot(m: dict) -> dict: + """Пилот на свежем замере: история burn → симуляции → отбор → вывод.""" + hist = [v["cost"] for v in m.get("spend_days", {}).values()] or [100.0] + return {"ts": m["ts"], "aum": m["aum"], "burn_7d": m.get("burn_7d"), + "evolution": evolve(hist), + "note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"} + + +def build_policy_aum_summary(m: dict, pilot: dict = None) -> str: + """Однострочник для policy/контекста всех уровней.""" + aum = m.get("aum") + part = (f"AUM={aum} у.е." if aum is not None + else "AUM пока не замерен (когда будет баланс — занеси в " + "docs/aum/balances.json)") + ev = (pilot or {}).get("evolution") or {} + tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else "" + return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов " + f"на длинной дистанции). {part}; burn 9router 7д=" + f"{m.get('burn_7d', 0)} у.е. (прямой минус к AUM). Фильтр любого " + f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; " + f"короткий доход, съедающий долгосрочную базу — ниже приоритетом." + f"{tail}") \ No newline at end of file diff --git a/harness/es.py b/harness/es.py index 6715a21..9480b40 100644 --- a/harness/es.py +++ b/harness/es.py @@ -68,6 +68,52 @@ def init_db(): # (aggregate, aggregate_id) уже даёт UNIQUE(level, seq). db.commit() db.close() + _ensure_signature_schema() + + +_signature_schema_ready = False + + +def _ensure_signature_schema(): + """Крипто-аудит (t-260930083716): колонки подписи + ретро-генезис. + + Миграция идемпотентна, лёгкая (ALTER TABLE / UPDATE по индексу), гонки + между процессами безопасны (IF NOT EXISTS / WHERE sig IS NULL). + Существующие события ДО внедрения подписи — ретро-маркер генезиса + sig='genesis:': честная граница доверия (см. harness/audit.py). + """ + global _signature_schema_ready + if _signature_schema_ready: + return + from . import audit + with _LOCK: + db = _conn() + cols = {r["name"] for r in db.execute("PRAGMA table_info(events)").fetchall()} + for col in ("sig", "key_id", "prev_sig"): + if col not in cols: + db.execute(f"ALTER TABLE events ADD COLUMN {col} TEXT") + db.commit() + # водяной знак генезиса: раз и навсегда = max(id) на момент первой + # миграции; Unsigned-строки ≤ знака — «до внедрения подписи» (genesis), + # позже — legacy (старый код демонов после включения). Окно гонки в + # миллисекунды между водяным знаком и UPDATE — наихудший случай: одна + # лишняя строка под genesis, на целостность не влияет. + row = db.execute("SELECT last_event FROM projection_meta " + "WHERE name='sig_genesis_upto'").fetchone() + upto = row["last_event"] if row else None + if upto is None: + upto = db.execute("SELECT COALESCE(MAX(id), 0) FROM events").fetchone()[0] + db.execute("INSERT INTO projection_meta (name, last_event, updated_at) " + "VALUES ('sig_genesis_upto', ?, ?)", + (upto, datetime.now(timezone.utc).isoformat())) + n = db.execute( + "UPDATE events SET sig='genesis:'||id WHERE sig IS NULL AND id <= ?", + (upto,)).rowcount + db.commit() + db.close() + if n: + print(f"[audit] retro-genesis: {n} событий до внедрения подписи", flush=True) + _signature_schema_ready = True # ────────────────── ИДЕМПОТЕНТНАЯ ДОСТАВКА (t-260930083937) ────────────────── @@ -208,8 +254,17 @@ def delivery_release(artifact_id: str): def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict): - """Append-only запись события. Возвращает (event_id, version) или None при дубликате.""" + """Append-only запись события. Возвращает (event_id, version) или None при дубликате. + + Крипто-аудит (t-260930083716): каждое событие подписывается приватным + ключом харнеса (ed25519) в момент записи; prev_sig связывает с предыдущим + (тампер-цепочка). Отказ подписи НЕ валит запись — событие уходит без sig + (верификатор пометит legacy), харнес продолжает работать. + """ with _LOCK: + _ensure_signature_schema() # лениво: колонки подписи/genesis должны + # существовать ДО INSERT (append_event зовут и без init_db — + # t-260930083716: без этого трейсы молча терялись на копиях БД) db = _conn() row = db.execute( "SELECT MAX(version) AS v FROM events WHERE aggregate=? AND aggregate_id=?", @@ -217,11 +272,22 @@ def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict) ).fetchone() version = (row["v"] or 0) + 1 ts = datetime.now(timezone.utc).isoformat() + data_str = json.dumps(data, ensure_ascii=False) + try: + from . import audit as _audit + prev = db.execute( + "SELECT sig FROM events ORDER BY id DESC LIMIT 1").fetchone() + sig_f = _audit.sign_event_row(aggregate, aggregate_id, event_type, + data_str, ts, version, + prev["sig"] if prev else None) + except Exception: + sig_f = {} # без подписи (legacy) — аудит отразит, харнес не падает try: cur = db.execute( - "INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version) VALUES (?,?,?,?,?,?)", - (aggregate, aggregate_id, event_type, - json.dumps(data, ensure_ascii=False), ts, version), + "INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version," + "sig,key_id,prev_sig) VALUES (?,?,?,?,?,?,?,?,?)", + (aggregate, aggregate_id, event_type, data_str, ts, version, + sig_f.get("sig"), sig_f.get("key_id"), sig_f.get("prev_sig")), ) db.commit() eid = cur.lastrowid @@ -399,6 +465,14 @@ def _apply_event(model: dict, ev: dict): model["cycle_state"].setdefault(data["level"], {})["paused"] = False elif etype == "PolicyChanged": model.setdefault("policy", {}).update(data) + elif etype == "AumMeasured": + # северная звезда (t-260929161138): последняя метрика AUM/burn — + # в policy (build_context инжектит всем уровням), история — кап 120 снапшотов + model.setdefault("policy", {}).update(data.get("policy", {})) + hist = model.setdefault("aum_history", []) + hist.append({k: v for k, v in data.items() if k != "policy"}) + if len(hist) > 120: + model["aum_history"] = hist[-120:] elif etype == "BudgetSpent": spend = model.setdefault("spend", []) spend.append(data) @@ -529,6 +603,20 @@ def run_projections(): if not rows: db.close() return _read_model + # верификация хвоста (t-260930083716): дельта событий должна быть + # подписана и не ломать цепочку; подделка = RuntimeError (проекции + # останавливаются — подделанное событие не попадает в read model) + try: + from . import audit as _audit + rep = _audit.verify_rows([dict(r) for r in rows]) + if not rep["ok"]: + raise RuntimeError( + f"audit: подпись события id={rep['first_bad']} не прошла — " + f"стор скомпрометирован, проекция остановлена") + except RuntimeError: + raise + except Exception: + pass # сам аудит не валит проекции (нет ключа/либ) — полный аудит: cli verify for r in rows: _apply_event(_read_model, dict(r)) new_last = rows[-1]["id"] diff --git a/harness/scheduler.py b/harness/scheduler.py index 94f630d..b9c701c 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -247,10 +247,13 @@ def build_context(level_cfg, model: dict, batch=None): """ parts = [level_cfg.mandate] - # идентичность/цели v0.1 — статично из policy (если есть) + # идентичность/цели v0.1 — статично из policy (если есть); + # north-star метрика AUM (t-260929161138) — тоже policy, инжект всем уровням policy = model.get("policy") if policy and policy.get("identity"): parts.append("ИДЕНТИЧНОСТЬ: " + policy["identity"]) + if policy and policy.get("aum_target"): + parts.append("ЦЕЛЬ (AUM): " + policy["aum_target"]) # выжимки нижних уровней: последние дайджесты order = ["min", "hour", "day"] @@ -528,6 +531,20 @@ def process_level(level_cfg, model: dict): if not emit_tick(level_cfg, seq): return "already" # тик этого такта уже был (идемпотентность) + # северная звезда (t-260929161138): дневной уровень раз в сутки пишёт + # AumMeasured снапшот (AUM + burn 9router) + обновляет aum_target в policy + if level_cfg.name == "day": + try: + from . import aum as _aum + snap = _aum.measure() + es.append_event("harness", "aum", "AumMeasured", { + "ts": snap["ts"], "aum": snap["aum"], + "burn_24h": snap["burn_24h"], "burn_7d": snap["burn_7d"], + "policy": {"aum_target": _aum.build_policy_aum_summary(snap)}, + }) + except Exception: + pass # замер — best effort, не валить тик + # ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается # только если есть событие ≥ порога уровня; тихий такт отрабатывает # детерминированной механикой (как sec) — без расхода кредитов роутера. diff --git a/tests/test_audit.py b/tests/test_audit.py new file mode 100644 index 0000000..769c6ea --- /dev/null +++ b/tests/test_audit.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +"""t-260930083716: криптографическая подпись событий и команд (hermetic). + +Запуск: ./venv/bin/python tests/test_audit.py + +Проверяет: ключ генерируется один раз (0600, key_id стабилен), события +подписываются при записи (sig/key_id/prev_sig), тампер данных события +ловится аудитом (first_bad), тампер НОВОГО (не спроецированного) события +останавливает проекции (RuntimeError), genesis-маркер ретро-строк, +unsigned-послегенезис строки = legacy (не падают), сцепка prev_sig +включена, конверты команд подпись/проверка/тампер, CLI verify/audit-key. +""" +import json +import os +import sqlite3 +import subprocess +import sys + +# ── изоляция: своя БД в /tmp, свой ключ в /tmp ── +_DST = "/tmp/simbiot-test-audit.db" +for p in (_DST, _DST + "-wal", _DST + "-shm"): + if os.path.exists(p): + os.unlink(p) +os.environ["SIMBIOT_DB"] = _DST +os.environ["SIMBIOT_AUDIT_KEY_DIR"] = "/tmp/simbiot-test-audit-keys" +sys.path.insert(0, "/root/simbiot-harness") + +from harness import audit, es # noqa: E402 + +fails = [] + + +def check(name, cond, extra=""): + print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "") + if not cond: + fails.append(name) + + +# ── 1. ключи ── +info = audit.key_info() +check("ключ создан", os.path.exists(audit.PRIV_PATH)) +check("приватный ключ 0600", info["priv_mode"] == 0o600, str(info["priv_mode"])) +kid = audit.key_id() +check("key_id 16 hex", len(kid) == 16 and all(c in "0123456789abcdef" for c in kid)) + +# ── 2. подпись при записи ── +es.init_db() +es.append_event("task", "a-1", "TaskSubmitted", {"title": "тест"}) +es.append_event("task", "a-1", "TaskClaimed", {"level": "day"}) +es.append_event("task", "a-1", "TaskCompleted", {"answer": "годно"}) +db = sqlite3.connect(_DST) +db.row_factory = sqlite3.Row +rows = [dict(r) for r in db.execute("SELECT * FROM events ORDER BY id")] +db.close() +check("3 события", len(rows) == 3) +check("все подписаны при записи", + all(r["sig"] and not r["sig"].startswith("genesis:") for r in rows)) +check("key_id совпадает", all(r["key_id"] == kid for r in rows)) +check("prev_sig = sig предыдущего", + all(rows[i]["prev_sig"] == rows[i - 1]["sig"] for i in (1, 2))) + +# ── 3. аудит чистого стора ── +rep = audit.verify_events() +check("verify: чисто", rep["ok"] and rep["checked"] == 3 and rep["total"] == 3, str(rep)) + +# ── 4. тампер уже спроецированного события: ловит ПОЛНЫЙ аудит ── +es.run_projections() +db = sqlite3.connect(_DST) +db.execute("UPDATE events SET data='{\"title\":\"TAMPERED\"}' WHERE id=2") +db.commit(); db.close() +rep = audit.verify_events() +check("тампер ловится аудитом", not rep["ok"] and rep["first_bad"] == 2, str(rep)) + +# ── 5. тампер хвоста останавливает проекции ── +os.unlink(_DST) # свежий стор +for p in (_DST + "-wal", _DST + "-shm"): + if os.path.exists(p): + os.unlink(p) +es._signature_schema_ready = False # новый инстанс схемы в этой же БД +es.init_db() +es.append_event("task", "b-1", "TaskSubmitted", {"title": "ok"}) +es.run_projections() # спроецировали id=1 +es.append_event("task", "b-1", "TaskClaimed", {"level": "day"}) # id=2 — в хвосте +db = sqlite3.connect(_DST) +db.execute("UPDATE events SET data='{\"level\":\"FAKE\"}' WHERE id=2") +db.commit(); db.close() +try: + es.run_projections() + check("подделка хвоста останавливает проекции", False, "RuntimeError не брошен") +except RuntimeError as e: + check("подделка хвоста останавливает проекции", "audit" in str(e), str(e)[:80]) + +# ── 6. genesis-маркер ретро-строк ── +db = sqlite3.connect(_DST) +db.execute("UPDATE events SET data='{\"level\":\"day\"}' WHERE id=2") # снять тампер секции 5 +db.execute("UPDATE events SET data='{\"title\":\"pre-audit\"}', sig=NULL WHERE id=1") # «старая» строка +db.execute("INSERT INTO projection_meta (name,last_event,updated_at) " + "VALUES ('sig_genesis_upto', 1, '2026-01-01') " + "ON CONFLICT(name) DO UPDATE SET last_event=1") # генезис до id=1 +db.commit() +db.close() +es._signature_schema_ready = False +es.init_db() # миграция: строка id=1 ≤ watermark и без sig → genesis +db = sqlite3.connect(_DST); db.row_factory = sqlite3.Row +r1 = dict(db.execute("SELECT sig FROM events WHERE id=1").fetchone()); db.close() +check("genesis-маркер ретро-строки", (r1["sig"] or "").startswith("genesis:"), str(r1)) +rep = audit.verify_events() +check("genesis-строки не ломают аудит", + rep["ok"] and rep["genesis"] == 1 and rep["checked"] == 1, str(rep)) + +# ── 7. unsigned-послегенезис строка = legacy, не краш ── +db = sqlite3.connect(_DST) +db.execute("INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version) " + "VALUES ('task','c-1','TaskSubmitted','{\"title\":\"x\"}','2026-01-01T00:00:00Z',1)") +db.commit(); db.close() +rep = audit.verify_events() +check("unsigned-послегенезис = legacy (аудит ок)", + rep["ok"] and rep["legacy"] == 1, str(rep)) + +# ── 8. конверты команд ── +signed = audit.sign_command({"op": "task", "title": "переписка"}) +vc = audit.verify_command(signed) +check("конверт подписан и валиден", vc["ok"] and vc["key_id_match"]) +bad = dict(signed); bad["signature"] = signed["signature"][:-4] + "AAAA" +check("конверт-тампер ловится", not audit.verify_command(bad)["ok"]) + +# ── 9. CLI verify / audit-key (hermetic) ── +env = dict(os.environ) +r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "verify"], + capture_output=True, text=True, timeout=60, env=env) +check("cli verify exit0", r.returncode == 0, r.stdout + r.stderr) +r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "audit-key"], + capture_output=True, text=True, timeout=60, env=env) +check("cli audit-key показывает ключ", + r.returncode == 0 and "public_b64" in r.stdout, r.stdout + r.stderr) + +print() +print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}") +sys.exit(1 if fails else 0) \ No newline at end of file diff --git a/tests/test_aum.py b/tests/test_aum.py new file mode 100644 index 0000000..5f5e561 --- /dev/null +++ b/tests/test_aum.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +"""t-260929161138: north-star метрика AUM (hermetic, без LLM/TG/9router). + +Запуск: ./venv/bin/python tests/test_aum.py + +Проверяет: замер не падает без БД роутера и balances.json (AUM=None), +детерминизм пилота (seed), отбор находит конфигурацию не хуже бейзлайна, +событие AumMeasured мержит policy.aum_target + копит aum_history (кап 120), +build_context инжектит «ЦЕЛЬ (AUM)» всем уровням, CLI aum/aum-pilot/--record +существуют, AumMeasured идемпотентен по aggregate_id. +""" +import io +import json +import os +import sys +import contextlib + +# ── изоляция: своя БД в /tmp, роутер и balances заведомо отсутствуют ── +_DST = "/tmp/simbiot-test-aum.db" +for p in (_DST, _DST + "-wal", _DST + "-shm"): + if os.path.exists(p): + os.unlink(p) +os.environ["SIMBIOT_DB"] = _DST +os.environ["SIMBIOT_ROUTER_DB"] = "/nonexistent-router.sqlite" +BAL_TMP = "/tmp/simbiot-test-balances.json" +if os.path.exists(BAL_TMP): + os.unlink(BAL_TMP) +os.environ["SIMBIOT_BALANCES_JSON"] = BAL_TMP +sys.path.insert(0, "/root/simbiot-harness") + +from harness import aum, es # noqa: E402 +from harness.levels import LEVELS # noqa: E402 +from harness.scheduler import build_context # noqa: E402 + +fails = [] + + +def check(name, cond, extra=""): + print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "") + if not cond: + fails.append(name) + + +# ── 1. замер hermetic: не падает, AUM=None, burn=0 ── +m = aum.measure() +check("measure hermetic ok", set(m) >= {"ts", "aum", "burn_24h", "burn_7d", "spend_days"}) +check("aum None без файла", m["aum"] is None) +check("burn 0 без роутера", m["burn_7d"] == 0 and m["spend_days"] == {}) + +# ── 2. balances с данными → total ── +with open(BAL_TMP, "w") as f: + json.dump({"accounts": [{"name": "a", "amount_usd": 12.5}, + {"name": "b", "amount_usd": 7.5}]}, f) +m2 = aum.measure() +check("balances→total", m2["aum"] == 20.0) + +# ── 3. детерминизм эволюции ── +h = [60., 300., 900., 500., 150., 200.] +e1, e2 = aum.evolve(h), aum.evolve(h) +check("эволюция детерминирована", + e1["best"] == e2["best"] and e1["best_fitness"] == e2["best_fitness"]) +check("отбор не хуже бейзлайна", e1["best_fitness"] >= e1["baseline_fitness"] - 1e-6) +check("fitness_history по поколениям", len(e1["fitness_history"]) == e1["generations"]) + +# ── 4. симуляция: больше burn без отдачи — всегда минус (плейсхолдер честен) ── +s_small = aum.simulate({"volume_mult": 0.2, "flash_share": 0.84}, h, aum.calibrate(h, {"volume_mult": 1.0, "flash_share": 0.84})) +s_big = aum.simulate({"volume_mult": 2.5, "flash_share": 0.84}, h, aum.calibrate(h, {"volume_mult": 1.0, "flash_share": 0.84})) +check("меньше объёма burn → лучше при плейсхолдер-модели", + s_small["mean"] > s_big["mean"]) + +# ── 5. события AumMeasured: policy мержится, история копится, кап 120 ── +# (повторный aggregate_id легален — каждая версия = новый снапшот; UNIQUE в es +# защищает только тики cycle. Дневная частота обеспечивается идемпотентностью тика.) +es.init_db() +es.append_event("harness", "aum", "AumMeasured", { + "ts": "2026-10-02T00:00:00+00:00", "aum": 20.0, "burn_24h": 1.5, + "burn_7d": 10.0, + "policy": {"aum_target": "ЦЕЛЬ: AUM↑ (тест)"}} +) +model = es.run_projections() +check("policy.aum_target записан", "AUM" in (model["policy"].get("aum_target") or "")) +check("aum_history ведётся", len(model.get("aum_history", [])) == 1) +es.append_event("harness", "aum", "AumMeasured", { + "ts": "2026-10-02T06:00:00+00:00", "aum": 21.0, "burn_24h": 1.0, + "burn_7d": 11.0, "policy": {"aum_target": "ЦЕЛЬ: AUM↑ (тест2)"}}) +model = es.run_projections() +check("повторный снапшот апендится в историю", + len(model.get("aum_history", [])) == 2) +check("aum_target перезаписан свежим", + "тест2" in (model["policy"].get("aum_target") or "")) +for i in range(118): + es.append_event("harness", "aum", "AumMeasured", { + "ts": f"2026-10-03T{i:02d}:00:00+00:00", "aum": float(i), + "burn_24h": float(i), "burn_7d": float(i), + "policy": {"aum_target": f"x{i}"}}) +model = es.run_projections() +check("кап истории 120", len(model.get("aum_history", [])) == 120) + +# ── 6. build_context инжектит цель всем LLM-уровням ── +ok_ctx = True +for name in ("min", "hour", "day"): + ctx = build_context(LEVELS[name], model, batch=[]) + if "ЦЕЛЬ (AUM)" not in ctx: + ok_ctx = False + print(f" ({name}: цель не в контексте)") +check("ЦЕЛЬ (AUM) в контексте min/hour/day", ok_ctx) + +# ── 7. summary содержит фильтр +1% и burn ── +summ = aum.build_policy_aum_summary(m2, {"evolution": e1}) +check("summary: фильтр +1% и burn", "+1%" in summ and "burn" in summ) + +# ── 8. CLI: команды существуют и работают hermetic ── +env = dict(os.environ, SIMBIOT_DB=_DST, SIMBIOT_ROUTER_DB="/nonexistent-router.sqlite", + SIMBIOT_BALANCES_JSON=BAL_TMP) +import subprocess +r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "aum"], + capture_output=True, text=True, timeout=60, env=env) +check("cli aum ok", r.returncode == 0 and "AUM" in r.stdout, r.stderr[:200]) +r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", + "aum", "--record"], capture_output=True, text=True, timeout=60, env=env) +check("cli aum --record ok -> стор читает цель", + r.returncode == 0 and "OK: AumMeasured" in r.stdout) +model = es.run_projections() +check("aum_target в сторе после --record", + "AUM" in (model["policy"].get("aum_target") or "")) +r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "aum-pilot"], + capture_output=True, text=True, timeout=120, env=env) +check("cli aum-pilot ok", r.returncode == 0 and "вердикт" in r.stdout, r.stderr[:200]) + +print() +print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}") +sys.exit(1 if fails else 0) \ No newline at end of file