feat: DSG Orchestrator: Trust Gate and Feed Transport (Phases 1–2a) + Federation Boundary Verification #2

Merged
kiper220 merged 24 commits from kiper220/GuildWine:dsg-orchestrator into main 2026-08-08 11:50:11 +00:00
Owner

Что в PR

Три независимых блока (3 атомарных коммита над baseline):

1. Верификация collision-guard (server)

P0-гард ensure_remote_shadow_ids_free уже стоял в коде. Добавлено:

  • regression-тест — remote Welcome с коллизией UUID не перезаписывает локальный sentinel, не пишет inbox-receipt и повторно доставляем после очистки (tests/social.rs);
  • негативный тест authority-guard для пути federated-conversation (tests/federated_conversation_writes.rs);
  • доки — гард помечен как транзитный до миграции на composite/surrogate-ключи (docs/11, docs/12).

Production-логика не менялась (только rustfmt-реформат).

2. Federated call integration-тесты (server)

Two-home round-trip start/end звонка, idempotent duplicate, shadow-authority guard для calls-пути (tests/calls.rs). Путь уже был реализован и врезан — теперь покрыт тестами.

3. DSG-оркестратор: Фаза 1 + Фаза 2a (client-web)

Фаза 1 — Welcome trust-gate: DsgProjectionStore + DsgAcceptedCommitVerifier + DsgWelcomeRosterPolicy (оба порта реализованы, exact-match, fail-closed). Врезан в production credential-policy: гард реальный, store пуст до Фазы 2 → join остаётся fail-closed (MLS_WELCOME_ROSTER_MISMATCH вместо UNCONFIGURED).

Фаза 2a — feed-transport: crash-safe DSG envelope feed (device.sync.envelopes.pull/ack) — loop + page-parser + atomic IndexedDB opaque-inbox + realtime wake-ветка device_sync_events_available; врезан в репозиторий (durable-ингест opaque-конвертов).

Статус / что НЕ в PR

  • MLS-обработка DSG-конвертов, DeviceSyncPayload-кодек/snapshot-reducer, наполнение projection-store и candidate-join — Фаза 2b/2c.
  • Открытый design-вопрос (блокер join'а): в текущей реализации нет канала доставки initial DSG Welcome кандидату (chicken-and-egg: DsgWelcomeBootstrap — это DSG application, нечитаемое non-member'ом). Документированный DeviceAddOperation server-transition flow (docs/09:325) разошёлся с реализацией и нуждается в уточнении перед 2c.

Валидация

  • cargo test -p guildwine-server — social 7/7, federated_conversation_writes 5/5, calls 5/5; clippy -D warnings, fmt — чисто.
  • client-web — 177 тестов (вкл. 16 новых DSG) + build:types; apps/web tsc -b + 14 тестов — зелёные.
