feat(day): криптоаудит стор/команд (ed25519, genesis-граница), метрика AUM (замер+policy+пилот), правки для портал-развёртывания (t-260930083716, t-260929161138, t-260930110622)

This commit is contained in:
Simbiot Harness 2026-10-02 00:34:07 +00:00
parent fa634ce526
commit e65d205e0f
17 changed files with 1441 additions and 5 deletions

9
.gitignore vendored
View file

@ -9,3 +9,12 @@ __pycache__/
.env .env
tg_inbox.db tg_inbox.db
simbiot.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

View file

@ -12,6 +12,22 @@ Strands Harness SDK как исполнительное ядро, модели
цели стоит автоприоритизация доски задач (t-260929161138; уточнение цели стоит автоприоритизация доски задач (t-260929161138; уточнение
автоприоритизации день+час — t-260930153845, задача e-260930161330). автоприоритизации день+час — 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) ## Лестница (v0.1)
| Уровень | Интервал | Контекст | Модель | Задачи | | Уровень | Интервал | Контекст | Модель | Задачи |

91
cli.py
View file

@ -97,6 +97,20 @@ def main():
p_ack.add_argument("-n", "--note", default="", help="что сделали с эскалацией") p_ack.add_argument("-n", "--note", default="", help="что сделали с эскалацией")
sub.add_parser("status", 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="доска задач") sub.add_parser("tasks", help="доска задач")
p_ev = sub.add_parser("events", help="хвост стора (последние N событий)") p_ev = sub.add_parser("events", help="хвост стора (последние N событий)")
@ -308,6 +322,83 @@ def main():
elif args.cmd == "status": elif args.cmd == "status":
es.init_db() es.init_db()
print(json.dumps(commands.status(), ensure_ascii=False, indent=1)) 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": elif args.cmd == "tasks":
es.init_db() es.init_db()
for t in commands.list_tasks(): for t in commands.list_tasks():

View file

@ -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
через директиву (не молча в лендинге).

71
docs/audit.md Normal file
View file

@ -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:<id>'` (водяной знак `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)

48
docs/aum.md Normal file
View file

@ -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-отчёт в манифесте дня (дельта к прошлой неделе).

View file

@ -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 не калиброван выручкой,
пилот отвечает только на вопрос «где направление улучшения», не «сколько именно».

View file

@ -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": ""
}

View file

@ -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.

