feat(day): криптоаудит стор/команд (ed25519, genesis-граница), метрика AUM (замер+policy+пилот), правки для портал-развёртывания (t-260930083716, t-260929161138, t-260930110622)
This commit is contained in:
parent
fa634ce526
commit
e65d205e0f
17 changed files with 1441 additions and 5 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
16
README.md
16
README.md
|
|
@ -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
91
cli.py
|
|
@ -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():
|
||||||
|
|
|
||||||
35
docs/PRICE-DIRECTIVE-SIMBIOT.md
Normal file
35
docs/PRICE-DIRECTIVE-SIMBIOT.md
Normal 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
71
docs/audit.md
Normal 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
48
docs/aum.md
Normal 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-отчёт в манифесте дня (дельта к прошлой неделе).
|
||||||
34
docs/aum/aum-pilot-2026-10-02.md
Normal file
34
docs/aum/aum-pilot-2026-10-02.md
Normal 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 не калиброван выручкой,
|
||||||
|
пилот отвечает только на вопрос «где направление улучшения», не «сколько именно».
|
||||||
11
docs/aum/template-balances.json
Normal file
11
docs/aum/template-balances.json
Normal 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": ""
|
||||||
|
}
|
||||||
31
docs/digests/9router-spend-2026-09-25_10-02.md
Normal file
31
docs/digests/9router-spend-2026-09-25_10-02.md
Normal 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.
|
||||||
91
docs/self-diagnostics.html
Normal file
91
docs/self-diagnostics.html
Normal 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>
|
||||||
140
docs/simbiot_presale_landing.html
Normal file
140
docs/simbiot_presale_landing.html
Normal 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 & 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
256
harness/audit.py
Normal 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
227
harness/aum.py
Normal 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}")
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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
139
tests/test_audit.py
Normal 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
132
tests/test_aum.py
Normal 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)
|
||||||
Loading…
Reference in a new issue