## Что в PR Три независимых блока (3 атомарных коммита над baseline): ### 1. Верификация collision-guard (server) P0-гард `ensure_remote_shadow_ids_free` уже стоял в коде. Добавлено: - **regression-тест** — remote Welcome с коллизией UUID не перезаписывает локальный sentinel, не пишет inbox-receipt и повторно доставляем после очистки (`tests/social.rs`); - **негативный тест authority-guard** для пути federated-conversation (`tests/federated_conversation_writes.rs`); - **доки** — гард помечен как транзитный до миграции на composite/surrogate-ключи (`docs/11`, `docs/12`). Production-логика не менялась (только rustfmt-реформат). ### 2. Federated call integration-тесты (server) Two-home round-trip start/end звонка, idempotent duplicate, shadow-authority guard для calls-пути (`tests/calls.rs`). Путь уже был реализован и врезан — теперь покрыт тестами. ### 3. DSG-оркестратор: Фаза 1 + Фаза 2a (client-web) **Фаза 1 — Welcome trust-gate:** `DsgProjectionStore` + `DsgAcceptedCommitVerifier` + `DsgWelcomeRosterPolicy` (оба порта реализованы, exact-match, fail-closed). Врезан в production credential-policy: гард реальный, store пуст до Фазы 2 → join остаётся fail-closed (`MLS_WELCOME_ROSTER_MISMATCH` вместо `UNCONFIGURED`). **Фаза 2a — feed-transport:** crash-safe DSG envelope feed (`device.sync.envelopes.pull/ack`) — loop + page-parser + atomic IndexedDB opaque-inbox + realtime wake-ветка `device_sync_events_available`; врезан в репозиторий (durable-ингест opaque-конвертов). ## Статус / что НЕ в PR - MLS-обработка DSG-конвертов, `DeviceSyncPayload`-кодек/snapshot-reducer, наполнение projection-store и candidate-join — **Фаза 2b/2c**. - **Открытый design-вопрос (блокер join'а):** в текущей реализации нет канала доставки initial DSG Welcome кандидату (chicken-and-egg: `DsgWelcomeBootstrap` — это DSG application, нечитаемое non-member'ом). Документированный `DeviceAddOperation` server-transition flow (docs/09:325) разошёлся с реализацией и нуждается в уточнении перед 2c. ## Валидация - `cargo test -p guildwine-server` — social 7/7, federated_conversation_writes 5/5, calls 5/5; `clippy -D warnings`, `fmt` — чисто. - client-web — 177 тестов (вкл. 16 новых DSG) + `build:types`; apps/web `tsc -b` + 14 тестов — зелёные.
The bare-UUID collision guard (ensure_remote_shadow_ids_free) was already implemented. This adds the import_remote_welcome collision regression test (no overwrite, no inbox receipt, redeliverable after cleanup), the conversation-authority-guard negative test, and documents the guard as transitional until composite/surrogate keys land. No production logic change (rustfmt reflow only).
Two-home federated start/end round-trip, idempotent duplicate delivery, and the shadow-authority guard test for the calls path. Tests-only; the federated call path was already implemented and wired.
Phase 1: DSG projection-store + accepted-Commit verifier + exact-roster policy (Welcome trust gate, fail-closed), wired into the production credential policy. Phase 2a: crash-safe DSG envelope feed transport (device.sync.envelopes pull/ack loop, page parser, atomic IndexedDB opaque inbox) + realtime wake branch, wired into the repository. MLS processing, the snapshot codec, projection population and the candidate join remain (Phase 2b/2c).
hermes left a comment

Ревью PR #2: DSG Orchestrator — Trust Gate + Feed Transport

Масштаб: 428 файлов, +131K строк. Основная масса — новый scaffolding монорепозитория (Rust server, client-web, Flutter, desktop-iced). Ключевой код — DSG Фаза 1+2a + тесты коллизий.

Безопасность — Solid

  • Fail-closed trust gate: пустой projection store → MLS_WELCOME_ROSTER_MISMATCH вместо обхода проверки. Нет лазейки через пустой callback.
  • Constant-time comparison (equalBytes) во всех сравнениях credential-байтов.
  • Welcome hash binding: SHA-256 welcome hash проверяется дважды — клиент-side при парсинге и server-side verifier при accepted-commit evidence.
  • DNS-free federation: TCP-адрес только из конфига, TLS SNI = domain (WebPKI сохранён), ALPN pinned на HTTP/1.1.
  • Non-forgeable provenance: WeakSet-маркировка — network JSON не может стать trust.

Архитектура — Good

  • Crash-safe DSG feed: cursor → ACK → pull → atomic IndexedDB apply+cursor → ACK. Replay-safe.
  • Transaction snapshot isolation (set_snapshot + get_for_update) — coherent reads внутри optimistic-transaction.
  • Outbox memory bounds: byte budget + count limit + eviction policy без потери reliable frames.
  • Self-revoke pattern: close_after_response доставляет финальный ответ и блокирует reconnect.

Замечания

# Файл Severity Описание
1 dsg-feed.ts:197 🟡 Should fix opaque_envelope не валидирует exact keys, в отличие от других вариантов
2 dsg-projection-store.ts:188 💡 Suggestion equalBytes дублируется в 3 файлах
3 event_feed.rs:81 💡 Suggestion Добавить migration note для legacy БД