View file

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simbiot Harness — самодиагностика</title>
<style>
:root{--bg:#0d1117;--card:#161b22;--line:#30363d;--tx:#e6edf3;--dim:#8b949e;--ok:#3fb950;--warn:#d29922;--bad:#f85149;--acc:#58a6ff}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--tx);font:15px/1.55 -apple-system,'Segoe UI',Roboto,sans-serif;padding:24px}
.wrap{max-width:860px;margin:0 auto}
h1{font-size:22px;margin-bottom:4px}
.sub{color:var(--dim);font-size:13px;margin-bottom:20px}
.verdict{background:linear-gradient(90deg,#12261a,#161b22);border:1px solid var(--ok);border-radius:10px;padding:14px 18px;margin-bottom:20px;font-size:15px}
.verdict b{color:var(--ok)}
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;margin-bottom:20px;font-size:14px}
th{background:#1c2128;text-align:left;padding:9px 12px;color:var(--dim);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em}
td{padding:9px 12px;border-top:1px solid var(--line);vertical-align:top}
td.num{font-variant-numeric:tabular-nums}
.ok{color:var(--ok);font-weight:600}
.warn{color:var(--warn);font-weight:600}
.bad{color:var(--bad);font-weight:600}
.acc{color:var(--acc)}
.dim{color:var(--dim)}
h2{font-size:14px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;margin:22px 0 10px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-bottom:8px}
.stat{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.stat .v{font-size:20px;font-weight:700}
.stat .l{font-size:12px;color:var(--dim);margin-top:2px}
ul.notes{list-style:none}
ul.notes li{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin-bottom:8px;font-size:14px}
.foot{color:var(--dim);font-size:12px;margin-top:24px;border-top:1px solid var(--line);padding-top:12px}
code{background:#1c2128;border:1px solid var(--line);border-radius:5px;padding:1px 6px;font-size:12.5px}
</style>
</head>
<body>
<div class="wrap">
<h1>🩺 Simbiot Harness — самодиагностика</h1>
<div class="sub">Снимок 2026-10-01 09:12 UTC · сгенерировано мин-уровнем из event store + <code>cli.py status</code> по запросу Alex (m-516173335/336)</div>
<div class="verdict">
Общий вердикт: <b>ЗДОРОВ</b> — все 4 уровня тикают, очередь идёт, эскалаций открыто 0,
падений циклов сегодня 1 (разобрано и закрыто на 09:11, повторов нет).
</div>
<h2>Циклы (beat)</h2>
<table>
<tr><th>Уровень</th><th>Тиков всего</th><th>Последний тик (UTC)</th><th>Состояние</th></tr>
<tr><td><b>sec</b> — пульс</td><td class="num">150 261</td><td class="num">09:12:16</td><td class="ok">OK, секундный ритм без пауз</td></tr>
<tr><td><b>min</b> — триаж/мелочи</td><td class="num">1 986</td><td class="num">09:12:01</td><td><span class="ok">OK</span> · <span class="dim">stale last_error от 08:35 — само разрешено, счётчик не сбрасывается</span></td></tr>
<tr><td><b>hour</b> — задачи/дайджест</td><td class="num">44</td><td class="num">09:00 → 09:11:59</td><td class="ok">OK · тик закрылся дайджестом (~12 мин, 1 эскалация разобрана)</td></tr>
<tr><td><b>day</b> — стратегия</td><td class="num">3</td><td class="num">00:00:00</td><td><span class="ok">OK</span> · <span class="dim">night tick 00:00–00:53, ошибок не оставил</span></td></tr>
</table>
<h2>Счётчики</h2>
<div class="grid">
<div class="stat"><div class="v acc">8</div><div class="l">задач в ready (очередь)</div></div>
<div class="stat"><div class="v ok">46</div><div class="l">задач закрыто всего</div></div>
<div class="stat"><div class="v ok">0</div><div class="l">открытых эскалаций</div></div>
<div class="stat"><div class="v">400</div><div class="l">дайджестов накоплено</div></div>
<div class="stat"><div class="v acc">132 / 130</div><div class="l">вход/ответы TG за 24 ч — backlog нет</div></div>
<div class="stat"><div class="v acc">31 / 31</div><div class="l">доставок файлов/сообщений за 24 ч (idempotent ledger, все tg_ok)</div></div>
<div class="stat"><div class="v">907</div><div class="l">LLM-вызовов сегодня (BudgetSpent)</div></div>
</div>
<h2>Что болело / что болит</h2>
<ul class="notes">
<li><span class="ok">✅ Разобрано</span> — <b>min-тик 08:35</b>: guard «бюджет исчерпан» вызывал requeue, а тот кидал ValueError на ready-статус → падение тика <span class="dim">(e-261001083543, acked и разобрано часовым в тике 09:00–09:11; задача «list» штатно закрыта)</span>.</li>
<li><span class="ok">✅ Прошло</span> — вчерашние таймауты <code>_llm_worker</code> (min 300s / hour 2880s, последний раз 30.09 16:48) сегодня не повторились ни разу.</li>
<li><span class="ok">✅ Прошло</span> — «cannot schedule new futures after shutdown» (29.09, после рестарта демона) — не встречается.</li>
<li><span class="warn">⚠ Наблюдение</span> — у min в <code>status</code> висит last_error от 08:35, хотя инцидент разобран: счётчик ошибок не сбрасывается, косметика — можно добавить сброс при успешном тике.</li>
<li><span class="warn">⚠ Наблюдение</span> — активной периодической самопроверки нет: здоровье видно только из digests/escalations; можно добавить hourly health-check-пробу (ticks растут, CycleError=0, inbox пуст) в часовой дайджест.</li>
</ul>
<h2>Чем себя диагностирую</h2>
<table>
<tr><th>Механизм</th><th>Что даёт</th></tr>
<tr><td><code>cli.py status</code></td><td>beat по 4 уровням: ticks, last_tick, errors, spend — жив ли каждый такт</td></tr>
<tr><td><code>cli.py escalations</code></td><td>реестр падений/проблем с ack-трекингом: ничего не потеряно</td></tr>
<tr><td>Event store (events)</td><td>CycleError, TaskFailed, DeliverySent/Skipped — с трейсбинами, можно раскрутить любую ошибку</td></tr>
<tr><td>delivery_ledger</td><td>идемпотентность отправок в TG: дублей не бывает, пропуски видны</td></tr>
<tr><td>Дайджесты (400+)</td><td>ритучевые отчёты каждого уровня — история, а не только «сейчас»</td></tr>
</table>
<div class="foot">
Источник: sqlite event store (<code>harness/harness.db</code>: CycleError, InvocationStarted, MessageReceived, DeliverySent…) + <code>cli.py status</code> на 2026-10-01T09:12Z.<br>
Simbiot Harness · кооператив 42 · мин-уровень, тик 09:12
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simbiot — Proactive AI Harness for Your Org · Presale $99.9/mo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;600;800&display=swap" rel="stylesheet">
<style>
:root{--ink:#1c1a17;--bg:#faf8f3;--panel:#f2efe7;--line:#e2ded2;--muted:#6f6a5e;--accent:#5b4be0;--accent2:#ffffff;--bad:#c0392b;}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{font-family:'Inter Tight',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--ink);line-height:1.6;letter-spacing:.01em}
.wrap{max-width:1080px;margin:0 auto;padding:0 24px}
section{padding:72px 0}
h1{font-size:clamp(2.4rem,6vw,4.2rem);font-weight:800;line-height:1.05;letter-spacing:-.02em}
h2{font-size:clamp(1.8rem,4vw,2.6rem);font-weight:800;letter-spacing:-.02em;margin-bottom:28px}
h3{font-size:1.15rem;font-weight:600;margin-bottom:8px}
p{color:var(--muted)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.badge{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:999px;padding:6px 16px;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:22px}
.badge .dot{width:8px;height:8px;border-radius:50%;background:var(--accent);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
.cta{display:inline-block;background:var(--accent);color:var(--accent2);text-decoration:none;font-weight:600;padding:14px 30px;border-radius:8px;transition:transform .15s}
.cta:hover{transform:translateY(-2px)}
.ghost{display:inline-block;color:var(--ink);text-decoration:none;font-weight:600;padding:14px 20px;border-bottom:2px solid var(--accent)}
/* hero */
.hero{padding:96px 0 64px}
.hero p.lead{font-size:1.25rem;max-width:640px;margin-top:22px}
.hero-row{display:flex;gap:32px;align-items:end;flex-wrap:wrap;margin-top:30px}
/* counter */
.counter{background:var(--ink);color:#fff;border-radius:16px;padding:26px 32px;min-width:280px}
.counter b{font-size:3rem;font-weight:800;color:#fff}
.counter span.cap{display:block;color:#b9b3a5;font-size:.85rem;text-transform:uppercase;letter-spacing:.1em;margin-top:4px}
/* principles */
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:26px}
.k{display:inline-block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.72rem;padding:3px 10px;border-radius:999px;border:1px solid var(--line);color:var(--muted);margin-bottom:12px;background:#fff}
/* offer */
.offer{background:var(--ink);color:#fff;border-radius:20px;padding:56px 48px;display:flex;gap:48px;align-items:center;flex-wrap:wrap;justify-content:space-between}
.offer .price{font-size:4.5rem;font-weight:800;line-height:1}
.offer .price small{font-size:1.1rem;font-weight:400;color:#b9b3a5}
.offer p{color:#b9b3a5}
.offer .cta{background:#fff;color:var(--ink)}
.offer ul{list-style:none;margin:18px 0 0}
.offer li{padding:6px 0;color:#e8e4da}
.offer li::before{content:'✓';color:#9f93ff;margin-right:10px;font-weight:700}
/* faq */
.faq{max-width:760px}
.faq details{border-bottom:1px solid var(--line);padding:16px 0}
.faq summary{font-weight:600;cursor:pointer;list-style:none}
.faq summary::after{content:'+';float:right;color:var(--accent);font-weight:800}
.faq details[open] summary::after{content:'–'}
/* prefooter */
.prefoot{background:var(--panel);border-top:1px solid var(--line);text-align:center}
.prefoot .counter{display:inline-block;min-width:240px;margin-top:20px}
footer{padding:40px 0;border-top:1px solid var(--line);font-size:.85rem;color:var(--muted)}
footer a{color:var(--muted)}
@media(max-width:640px){.offer{padding:40px 24px}.offer .price{font-size:3.2rem}}
</style>
</head>
<body>
<header class="hero">
<div class="wrap">
<div class="badge"><span class="dot"></span> Presale · 10 final seats</div>
<h1>Your org deserves<br>its own AI harness.</h1>
<p class="lead">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.</p>
<div class="hero-row">
<a class="cta" href="#offer">Reserve a seat — $99.9/mo</a>
<div class="counter"><b>10</b><span class="cap">seats left</span></div>
</div>
</div>
</header>
<section id="why">
<div class="wrap">
<h2>Built safe by design. Runs proactively by default.</h2>
<div class="grid">
<div class="card"><span class="k">safe by design</span><h3>Cryptographic trail</h3><p>Every event, command and artifact is signed and recorded. You can replay exactly how any outcome happened — nothing lives in someone's head.</p></div>
<div class="card"><span class="k">safe by design</span><h3>Human approval gates</h3><p>Irreversible actions require a human decision. The agent escalates instead of improvising when stakes rise.</p></div>
<div class="card"><span class="k">safe by design</span><h3>Idempotent everywhere</h3><p>Retries never double-send, double-charge or double-book. Crash-safe by construction, not by supervision.</p></div>
<div class="card"><span class="k">proactive</span><h3>Triage in minutes</h3><p>Incoming requests are answered, batched and closed while your team is still reading them. Nothing waits for the next morning.</p></div>
<div class="card"><span class="k">proactive</span><h3>Autonomous execution</h3><p>Real tools, real deliverables: files, deployments, reports, payments. Not a chat toy — a worker with an audit log.</p></div>
<div class="card"><span class="k">proactive</span><h3>One organizational rhythm</h3><p>Minute/hour/day cadence keeps tasks, escalations and digests flowing up and down the org — on schedule, without micromanagement.</p></div>
</div>
</div>
</section>
<section id="offer">
<div class="wrap">
<div class="offer">
<div>
<div style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em;color:#9f93ff;margin-bottom:10px">Presale offer</div>
<div class="price">$99.9<small>/mo</small></div>
<ul>
<li>Dedicated Simbiot harness for your org</li>
<li>Setup, onboarding &amp; delivery by end of month</li>
<li>Priority support during presale</li>
</ul>
</div>
<div style="text-align:center">
<a class="cta" href="https://buy.stripe.com/REPLACE_WITH_PAYMENT_LINK">Pre-order now</a>
<div style="margin-top:16px;font-size:.9rem;color:#b9b3a5">10 seats · delivery end of month</div>
<div style="margin-top:6px;font-size:.85rem;color:#9f93ff">Full price after presale: $100/mo</div>
<div style="margin-top:14px"><b style="font-size:2rem">10</b> <span style="color:#b9b3a5;font-size:.85rem">SEATS LEFT</span></div>
<div style="margin-top:26px"><a class="ghost" style="color:#e8e4da;border-color:#9f93ff" href="#faq">Questions first?</a></div>
<p style="margin-top:14px;font-size:.8rem">Secure checkout via Stripe</p>
</div>
</div>
</div>
</section>
<section id="faq" class="faq wrap">
<h2>F.A.Q.</h2>
<details><summary>Is the $99.9 presale price locked forever?</summary><p>Yes — presale subscribers keep $99.9/mo for the lifetime of the subscription. The standard subscription is $100/mo once presale ends.</p></details>
<details open><summary>What exactly do I get?</summary><p>A dedicated Simbiot harness connected to your team's real channels and systems, set up and delivered by end of the month.</p></details>
<details><summary>What does "proactive" actually mean?</summary><p>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.</p></details>
<details><summary>How is it safe?</summary><p>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.</p></details>
<details><summary>Who is behind Simbiot?</summary><p>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.</p></details>
<details><summary>Can I cancel anytime?</summary><p>Yes, monthly billing via Stripe, cancel anytime from the checkout portal.</p></details>
<details><summary>What will my $99.9 go toward?</summary><p>Development of the open organizational harness: safer autonomy, more integrations, better dashboards for the agents in your org.</p></details>
</section>
<div class="prefoot">
<div class="wrap">
<h2>10 seats. One harness that never sleeps.</h2>
<div class="counter"><b>10</b><span class="cap">seats left</span></div>
<div style="margin-top:24px"><a class="cta" href="https://buy.stripe.com/REPLACE_WITH_PAYMENT_LINK">Pre-order now</a></div>
</div>
</div>
<footer>
<div class="wrap" style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px">
<span>© 2026 42 Cooperative</span>
<span><a href="#">Privacy</a> · <a href="#">Terms</a> · <a href="#">Contact</a></span>
</div>
</footer>
</body>
</html>

256
harness/audit.py Normal file
View file

@ -0,0 +1,256 @@
"""Simbiot Harness — криптографический аудит (t-260930083716).
Alex (m-516173240): у харнеса свой приватный ключ; каждое событие стора и
каждая команда подписываются ним. Цели: полная аудируемость, доказуемое
авторство («это сделал харнес»), защита стора/командной шины от инъекций
(подделка ≠ валид).
Схема:
- ed25519 (cryptography), приватный ключ на хосте 0600 —
<repo>/.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:<id>' (не подписаны криптоключом, честно оговорено;
после включения всё новое подписано).
- Верификация: `cli.py verify` — полный аудит всей цепочки; каждая
проекция (run_projections) проверяет хвостовую дельту и цепочку-голову;
КРИПТО-ошибка подписи ⇒ RuntimeError — проекции останавливаются
(подделанное событие не проектируется). Без-подписные «послегенезис»
строки (писались старым кодом до рестарта демонов) — учитываются
как legacy, ошибка не поднимается.
- Команды: cli.py audit-command '<json>' — подписывает конверт команды;
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> — ретро-маркер строк ДО внедрения подписи (доверенная
граница): маркер сверяется со своим 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]}

227
harness/aum.py Normal file
View file

@ -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}")

View file

@ -68,6 +68,52 @@ def init_db():
# (aggregate, aggregate_id) уже даёт UNIQUE(level, seq). # (aggregate, aggregate_id) уже даёт UNIQUE(level, seq).
db.commit() db.commit()
db.close() 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:<id>': честная граница доверия (см. 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) ────────────────── # ────────────────── ИДЕМПОТЕНТНАЯ ДОСТАВКА (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): 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: with _LOCK:
_ensure_signature_schema() # лениво: колонки подписи/genesis должны
# существовать ДО INSERT (append_event зовут и без init_db —
# t-260930083716: без этого трейсы молча терялись на копиях БД)
db = _conn() db = _conn()
row = db.execute( row = db.execute(
"SELECT MAX(version) AS v FROM events WHERE aggregate=? AND aggregate_id=?", "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() ).fetchone()
version = (row["v"] or 0) + 1 version = (row["v"] or 0) + 1
ts = datetime.now(timezone.utc).isoformat() 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: try:
cur = db.execute( cur = db.execute(
"INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version) VALUES (?,?,?,?,?,?)", "INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version,"
(aggregate, aggregate_id, event_type, "sig,key_id,prev_sig) VALUES (?,?,?,?,?,?,?,?,?)",
json.dumps(data, ensure_ascii=False), ts, version), (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() db.commit()
eid = cur.lastrowid eid = cur.lastrowid
@ -399,6 +465,14 @@ def _apply_event(model: dict, ev: dict):
model["cycle_state"].setdefault(data["level"], {})["paused"] = False model["cycle_state"].setdefault(data["level"], {})["paused"] = False
elif etype == "PolicyChanged": elif etype == "PolicyChanged":
model.setdefault("policy", {}).update(data) 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": elif etype == "BudgetSpent":
spend = model.setdefault("spend", []) spend = model.setdefault("spend", [])
spend.append(data) spend.append(data)
@ -529,6 +603,20 @@ def run_projections():
if not rows: if not rows:
db.close() db.close()
return _read_model 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: for r in rows:
_apply_event(_read_model, dict(r)) _apply_event(_read_model, dict(r))
new_last = rows[-1]["id"] new_last = rows[-1]["id"]

View file

@ -247,10 +247,13 @@ def build_context(level_cfg, model: dict, batch=None):
""" """
parts = [level_cfg.mandate] parts = [level_cfg.mandate]
# идентичность/цели v0.1 — статично из policy (если есть) # идентичность/цели v0.1 — статично из policy (если есть);
# north-star метрика AUM (t-260929161138) — тоже policy, инжект всем уровням
policy = model.get("policy") policy = model.get("policy")
if policy and policy.get("identity"): if policy and policy.get("identity"):
parts.append("ИДЕНТИЧНОСТЬ: " + policy["identity"]) parts.append("ИДЕНТИЧНОСТЬ: " + policy["identity"])
if policy and policy.get("aum_target"):
parts.append("ЦЕЛЬ (AUM): " + policy["aum_target"])
# выжимки нижних уровней: последние дайджесты # выжимки нижних уровней: последние дайджесты
order = ["min", "hour", "day"] order = ["min", "hour", "day"]
@ -528,6 +531,20 @@ def process_level(level_cfg, model: dict):
if not emit_tick(level_cfg, seq): if not emit_tick(level_cfg, seq):
return "already" # тик этого такта уже был (идемпотентность) 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-тик запускается # ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
# только если есть событие ≥ порога уровня; тихий такт отрабатывает # только если есть событие ≥ порога уровня; тихий такт отрабатывает
# детерминированной механикой (как sec) — без расхода кредитов роутера. # детерминированной механикой (как sec) — без расхода кредитов роутера.

139
tests/test_audit.py Normal file
View file

@ -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)

132
tests/test_aum.py Normal file
View file

@ -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)