BOS-X — Architecture
A lean, self-owned stack that turns a chat message into a live web app: a buildless Surface talks to one Node Runner; the runner resolves an Engine to build the app and isolates untrusted execution in a managed Sandbox, routes every model call through a key-injection proxy so the sandbox never sees a provider key (Invariant 6), persists builds durably, and serves the live preview. State lives in an isolated Supabase.
The system at a glance
SURFACE (buildless SPA) x.dev.bos.pro · Cloudflare Pages, also served by the runner
built-apps rail │ chat + SSE transcript │ live preview iframe (sandboxed, no same-origin)
│
│ POST /api/run (SSE) + GET /api/* (apps · messages · models · health)
▼
RUNNER (one Node/TS service) x-runner.bos.pro · durable CF tunnel (laptop + Colima; VPS = BLK-12)
server.ts → runTurn → engine registry → oma · aider · router-direct
│ fallback chain: oma → aider → router-direct (router-direct actually runs in deploy)
│
├─ KEY-INJECTION PROXY (Invariant 6): injects the real router key — sandbox gets only a proxy URL + token, never a key
│
└─ durable builds: runner/built-apps/{app_id}/ → GET /apps/{app_id}/* (survives sandbox reaping)
│ │ │
▼ ▼ ▼
SUPABASE (isolated) MANAGED SANDBOX MODEL ROUTER (read-only)
gsuafvvzsdfkfybgfzjv Daytona (wired default) rundev.cloved.ai/v1
apps · messages · build → preview bos:* aliases · provider failover
model_runs · engine_sessions egress allowlist + proxy no /v1/models · no streaming
RLS on · runner-only writes URL only (Invariant 6) (proxy de-streams)
Components
Surface
surface/Buildless HTML/CSS/JS SPA: chat + SSE transcript + a sandboxed preview iframe (no allow-same-origin). Served by the runner and as a Cloudflare Pages copy at x.dev.bos.pro; its config.js pins BOSX_API_BASE → x-runner.bos.pro.
Runner
runner/src/server.tsThe one Node/TS service: every /api/* route, SSE framing, sandbox provisioning, durable build persistence, static serving.
Engine seam
runner/src/engines/*IAgentEngine registry: oma (opencode, gated, default), aider (LiteLLM, git-diff), router-direct (one call + WRITE_FILE — runs in the live deploy). Fallback oma→aider→router-direct.
Key-injection proxy
runner/src/router/keyInjectionProxy.tsInvariant 6. Strips inbound auth, injects the real router key, and de-streams (the router rejects stream:true). The sandbox only ever gets the proxy URL + a non-secret token.
Sandbox adapter
runner/src/sandbox/*One branded SandboxedSpec; Daytona is the wired default, E2B an alternate adapter. The managed engines (oma/aider) run code only here; router-direct — the live fallback — writes its generated files in-process on the trusted runner, which then serves the durable preview at /apps/{app_id}/.
Model router
rundev.cloved.ai/v1Read-only egress; bos:* aliases over provider failover. No /v1/models, no streaming (the proxy de-streams).
Supabase (isolated)
gsuafvvzsdfkfybgfzjvapps · messages · model_runs · engine_sessions (the last is a stub). RLS enabled (forced on engine_sessions); the runner writes with the service-role key (host-checked, fail-closed).
Factory (offline)
factory/Not in the request path: prebro_orchestrator.py (DAG build-waves) + proposal_debate.py / playwright-verify/goal_judge_debate.py (multi-model judge panels via callmodel.py).
Request flow — chat → live app
- The Surface POSTs
{prompt, model, engine_ref?, session_id, app_id?}to/api/runand opens an SSE stream. - The runner mints
run-<id>, validates the model against a server-side allowlist, mints/reusesapp-<id>, and emitsmeta{app_id}. - It provisions a managed sandbox via
sandboxedSpec()— which asserts Invariant 6: the sandbox env carries the proxy URL + a token, never a provider key. - It persists the user turn to
messages, loads any follow-up history + existing files, then runs the engine (fallback oma→aider→router-direct). - The engine calls the model through the router and applies its file edits (router-direct parses
WRITE_FILEblocks; aider derives them fromgit diff); every step streams to the Surface asengine_event. - On success the files are persisted durably to
runner/built-apps/{app_id}/and the runner emitspreview{url = /apps/{app_id}/}— a runner-served URL that survives the sandbox being torn down. - The build is recorded in
apps, the assistant turn inmessages, and every attempt (success/fail) inmodel_runs;done{status}closes the stream. - The Surface loads the preview URL into the sandboxed iframe (no
allow-same-origin— generated apps are untrusted).
Invariants & trust boundaries
- Invariant 6 — one egress authority. The runner holds the real model-router key; the sandbox never does. The key-injection proxy is the only hop that adds it.
- One sandbox adapter. Managed engines (oma/aider) run code only in the sandbox and are fail-closed by default (the host-engine co-location guard) until exec is co-located into the sandbox; the live fallback (router-direct) makes one model call and writes the generated files on the trusted runner, which serves the durable preview at
/apps/{app_id}/. - Engine ⊥ model ⊥ memory. The engine is a registry plugin, the model is the router's job, and cross-engine continuity is designed as a small summary-handoff (the store is still a stub).
- Isolated state. Only the Supabase project
gsuafvvzsdfkfybgfzjv; RLS is enabled (forced onengine_sessions) and every write is runner-only (service-role, host-checked). - Untrusted preview. The preview iframe deliberately omits
allow-same-originso a generated app can't read the parent/runner origin.
Status & honest gaps
- The engine that actually runs in the live deploy is router-direct; oma/aider are registered but fail-closed by default (host-engine co-location guard) until sandbox exec co-location ships.
- The runner is hosted on a laptop (Colima + the durable Cloudflare tunnel
x-runner.bos.pro); a dedicated Prebro VPS is still pending (BLK-12). engine_sessionsexists in the schema but its store is a stub — cross-engine continuity is not yet wired.- There is no end-user auth yet: anon read is intentional; all writes are runner-only.
- Canonical spec =
memory/ARCHITECTURE.md(v2.0, “lean self-owned stack”), not the superseded root01_ARCHITECTURE.md(v1.0).
Сравнение архитектур (RU)
bos-x — намеренно «тонкий» (lean) стек, который уже работает в проде. Ниже — сравнение с двумя эталонными архитектурами того же класса («чат → приложение в песочнице»): исследовательской modular-agentic-system и SaaS-рантаймом vbp-german runner-v2. Для каждой — схема-различие и таблица дельт.
bos-x ↔ modular-agentic-system
modular-agentic-system — это в основном проектная документация плюс три прототипа. Её суть —
ядро с двумя реестрами: политика (приём → сессия → оркестратор → расчёт) живёт в крошечном ядре,
которое никогда не видит подложку, а две независимые оси — харнесс (петля агента) и
окружение (песочница) — подключаются по ref-строке через реестры. Ключевая дисциплина —
непрозрачный EnvironmentHandle и CI-grep-гейт, который роняет сборку, если в ядро
просочилось слово про подложку (container_id, dockerode, workspace_dir).
Песочницы договариваются о возможностях (capability negotiation), а самый сложный момент — публикация порта
превью — спрятан за единым контрактом exposePort(port)→url.
bos-x устроен проще: одна ось-реестр (движки oma/aider/router-direct), а песочница —
единственный адаптер (Daytona подключён; E2B-адаптер есть, но не задействован), а не вторая ось-реестр. Границу гарантируют
именованный (branded) тип SandboxedSpec и Инвариант 6 (прокси-инъекция ключа), а не CI-grep-гейт.
Итог: bos-x — тоньше и уже работает в проде; modular-agentic-system — модульнее и строже к развязке слоёв, но почти не
построена (реально запускается лишь sdk × docker).
bos-x (в проде)
─────────────────────────────
Surface SPA (buildless)
│
Runner (один Node-сервис)
├ engine registry (1 ось):
│ oma · aider · router-direct
├ key-injection proxy (Инв.6)
└ sandbox adapter
Daytona — один адаптер (E2B не подключён),
НЕ реестр
Supabase: apps·messages·model_runs·engine_sessions
превью: durable /apps/{id}/ на runner
modular-agentic-system (эталон/прототип)
─────────────────────────────
Studio UI (chat + превью)
│
Core-ядро (ТОЛЬКО политика)
├ registry ХАРНЕССОВ (ось 1)
│ sdk · opencode · …
└ registry ОКРУЖЕНИЙ (ось 2)
docker · e2b · vercel …
EnvironmentHandle (непрозрачный)
exposePort(port)→url
CI-grep-гейт против утечки подложки
запускается лишь sdk × docker
| Аспект | bos-x | modular-agentic-system | Δ дельта |
|---|---|---|---|
| Оси подключения | 1 (движки) | 2 (харнесс + окружение) | MAS модульнее |
| Песочница | один адаптер (Daytona; E2B не подключён) | реестр окружений по ref | MAS гибче, bos-x проще |
| Развязка слоёв | SandboxedSpec + Инв.6 | непрозрачный handle + CI-grep-гейт | у MAS жёстче механически |
| Зрелость | в проде | документация + 3 прототипа | bos-x реальнее |
| Секрет ключа | прокси-инъекция (Инв.6): песочница ключа не видит | секрет инжектится в окружение (ProvisionSpec.env) | у bos-x изоляция строже |
bos-x ↔ vbp-german runner-v2
runner-v2 — полноценный многопользовательский SaaS-рантайм (Express 5, ~28 групп
маршрутов). Песочница — облачная Daytona (одна на сессию, hibernate/wake). Состояние сессии
двухуровневое: горячее в Redis + долговечное в Supabase с восстановлением после краша.
Есть writer-lock (один писатель на проект), 4-канальная сборка контекста (ContextFrame) с
аудитом и бюджетом токенов, цепочка BYOK (ключ пользователя → организации → платформы), именованные
профили агентов и под-агенты с ограничением глубины (≤4), биллинг / права доступа / маркетплейс и «глубокая» проверка работоспособности.
Провайдерский ключ резолвится на хосте и в песочницу не попадает — та же дисциплина, что у bos-x.
bos-x намеренно тоньше: один сервис, тонкий surface, слой движков (3 движка), прокси-инъекция
ключа (Инв.6) как первоклассная граница, Daytona (E2B-адаптер не подключён) и Supabase с 4 таблицами. Чего у bos-x пока нет: облачного
хостинга рантайма (сейчас ноутбук + Colima, VPS — BLK-12), восстановления сессий после краша (состояние
не реализовано, stub), формального writer-lock, аудируемой сборки контекста, глубокой проверки работоспособности, биллинга. Итог: runner-v2 —
операционно куда зрелее, но тяжелее (часть абстракций — заглушки: failover, контроль model-health, MCP — но deep-health реальна); bos-x — легче, честнее про
«построено / заглушка» и с более чистой границей ключа.
bos-x (lean, в проде)
─────────────────────────────
один Node-сервис, /api/*
движки: oma · aider · router-direct
key-injection proxy (Инв.6)
sandbox: Daytona (E2B не подключён)
сессии: не реализовано (engine_sessions=stub)
единый писатель: не формализовано
контекст: системный промпт
health: /api/health
хост: ноутбук + Colima (VPS=BLK-12)
биллинга нет
runner-v2 (SaaS-рантайм)
─────────────────────────────
Express 5, ~28 групп маршрутов
профили агентов + под-агенты (≤4)
ключ на хосте (в песочницу не идёт)
sandbox: Daytona облако, 1/сессия, hibernate
сессии: Redis(горячо)+Supabase(durable)+recovery
writer-lock (Redis, NX, TTL)
ContextFrame: 4 канала, аудит, бюджет токенов
health: deep (пингует Redis), BYOK-проверки
хост: облако
биллинг / права доступа / маркетплейс
| Аспект | bos-x | runner-v2 | Δ дельта |
|---|---|---|---|
| Хост рантайма | ноутбук + Colima (VPS=BLK-12) | облако | runner-v2 устойчивее |
| Состояние сессии | не реализовано (stub) | Redis + Supabase + recovery | runner-v2 переживает сбой |
| Один-писатель | не формализовано | writer-lock (Redis NX TTL) | заимствовать |
| Сборка контекста | системный промпт | ContextFrame: 4 канала + аудит | заимствовать |
| Граница ключа | прокси-инъекция (Инв.6) | резолв на хосте (в песочницу не идёт) | оба изолируют ключ — разные механизмы |
| Объём | один сервис, тонкий | SaaS-плоскость (часть — заглушки) | bos-x легче |
Что заимствовать · где bos-x лучше / хуже
Заимствовать в bos-x (по приоритету)
Из runner-v2 (операционная зрелость) — то, что напрямую закрывает слабые места bos-x:
- Облачный хостинг рантайма + жизненный цикл песочницы на уровне сессии (одна Daytona-песочница на сессию, hibernate/wake, очистка по простою) — bos-x уже использует Daytona для песочниц, но сам рантайм живёт на ноутбуке (Colima); вынос рантайма в облако + посессионный жизненный цикл убирают зависимость от хрупкого ноутбука-хоста и закрывают
BLK-12. - Двухуровневое состояние сессии: Redis (горячо) + Supabase (durable) + восстановление после краша — сохраняется после перезапуска рантайма.
- writer-lock на проект (Redis, NX, TTL) — один писатель, без гонок (ложится на модель «строка
apps= тред»). - 4-канальный ContextFrame с аудитом и бюджетом токенов — инспектируемая и воспроизводимая сборка контекста.
- «Глубокая» проверка работоспособности, пингующая зависимости — честный сигнал вместо поверхностного
/api/health. - Цепочка BYOK (ключ пользователя → организации → платформы), резолв на хосте — расширяет границу ключа bos-x.
Из modular-agentic-system (модульность и строгость развязки):
- CI-grep-гейт против утечки подложки в ядро — механически защищает Инвариант 6 и границу песочницы.
- Флаги возможностей песочницы + ступенчатая деградация (publicPorts/snapshot/nativeGit) — одно ядро ведёт Daytona и E2B с мягкой деградацией.
- Контракт
exposePort(port)→url, прокси спрятан в адаптере — сохраняет Daytona и E2B взаимозаменяемыми. - Именованный контракт кросс-движковой непрерывности (производная идея, а не функция MAS) — решить, что переносится при смене
engine_refи в fallback-цепочке.
Где bos-x лучше
- Тоньше и уже работает в проде — один связный сервис, без SaaS-разрастания (~28 групп маршрутов у runner-v2).
- Единый egress-чокпойнт (Инвариант 6): весь модельный трафик идёт через один проксируемый шлюз — его проще аудировать.
- Честность про «построено / заглушка» прямо на странице архитектуры — встроенный блок Status с открытым перечнем недоделок.
Где bos-x хуже
- Хост — ноутбук + Colima против облака (VPS —
BLK-12); облачный рантайм был бы устойчивее. - Сессии не реализованы (stub), без восстановления после сбоя (у runner-v2 — Redis+Supabase+recovery).
- Нет формального writer-lock, аудируемой сборки контекста, глубокой проверки работоспособности и биллинга.
Текущее vs Идеальное + резюме (RU)
Где bos-x сейчас и какой должна быть «идеальная» модульная
архитектура — сменный движок без потери контекста + доступ по ролям к сессиям, данным организации и
памяти. Полное руководство для агентов: docs/TARGET_ARCHITECTURE.md.
Легенда: ✅ есть · 🟡 частично/на ветке · ⬜ цель.
Сейчас (как построено) ↔ Идеал (как должно быть, «perfect»)
| Аспект | Сейчас — bos-x | Идеал — perfect |
|---|---|---|
| Движок | 🟡 реестр + 3 движка (oma/aider/router-direct), fallback; в проде реально работает router-direct | ⬜ сменный движок за нормализованным контрактом EngineEvent; контракт load/resume |
| Контекст при смене движка | 🟡 lossy summary-handoff (DEC-004), не подключён — контекст теряется | ⬜ событийный журнал (event-sourced, T1) = единый источник истины; замена без потерь (на границе хода) |
| Исполнение кода | 🟡 router-direct пишет файлы на хосте; oma/aider fail-closed | ⬜ всё в песочнице (mode-1 exec-в-песочницу / mode-2); host-exec запрещён |
| Песочница | 🟡 один адаптер Daytona (E2B есть, не подключён) | ⬜ реестр окружений по ref; opaque handle; exposePort()→url |
| Хост рантайма | 🟡 ноутбук + Colima (BLK-12) | ⬜ облако (Daytona cloud, hibernate/wake) |
| Состояние сессий | 🟡 не реализовано (stub), без восстановления | ⬜ Redis-hot + Supabase-durable + recovery после краша |
| Доступ (RBAC) | 🟡 нет (single-tenant); готов на ветке | ⬜ scopedDb reference-monitor; capability-vector SSOT; effective = ∩; fail-closed |
| Кросс-сессии / организация | 🟡 не разграничены | ⬜ session.read_own/workspace/org; supervisor — по workspace |
| Память (memory) | ⬜ не разделена | ⬜ scopedMemory: namespace по identity, 3 уровня, фильтр на retrieval |
| Ключ / секреты | ✅ прокси-инъекция (Инв.6) — песочница ключа не видит | ✅+⬜ то же + OBO-делегирование (sub/act.sub), секреты файлом / из vault |
| Health / failover | 🟡 /api/health поверхностный; 1 маршрут модели | ⬜ deep-health (пингует зависимости); провайдер-failover |
Резюме (RU): diff · дилеммы · исследования · тесты
① Главный diff — что менять (по приоритету)
- Облако вместо ноутбука (Daytona cloud) → убирает падения
x-runner(BLK-12). - Событийный журнал (event-sourced) вместо summary → смена движка / краш без потери контекста.
- Подключить RBAC (
scopedDb) + RLS → доступ по ролям к сессиям / орг / памяти. - Память как разграниченный ресурс (
scopedMemory, namespace по identity). - Реестр окружений + CI grep-гейт → настоящая двух-осевая модульность.
② Сложные выборы / дилеммы (D1–D13)
- Непрерывность: summary (дёшево, с потерями) vs событийный журнал (без потерь, дороже) → журнал.
- Протокол движка: ACP (pre-1.0) vs in-proc SDK vs HTTP-SSE → нормализовать в
EngineEvent, ACP на краю. - Доступ: capability-vector (RBAC) vs ReBAC-граф → вектор сейчас, ReBAC при шеринге сессий / иерархии.
- Enforcement:
scopedDbvs RLS → оба (scopedDb — шлюз; RLS — бэкап для не-service-role путей). - Режим движка: исполнение всегда в песочнице, не на хосте (mode-1/2).
- Память: per-user / workspace / org — по умолчанию узко.
③ Области для исследования
- Доказать «без потерь» при свапе движка (тесты §2.3) — пока не реализовано.
- Зрелость ACP
session/resume(в проде есть баги → леджер как бэкап). - Порог перехода на ReBAC (когда одного вектора возможностей уже мало).
- Tiering и анти-bleed памяти (namespace по identity, фильтр на retrieval).
- Событие
capability_degradedпри fallback (смена поведения под одним run-id). - Резолвер BYOK user→org→platform (подтверждён частично).
④ Как проверить (тесты)
- Свап движка в середине сессии → новый движок видит прошлые ходы + файлы + решения.
- Краш-resume: убить рантайм → сессия восстанавливается из durable.
- manager не читает чужую сессию → 403 + 0 строк (нет IDOR).
- viewer не вызывает агента → 403 + composer выключен.
- Кросс-workspace чтение → пусто (не чужие строки).
- Запрос памяти из workspace A → только A; namespace B → пусто.
- Обход
scopedDb(from('sessions')) → CI grep-линт роняет сборку.