Резюме

Качество кода очень высокое. Security-дизайн многоуровневый и fail-closed. Замечание #1 стоит поправить (консистентность валидации wire-формата), остальное — nits.

Вердикт: COMMENT — не блокирует. Исправление opaque_envelope валидации можно сделать в следующем PR.

## Ревью PR #2: DSG Orchestrator — Trust Gate + Feed Transport **Масштаб:** 428 файлов, +131K строк. Основная масса — новый scaffolding монорепозитория (Rust server, client-web, Flutter, desktop-iced). Ключевой код — DSG Фаза 1+2a + тесты коллизий. ### Безопасность — ✅ Solid - **Fail-closed trust gate**: пустой projection store → `MLS_WELCOME_ROSTER_MISMATCH` вместо обхода проверки. Нет лазейки через пустой callback. - **Constant-time comparison** (`equalBytes`) во всех сравнениях credential-байтов. - **Welcome hash binding**: SHA-256 welcome hash проверяется дважды — клиент-side при парсинге и server-side verifier при accepted-commit evidence. - **DNS-free federation**: TCP-адрес только из конфига, TLS SNI = domain (WebPKI сохранён), ALPN pinned на HTTP/1.1. - **Non-forgeable provenance**: `WeakSet`-маркировка — network JSON не может стать trust. ### Архитектура — ✅ Good - **Crash-safe DSG feed**: cursor → ACK → pull → atomic IndexedDB apply+cursor → ACK. Replay-safe. - **Transaction snapshot isolation** (`set_snapshot` + `get_for_update`) — coherent reads внутри optimistic-transaction. - **Outbox memory bounds**: byte budget + count limit + eviction policy без потери reliable frames. - **Self-revoke pattern**: `close_after_response` доставляет финальный ответ и блокирует reconnect. ### Замечания | # | Файл | Severity | Описание | |---|------|----------|----------| | 1 | `dsg-feed.ts:197` | 🟡 Should fix | `opaque_envelope` не валидирует exact keys, в отличие от других вариантов | | 2 | `dsg-projection-store.ts:188` | 💡 Suggestion | `equalBytes` дублируется в 3 файлах | | 3 | `event_feed.rs:81` | 💡 Suggestion | Добавить migration note для legacy БД | ### Резюме Качество кода очень высокое. Security-дизайн многоуровневый и fail-closed. Замечание #1 стоит поправить (консистентность валидации wire-формата), остальное — nits. **Вердикт: COMMENT** — не блокирует. Исправление opaque_envelope валидации можно сделать в следующем PR.
Collaborator

Good: Outbox byte budget (max_buffered_bytes) + count limit (cap) + eviction policy (evict_oldest_ephemeral никогда не выкидывает reliable frames) — solid защита от memory exhaustion на медленных соединениях. close_after_response для self-revoke — аккуратный паттерн.

✅ **Good**: Outbox byte budget (`max_buffered_bytes`) + count limit (`cap`) + eviction policy (`evict_oldest_ephemeral` никогда не выкидывает reliable frames) — solid защита от memory exhaustion на медленных соединениях. `close_after_response` для self-revoke — аккуратный паттерн.
kiper220 marked this conversation as resolved
Collaborator

Good: DNS-free transport через TcpStream::connect(SocketAddr) — адрес берётся только из конфига, не из запроса. Валидация endpoint (порт ≠ 0, не unspecified/multicast). TLS SNI использует FederationDomain, а не IP — WebPKI аутентификация сохраняется при IP-пиннинге.

✅ **Good**: DNS-free transport через `TcpStream::connect(SocketAddr)` — адрес берётся только из конфига, не из запроса. Валидация endpoint (порт ≠ 0, не unspecified/multicast). TLS SNI использует `FederationDomain`, а не IP — WebPKI аутентификация сохраняется при IP-пиннинге.
kiper220 marked this conversation as resolved
Collaborator

💡 Suggestion: scheduled_through с #[serde(default)] и нормализацией legacy-значений — хорошее backcompat-решение. Стоит добавить комментарий в migration docs, что старые БД до этого PR будут нормализовать 0 → acknowledged_through при чтении.

💡 **Suggestion**: `scheduled_through` с `#[serde(default)]` и нормализацией legacy-значений — хорошее backcompat-решение. Стоит добавить комментарий в migration docs, что старые БД до этого PR будут нормализовать `0 → acknowledged_through` при чтении.
kiper220 marked this conversation as resolved
Collaborator

Good: set_snapshot(true) + snapshot.get_cf для reads в сочетании с get_for_update_cf(true) для writes — даёт coherent view внутри транзакции. Комментарий точно описывает проблему: без snapshot, RocksDB может вернуть старое значение для первого ключа и новое для второго при конкурентном коммите.

✅ **Good**: `set_snapshot(true)` + `snapshot.get_cf` для reads в сочетании с `get_for_update_cf(true)` для writes — даёт coherent view внутри транзакции. Комментарий точно описывает проблему: без snapshot, RocksDB может вернуть старое значение для первого ключа и новое для второго при конкурентном коммите.
kiper220 marked this conversation as resolved
Collaborator

Good: Welcome hash проверяется через digestSha256(welcome) и сравнивается с декларированным welcome_hash. Сервер-side verifier (DsgAcceptedCommitVerifier) дополнительно проверяет evidence против projection store — two-layer защита от replay stale bootstrap material.

✅ **Good**: Welcome hash проверяется через `digestSha256(welcome)` и сравнивается с декларированным `welcome_hash`. Сервер-side verifier (`DsgAcceptedCommitVerifier`) дополнительно проверяет evidence против projection store — two-layer защита от replay stale bootstrap material.
kiper220 marked this conversation as resolved
Collaborator

🟡 Should fix: Кейс opaque_envelope не вызывает hasExactKeys, в отличие от bootstrap_requested (стр. 209) и device_revoked (стр. 220). Лишние поля в opaque-конверте будут молча проигнорированы вместо отклонения.

Так как request_id опционален, можно проверить оба варианта:

const hasRequestId = Object.prototype.hasOwnProperty.call(value, "request_id");
const expectedKeys = hasRequestId
  ? ["kind", "envelope_id", "sender_device_id", "request_id", "expires_at", "ciphertext"]
  : ["kind", "envelope_id", "sender_device_id", "expires_at", "ciphertext"];
if (!hasExactKeys(value, expectedKeys)) {
  throw new DsgFeedError("DSG_FEED_PAGE_INVALID", `DSG feed event ${index} opaque_envelope has an invalid shape`);
}

Это поможет отловить протокольные изменения на ранней стадии.

🟡 **Should fix**: Кейс `opaque_envelope` не вызывает `hasExactKeys`, в отличие от `bootstrap_requested` (стр. 209) и `device_revoked` (стр. 220). Лишние поля в opaque-конверте будут молча проигнорированы вместо отклонения. Так как `request_id` опционален, можно проверить оба варианта: ```typescript const hasRequestId = Object.prototype.hasOwnProperty.call(value, "request_id"); const expectedKeys = hasRequestId ? ["kind", "envelope_id", "sender_device_id", "request_id", "expires_at", "ciphertext"] : ["kind", "envelope_id", "sender_device_id", "expires_at", "ciphertext"]; if (!hasExactKeys(value, expectedKeys)) { throw new DsgFeedError("DSG_FEED_PAGE_INVALID", `DSG feed event ${index} opaque_envelope has an invalid shape`); } ``` Это поможет отловить протокольные изменения на ранней стадии.
kiper220 marked this conversation as resolved
Collaborator

Good: Fail-closed дизайн безупречный — пустой store → getRoster возвращает undefinedsameCredentialSet не вызывается → authorizeExactRoster возвращает false → Welcome join падает с MLS_WELCOME_ROSTER_MISMATCH. Нет лазейки через пустой callback.

✅ **Good**: Fail-closed дизайн безупречный — пустой store → `getRoster` возвращает `undefined` → `sameCredentialSet` не вызывается → `authorizeExactRoster` возвращает `false` → Welcome join падает с `MLS_WELCOME_ROSTER_MISMATCH`. Нет лазейки через пустой callback.
Collaborator

💡 Suggestion: equalBytes дублируется в dsg-projection-store.ts, mls-credential-policy.ts и dsg-bootstrap.ts — три идентичные constant-time реализации. Можно вынести в общий utility-модуль (например crypto-utils.ts), чтобы избежать расхождения при рефакторинге.

Не блокирующее, но при росте кодовой базы три копии одного крипто-примитива — потенциальный источник ошибок.

💡 **Suggestion**: `equalBytes` дублируется в `dsg-projection-store.ts`, `mls-credential-policy.ts` и `dsg-bootstrap.ts` — три идентичные constant-time реализации. Можно вынести в общий utility-модуль (например `crypto-utils.ts`), чтобы избежать расхождения при рефакторинге. Не блокирующее, но при росте кодовой базы три копии одного крипто-примитива — потенциальный источник ошибок.
kiper220 marked this conversation as resolved
Collaborator

Good: Последовательная цепочка проверок Welcome provenance: isAuthenticatedDsgWelcomeProvenance → проверка conversation/groupId/epoch/sponsorcandidateDeviceId === ownDeviceIdroster.authorizeExactRoster. Каждое звено fail-closed. Использование WeakSet для non-forgeable provenance — элегантное runtime-ограничение.

✅ **Good**: Последовательная цепочка проверок Welcome provenance: `isAuthenticatedDsgWelcomeProvenance` → проверка `conversation/groupId/epoch/sponsor` → `candidateDeviceId === ownDeviceId` → `roster.authorizeExactRoster`. Каждое звено fail-closed. Использование `WeakSet` для non-forgeable provenance — элегантное runtime-ограничение.
kiper220 marked this conversation as resolved
Collaborator

Ревью PR #2 — детальные замечания

(Inline-комментарии не прикрепились через API — публикую здесь)


🟡 Should fix

1. packages/client-web/src/dsg-feed.ts:197 — кейс opaque_envelope не вызывает hasExactKeys

В отличие от bootstrap_requested (стр. 209) и device_revoked (стр. 220), которые валидируют точный набор ключей, opaque_envelope молча игнорирует лишние поля. Это нарушает консистентность wire-валидации и может пропустить протокольные изменения:

// Добавить перед парсингом:
const hasRequestId = Object.prototype.hasOwnProperty.call(value, "request_id");
const expectedKeys = hasRequestId
  ? ["kind", "envelope_id", "sender_device_id", "request_id", "expires_at", "ciphertext"]
  : ["kind", "envelope_id", "sender_device_id", "expires_at", "ciphertext"];
if (!hasExactKeys(value, expectedKeys)) {
  throw new DsgFeedError("DSG_FEED_PAGE_INVALID", `DSG feed event ${index} opaque_envelope has an invalid shape`);
}

💡 Suggestions

2. packages/client-web/src/dsg-projection-store.ts:188equalBytes дублируется

Constant-time equalBytes идентично реализован в трёх файлах: dsg-projection-store.ts, mls-credential-policy.ts, dsg-bootstrap.ts. Стоит вынести в общий crypto-utils.ts — три копии крипто-примитива = риск расхождения при рефакторинге.

3. crates/server/src/infrastructure/persistence/event_feed.rs:81scheduled_through migration note

#[serde(default)] с нормализацией 0 → acknowledged_through — хорошее backcompat-решение. Стоит задокументировать в migration docs, что старые БД до этого PR будут нормализовать поле при первом чтении.


Что хорошо

  • Fail-closed trust gate (dsg-projection-store.ts:165): пустой store → MLS_WELCOME_ROSTER_MISMATCH вместо bypass. Нет лазейки через пустой callback.
  • Welcome provenance chain (mls-credential-policy.ts:150-210): WeakSet-маркировка → каждый уровень проверяется → все fail-closed.
  • Welcome hash binding (dsg-bootstrap.ts:115): SHA-256 проверяется дважды — при парсинге и через server-side verifier.
  • Transaction snapshot isolation (txn.rs:53): set_snapshot(true) + get_for_update для coherent reads.
  • DNS-free federation transport (transport.rs:60): TCP-адрес только из конфига, TLS SNI = domain, ALPN pinned.
  • Outbox memory bounds (registry.rs:55): byte budget + eviction policy без потери reliable frames.
  • Self-revoke pattern (registry.rs:revoke_device): close_after_response доставляет финальный ответ и блокирует reconnect.

Резюме

Качество кода очень высокое. Security-дизайн многоуровневый и fail-closed. Единственное, что стоит поправить — консистентность hasExactKeys в opaque_envelope. Остальное — nits на будущее.

## Ревью PR #2 — детальные замечания *(Inline-комментарии не прикрепились через API — публикую здесь)* --- ### 🟡 Should fix **1. `packages/client-web/src/dsg-feed.ts:197`** — кейс `opaque_envelope` не вызывает `hasExactKeys` В отличие от `bootstrap_requested` (стр. 209) и `device_revoked` (стр. 220), которые валидируют точный набор ключей, `opaque_envelope` молча игнорирует лишние поля. Это нарушает консистентность wire-валидации и может пропустить протокольные изменения: ```typescript // Добавить перед парсингом: const hasRequestId = Object.prototype.hasOwnProperty.call(value, "request_id"); const expectedKeys = hasRequestId ? ["kind", "envelope_id", "sender_device_id", "request_id", "expires_at", "ciphertext"] : ["kind", "envelope_id", "sender_device_id", "expires_at", "ciphertext"]; if (!hasExactKeys(value, expectedKeys)) { throw new DsgFeedError("DSG_FEED_PAGE_INVALID", `DSG feed event ${index} opaque_envelope has an invalid shape`); } ``` --- ### 💡 Suggestions **2. `packages/client-web/src/dsg-projection-store.ts:188`** — `equalBytes` дублируется Constant-time `equalBytes` идентично реализован в трёх файлах: `dsg-projection-store.ts`, `mls-credential-policy.ts`, `dsg-bootstrap.ts`. Стоит вынести в общий `crypto-utils.ts` — три копии крипто-примитива = риск расхождения при рефакторинге. **3. `crates/server/src/infrastructure/persistence/event_feed.rs:81`** — `scheduled_through` migration note `#[serde(default)]` с нормализацией `0 → acknowledged_through` — хорошее backcompat-решение. Стоит задокументировать в migration docs, что старые БД до этого PR будут нормализовать поле при первом чтении. --- ### ✅ Что хорошо - **Fail-closed trust gate** (`dsg-projection-store.ts:165`): пустой store → `MLS_WELCOME_ROSTER_MISMATCH` вместо bypass. Нет лазейки через пустой callback. - **Welcome provenance chain** (`mls-credential-policy.ts:150-210`): `WeakSet`-маркировка → каждый уровень проверяется → все fail-closed. - **Welcome hash binding** (`dsg-bootstrap.ts:115`): SHA-256 проверяется дважды — при парсинге и через server-side verifier. - **Transaction snapshot isolation** (`txn.rs:53`): `set_snapshot(true)` + `get_for_update` для coherent reads. - **DNS-free federation transport** (`transport.rs:60`): TCP-адрес только из конфига, TLS SNI = domain, ALPN pinned. - **Outbox memory bounds** (`registry.rs:55`): byte budget + eviction policy без потери reliable frames. - **Self-revoke pattern** (`registry.rs:revoke_device`): `close_after_response` доставляет финальный ответ и блокирует reconnect. --- ### Резюме Качество кода очень высокое. Security-дизайн многоуровневый и fail-closed. Единственное, что стоит поправить — консистентность `hasExactKeys` в opaque_envelope. Остальное — nits на будущее.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
WetGrape/GuildWine!2
No description provided.