docs: establish canonical documentation architecture (#1210)
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #1210.
This commit is contained in:
2026-08-13 17:56:13 +00:00
parent f82307c4dc
commit 7a6fb024b4
241 changed files with 4722 additions and 1579 deletions
+50
View File
@@ -0,0 +1,50 @@
# Developer Guide
> **Status:** Partially migrated. Architecture, lease-broker verification, and channel-adapter authoring pages are current; other contributor chapters remain unmigrated.
This book is the canonical home for architecture, package and application guides, local development, testing, contribution workflow, and integration authoring. User-facing procedures belong in [`USER-GUIDE/`](../USER-GUIDE/); operator procedures belong in [`ADMIN-GUIDE/`](../ADMIN-GUIDE/); API contracts belong in [`API/`](../API/).
## Start here
- [Documentation atlas](../README.md) — placement rules and source-of-truth boundaries.
- [Architecture index](architecture/README.md) — current architecture chapter scaffold.
- [Documentation audit](../reports/documentation/2026-08-10-docs-catalog-audit.md) — evidence-based migration inventory.
- [Product requirements](../PRD.md) — normative requirements, currently marked draft.
## Chapter map
| Chapter | Scope | Status |
| ----------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------- |
| [`architecture/`](architecture/README.md) | System model, components, data flow, security model, ADRs, and RFCs. | Partially migrated. |
| `packages/` | Package- and application-level contracts and guides. | Scaffold only. |
| `local-development/` | Safe local setup and development routes. | Scaffold only. |
| `testing/` | Test strategy, verification, and quality gates. | Lease-broker verification boundary is current. |
| `contributing/` | Contribution, review, and delivery workflow. | Scaffold only. |
| `integrations/` | Plugin, provider, and adapter authoring. | Channel-adapter authoring boundary is current. |
### Current contributor pages
- [Lease-broker operations and verification](testing/lease-broker-operations.md) — safe static/test commands plus explicitly held live operations.
- [Channel adapters](integrations/channel-adapters.md) — current shared contracts and Discord reference boundary; future adapter parity is draft.
Every promoted page must be added to this index and to [`SITEMAP.md`](../SITEMAP.md) in the same migration slice.
## Migration backlog — not current developer guidance
These are source candidates or stale records, not verified current instructions:
- [`archived TUI PRD`](../archive/tui/PRD-TUI_Improvements.md) — contradicted/stale; it references a missing `packages/cli`, while current TUI code is under `packages/mosaic`.
- [`archived TUI task ledger`](../archive/tui/TASKS-TUI_Improvements.md) — historical task ledger; its status and worktree claims require revalidation.
- `_old_structure/guides/dev-guide.md` — quarantined historical source; verify paths and commands before promotion. See the [documentation catalog](../reports/documentation/2026-08-10-docs-catalog-audit.md) for its disposition.
Do not make a legacy or archived page current by linking it from a chapter as if it were already promoted.
## Authoring boundary
New developer documentation belongs under one of the chapter directories above. Architecture decisions and RFCs must identify their status and authority; executable behavior must be checked against current code and tests.
## Related
- [[README|Documentation contract]]
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
- [[API/README|API index]]
@@ -0,0 +1,50 @@
# Architecture
> **Status:** Partially migrated. The lease-broker security-contract pages below are current references; the remaining architecture pages are still being classified.
This chapter is the canonical home for Mosaic Stack's system model, component boundaries, data and control flow, security model, architecture decisions, and RFCs. It explains why the system has its shape; it does not replace [`PRD.md`](../../PRD.md), [`TASKS.md`](../../TASKS.md), or the API contract.
## Promoted pages
- [`lease-broker-protocol.md`](lease-broker-protocol.md) — authenticated Unix-socket protocol, identity binding, framing, persistence, and lease transitions.
- [`lease-broker-security.md`](lease-broker-security.md) — identity, ancestry, filesystem, whole-class, observer, and named residual security boundaries.
- [`mutator-class-gate.md`](mutator-class-gate.md) — default-deny tool authorization, runtime adapters, launch choke point, and parser assurance boundary.
- [`compaction-revocation.md`](compaction-revocation.md) — Claude/Pi observer lifecycle, runtime generations, revocation, and the bounded residual stale window.
- [`channel-protocol.md`](channel-protocol.md) — current shared channel DTOs and Discord compatibility baseline, with unimplemented adapter work explicitly marked draft.
- [`decisions/mos-runtime-portability-m1.md`](decisions/mos-runtime-portability-m1.md) — current logical identity, connector lease, grant, audit, and fencing decision; connector activation remains held.
These pages are current security-contract references and are consumed by the lease-broker acceptance suites. Their live deployment gaps remain explicitly labeled in the pages; this migration does not change runtime behavior.
## Planned pages
| Path | Purpose | Status |
| ----------------------------------- | --------------------------------------------------------------------- | ------------------------- |
| `system-overview.md` | Platform boundary and major request, event, and agent-runtime flows. | Planned. |
| `component-map.md` | Apps, packages, plugins, and dependency ownership. | Planned. |
| `data-flow.md` | Data, event, and control-plane movement. | Planned. |
| `security-model.md` | Trust boundaries, authority, authentication, and authorization model. | Planned. |
| [`decisions/`](decisions/README.md) | Approved architecture decision records. | Partially migrated. |
| [`rfcs/`](rfcs/README.md) | Proposals and protocol RFCs. | Draft egress RFC indexed. |
### Draft RFCs
- [`rfcs/optional-ai-egress-gateways.md`](rfcs/optional-ai-egress-gateways.md) — proposed model-egress boundary; not approved or integrated.
Promoted pages must be linked here, from [`DEVELOPER-GUIDE/README.md`](../README.md), and from [`SITEMAP.md`](../../SITEMAP.md). Do not create duplicate architecture pages in `docs/mosaic-stack/` or the docs root.
## Migration backlog — not current architecture
- [`docs/README.md`](../../README.md) — current documentation contract and placement rules.
- [`Documentation information architecture design`](../../plans/2026-08-10-docs-information-architecture-design.md) — approved documentation structure decision, not product architecture.
- [`Documentation catalog audit`](../../reports/documentation/2026-08-10-docs-catalog-audit.md) — evidence and migration recommendations, not normative architecture.
## Source-of-truth boundary
Architecture pages explain approved design and current system boundaries. Requirements remain in [`PRD.md`](../../PRD.md); active work remains in [`TASKS.md`](../../TASKS.md); executable behavior remains authoritative in source and tests. Draft proposals belong in `rfcs/` or [`docs/plans/`](../../plans/), with status clearly labeled.
## Related
- [[README|Documentation contract]]
- [[DEVELOPER-GUIDE/README|Developer guide]]
- [[PRD|Product requirements]]
- [[API/README|API index]]
@@ -0,0 +1,285 @@
# Channel protocol architecture
> **Status:** Current shared type contract and Discord compatibility baseline. The shared gateway registry, Telegram parity, Matrix integration, identity-linking, and multi-surface multiplexing described below are draft or unimplemented.
>
> **Audience:** Developers maintaining `@mosaicstack/types`, channel plugins, the gateway chat/plugin boundaries, or future official adapters.
>
> **Last verified:** 2026-08-10 against the source and focused tests listed in [Evidence](#evidence).
>
> **Authority:** Executable source and tests are authoritative for current behavior. This page explains the boundary; it is not a runtime registry, an API contract, a requirements document, or proof that every channel uses the shared DTOs.
## Reading this page
This page intentionally separates three states:
- **Current** — implemented in the repository and supported by the cited tests.
- **Compatibility** — an existing wire path that preserves current behavior but does not yet mean that the shared channel ports are wired through the gateway.
- **Draft** — a design direction or follow-up work item. Draft sections have no implementation authority and must not be used as instructions for operating Telegram, Matrix, identity linking, or cross-surface fanout.
The migration from `docs/_old_structure/architecture/channel-protocol.md` is a documentation correction. It does not add adapters, change gateway behavior, change authentication, or create database objects.
## Authority and evidence boundaries
| Boundary | Current authority | What this page may claim |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Shared channel types and ports | [`channel.dto.ts`](../../../packages/types/src/channel/channel.dto.ts), [`channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts), and their exports | The TypeScript shapes and method signatures that are currently published from `@mosaicstack/types`. |
| Discord native behavior | [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) and [`index.test.ts`](../../../plugins/discord/src/index.test.ts) | The Discord allowlist, pairing, role, thread, ingress, egress, retry, and health behavior covered by source and tests. |
| Discord gateway compatibility | [`chat.gateway.ts`](../../../apps/gateway/src/chat/chat.gateway.ts), [`chat.gateway-auth.ts`](../../../apps/gateway/src/chat/chat.gateway-auth.ts), and the focused gateway tests | The signed Socket.IO service path, gateway validation, raw chat events, and current session dispatch behavior. |
| Plugin hosting | [`plugin.interface.ts`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), and [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) | The lifecycle registry that exists today. It is not evidence of a shared `OfficialChannelAdapter` registry. |
| Telegram | [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts) and [`package.json`](../../../plugins/telegram/package.json) | The raw legacy behavior that exists. It is not evidence of shared-contract parity or a working authenticated gateway integration. |
| Matrix, identity linking, and multiplexing | No matching current implementation and test boundary was found for the old page's designs | These topics remain explicitly draft/unimplemented here. |
The current source boundaries also distinguish two identities:
1. A channel route carries a configuration-owned logical agent and response destination.
2. The gateway chooses provider, model, and runtime session internally. Durable-session enrollment is separate and is not proven by the external channel route alone.
A route is therefore not a claim that a channel adapter owns or exposes a harness, provider, model, process, or native runtime-session identity.
## Current shared contract
The channel types are exported through `packages/types/src/channel/index.ts` and `packages/types/src/index.ts`. They define a transport-neutral vocabulary, but TypeScript interfaces alone do not prove that every producer or consumer uses that vocabulary.
### DTOs
The current DTO surface is:
| Type | Current shape and boundary |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ChannelMetadataValue` | JSON-safe strings, numbers, booleans, `null`, arrays, and nested objects. |
| `ChannelAttachmentDto` | `id`, `name`, `mimeType`, `url`, and optional `sizeBytes`. |
| `ChannelMessageDto` | `id`, `channelName`, `channelId`, `senderId`, `senderKind`, `content`, `contentKind`, `timestamp`, and `metadata`; `threadId`, `replyToId`, and `attachments` are optional. |
| `ChannelAuthorizedPrincipalDto` | Native `channelUserId`, a `viewer`/`operator`/`admin` role, and an optional `mosaicUserId` for privileged gateway policy. |
| `ChannelBindingDto` | Configuration-owned `bindingId`, `channelName`, `workspaceId`, `channelId`, `logicalAgentId`, and paired `principals`. Credentials are intentionally absent. |
| `ChannelResponseTargetDto` | `channelId` and an optional `threadId`. |
| `ChannelConversationRouteDto` | `bindingId`, `logicalAgentId`, `conversationId`, `channelName`, `authorizationChannelId`, and `responseTarget`. |
| `ChannelIngressDto` | `correlationId`, `nativeMessageId`, an operation, an authorized principal, a normalized message, and a stable route. |
| `ChannelEgressDto` | `correlationId`, a normalized message, and the stable route. |
| `ChannelAdapterHealthDto` | `status` of `connected`, `degraded`, or `disconnected`, with optional `detail`. |
The available operations are `message.send`, `approval.create`, and `session.stop`. The available sender kinds are `user`, `agent`, and `system`; content kinds are `text`, `markdown`, `code`, `image`, and `file`.
### Lifecycle and ports
The shared adapter file currently defines these seams:
```typescript
interface OfficialChannelAdapter {
readonly name: string;
start(): Promise<void>;
stop(): Promise<void>;
health(): Promise<ChannelAdapterHealthDto>;
}
interface ChannelIngressPort {
receive(ingress: ChannelIngressDto): Promise<void>;
}
interface ChannelEgressPort {
send(egress: ChannelEgressDto): Promise<void>;
}
```
`ChannelDeliveryError` currently has only these codes: `invalid_route`, `destination_unavailable`, and `delivery_failed`. The type surface does not define a revoked-auth error code or an executable protocol version `1.0.0`.
### Stable route rule
`ChannelConversationRouteDto` deliberately omits provider, harness, model, process, and native runtime-session fields. The Discord implementation derives its current conversation address as:
```text
<logical-agent-id>:discord:<response-channel-id>
```
and derives its binding address from the configured guild, parent channel, and logical-agent instance. The gateway validates the expected Discord conversation address before dispatch. This is a route-integrity rule, not a claim that the shared DTO is already the gateway's universal session API.
### What is and is not wired today
The Discord class implements both `OfficialChannelAdapter` and `ChannelEgressPort`, and accepts an optional `ChannelIngressPort` dependency. The direct ingress seam is exercised by the Discord tests. However, the gateway host currently registers `IChannelPlugin` objects with only `name`, `start`, `stop`, and optional project provisioning. Its `PLUGIN_REGISTRY` is an array of those lifecycle wrappers; it does not expose `health()`, `ChannelRegistry.healthAll()`, or shared port wiring.
The gateway's current output path is also still Socket.IO event streaming (`agent:start`, `agent:text`, and `agent:end`). No gateway service in the cited implementation produces a `ChannelEgressDto` for a registered adapter. The shared ports are therefore current contracts and a tested Discord seam, not a completed gateway-wide adapter architecture.
## Current Discord compatibility path
Discord is the current reference implementation for the shared contract and the compatibility path. Its behavior is split between native Discord translation in the plugin and gateway-side validation/dispatch.
### Native ingress and authorization
For an inbound Discord message, the plugin currently:
1. Ignores bot-authored messages and messages without a guild.
2. Uses the configured parent text channel as the authorization channel only when the message is in a thread. A normal channel's category parent is not substituted for the channel itself.
3. Applies default-deny guild, channel, and user allowlists.
4. Resolves a configuration-owned binding and paired user role before creating a thread or dispatching to the gateway. `viewer` cannot send turns; approval and stop are admin operations.
5. Applies per-user/channel message and mention-thread rate limits before Discord thread creation or gateway dispatch.
6. Builds a stable route from the configured logical-agent instance and the response channel/thread.
7. Normalizes the authorized turn to `ChannelIngressDto` when a direct `ingressPort` dependency is supplied.
The normalized `ChannelMessageDto` currently includes:
- `channelName: "discord"`;
- the response channel as `channelId`;
- the Discord author as `senderId` and `senderKind: "user"`;
- `markdown` for non-empty text, or `image`/`file` for attachment-only input;
- attachments mapped to `ChannelAttachmentDto`; and
- `metadata` containing `channelMessageId` and `guildId`.
The implementation does **not** currently populate `channelType`, mentions, embeds, or `replyToId` in that normalized metadata. The old page's broader Discord metadata table must not be treated as current behavior.
### Direct shared ingress versus compatibility envelope
When a direct port is present, the plugin calls `ChannelIngressPort.receive()` with the complete normalized ingress DTO. In the current gateway-hosted path, the plugin instead signs a compatibility envelope and emits one of these Socket.IO events:
| Shared operation | Compatibility event | Current envelope boundary |
| ----------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `message.send` | `message` | Correlation ID, native Discord message ID, guild/channel/user IDs, conversation ID, content, optional thread ID, and attachments. |
| `approval.create` | `discord:approve` | The same signed Discord identity and route fields, carrying the approval command. |
| `session.stop` | `discord:stop` | The same signed Discord identity and route fields, carrying the stop command. |
The signature is HMAC-SHA-256 over the ordered envelope payload using the injected Discord service token. The token is used for service authentication and is not part of the protocol payload.
### Gateway validation and dispatch
The gateway exposes the `/chat` Socket.IO namespace. A Discord connection authenticates with `discordServiceToken`; ordinary clients use a BetterAuth session. For Discord service messages, the gateway:
1. Verifies the signed envelope with `DISCORD_SERVICE_TOKEN`.
2. Re-applies the configured guild, channel, and user allowlists.
3. Resolves the configured binding and operation role.
4. Checks that the conversation ID matches the bound logical-agent instance and channel/thread.
5. Rejects a repeated native Discord message ID through the bounded replay protector.
6. Reconstructs a gateway `ChatSocketMessageDto` containing the conversation ID, content, and validated attachments.
7. Uses the configured Discord service principal/tenant for ordinary chat dispatch and the paired `mosaicUserId` for privileged approval/stop policy where required.
8. Selects the trusted `agentConfigId` from the binding and verifies that the provisioned agent name matches the binding's logical-agent instance.
This path is intentionally described as compatibility: the gateway receives a signed Discord envelope and reconstructs chat input; it does not currently receive a complete `ChannelIngressDto` from the host registry.
### Persistence and durability boundary
The Discord `conversationId` is an external route string such as `<logical-agent-id>:discord:<channel-or-thread-id>`. Persisted conversations and messages use UUID conversation IDs. No current route-mapping layer was found that resolves the external route to a generated UUID before ordinary Discord writes. The gateway catches persistence/binding failures and may continue dispatch, so live output does not prove durable history or restart/resume continuity.
The focused cross-surface integration test explicitly pre-enrolls a durable session before exercising control flow. It does not prove that a fresh ordinary Discord message creates durable conversation/message rows. Current architecture claims are therefore limited to authenticated routing and live delivery. Durable Discord continuity requires a route-to-UUID mapping, observable persistence failures, ordinary-ingress enrollment where required, and a fresh-database restart test.
### Thread and conversation behavior
| Inbound case | Current route and side effect |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Authorized untagged message in a parent channel | Uses the parent channel as the response target; no thread is created. |
| Authorized bot mention in a parent channel | Creates a public thread, or reuses the thread already attached to that message, and routes the response to that thread. |
| Authorized follow-up in an existing thread | Authorizes against the configured parent channel and keeps the existing thread; it does not create a nested thread. |
| `/approve` or `/stop <approval>` | Uses the current parent/thread route and requires an already enrolled durable session; ordinary chat does not prove enrollment. |
| Requested thread creation fails | Does not dispatch the message, because the requested response target cannot be honored. |
### Discord egress and health
`DiscordPlugin.send()` is a typed egress implementation, even though the current gateway does not wire it as a universal `ChannelEgressPort`. It:
- rejects a forged or route-misaligned conversation before looking up a Discord destination;
- rejects a missing destination with `destination_unavailable`;
- chunks text at a Discord-safe 1,900-character boundary;
- retries transient rate-limit, server, and network failures up to three attempts;
- derives one stable nonce per correlation/chunk and sends with Discord's enforced nonce option; and
- does not retry permanent delivery failures.
The plugin reports `connected`, `degraded`, or `disconnected` from Discord client readiness and gateway socket connectivity. The health method is tested without exposing provider or runtime state. Outbound delivery uses Discord's native send operation; code-content wrapping from the former page is not implemented.
Agent output reaches the plugin through the current `agent:start`/`agent:text`/`agent:end` Socket.IO events. The plugin buffers the text by conversation ID and calls its typed Discord egress method when the stream ends.
## Draft: shared adapter registry and gateway wiring
**Status: Draft / unimplemented.**
The gateway does have a startup `IChannelPlugin[]` registry, but that registry is a lifecycle host for the current Discord and Telegram wrappers. It does not register `OfficialChannelAdapter` instances, inject `ChannelIngressPort` and `ChannelEgressPort` through a common gateway service, expose adapter health, or implement a dynamic `ChannelRegistry` with `getAdapter`, `listAdapters`, and `healthAll` semantics.
The former page's claim that adapters are already registered uniformly, or that new adapters can be added without channel-specific gateway branches, is not current. The gateway still has Discord-specific authentication, envelope, approval, stop, replay, and binding branches.
A future implementation may define a registry and host lifecycle, but that work must first specify:
- ownership and injection of ingress and egress ports;
- health and failure semantics;
- binding and credential loading boundaries;
- compatibility behavior for existing Socket.IO clients; and
- tests proving that an adapter cannot bypass gateway authorization or route validation.
Until then, this section is design context only.
## Draft: Telegram shared-contract parity
**Status: Raw legacy adapter exists; shared protocol parity and authenticated gateway participation are unimplemented/unproven.**
The current Telegram plugin is not an `OfficialChannelAdapter` implementation. Its source currently:
- launches a Telegraf bot and a Socket.IO client;
- accepts only messages with a text field and ignores attachment-only messages;
- maps each Telegram chat ID to `telegram-<chatId>`;
- emits a raw `{ conversationId, content, role: "user" }` object rather than `ChannelIngressDto`;
- has no shared DTO import, channel binding, principal/role policy, native message ID, attachment mapping, route validation, or health method; and
- sends plain `sendMessage` responses in chunks, without the former page's claimed MarkdownV2, photo, or document handling.
The Telegram Socket.IO client does not provide the Discord service token or a BetterAuth session in its connection options. The source therefore does not establish participation in the gateway's current authenticated connection path. The package's test script uses `--passWithNoTests`, and no package test file is present in this checkout.
Future Telegram parity is draft work. It would need an explicit identity/authentication boundary, shared ingress normalization, binding and operation policy, route-safe egress, health reporting, and focused tests before this page could describe Telegram as an official shared-contract adapter.
## Draft: Matrix integration
**Status: Draft / unimplemented in the channel protocol.**
No current gateway adapter, shared-port wiring, channel binding, identity resolver, room/conversation persistence boundary, or focused channel tests were found for the Matrix design described by the former page. The old Conduit choice, appservice registration, room and Space mappings, ghost users, encryption defaults, retention jobs, and agent-room behavior are therefore proposals, not current system behavior.
Those details must not be copied into implementation instructions or treated as deployment requirements. A future Matrix effort must independently decide and implement its homeserver/appservice boundary, authentication, route mapping, persistence, authorization, delivery, and tests.
## Draft: channel identity linking
**Status: Draft / unimplemented.**
The shared contract carries an already-authorized `ChannelAuthorizedPrincipalDto`; it does not implement a generic channel-identity database or linking flow. Discord currently uses configuration-owned allowlists and pairings. A pairing may include a `mosaicUserId` for privileged operations, while ordinary Discord chat dispatch uses the configured service principal and tenant in the gateway.
No current evidence establishes the former page's proposed `channel_identities` table, OAuth/deep-link flow, anonymous-principal behavior, persistent Matrix session, or revocation endpoint. Those are not implied by the optional `mosaicUserId` field and must remain planned work until schema, auth, gateway, and adapter implementations exist together.
## Draft: multi-surface conversation multiplexing
**Status: Partial raw chat-session support exists; the proposed channel-protocol fanout architecture is unimplemented.**
The gateway currently tracks client/conversation sessions and emits raw typed Socket.IO chat events. Focused gateway tests verify isolation of concurrent conversation streams sharing one socket. The chat event contract is `ChatMessagePayload` plus `agent:*` events, not `ChannelMessageDto` fanout.
There is no evidence in the cited current path for the former page's complete `ConversationService` plus Valkey pub/sub topology, canonical cross-surface `ChannelMessageDto` persistence, or Matrix fanout. Concurrent stream isolation must not be presented as multi-surface channel multiplexing.
A future multiplexing design must define canonical message ownership, subscription and fanout boundaries, replay/ordering behavior, conflict semantics, and per-surface authorization before it can become architecture guidance.
## Current limitations and version boundary
The following are intentionally not claimed as current protocol policy:
- A semantic protocol version of `1.0.0`. `packages/types/package.json` currently reports package version `0.0.2`, while `packages/types/src/index.ts` exports `VERSION = "0.0.0"`; no migration machinery is present in the cited channel code.
- A generic revoked-auth `ChannelDeliveryError` code. The current union contains only `invalid_route`, `destination_unavailable`, and `delivery_failed`.
- Structured log records with a universal `{ channel, event, ... }` schema. Current plugin logs are string-prefixed.
- Universal adapter health monitoring by the gateway host. Discord exposes health; the `IChannelPlugin` host does not.
- Complete `ChannelEgressDto` delivery from the gateway to every channel. Discord's current gateway egress remains Socket.IO stream events followed by plugin-side delivery.
These limitations are evidence boundaries, not requests to change implementation in this documentation migration.
## Evidence
Current-contract evidence:
- [`packages/types/src/channel/channel.dto.ts`](../../../packages/types/src/channel/channel.dto.ts) — DTOs, enums, route fields, and metadata shape.
- [`packages/types/src/channel/channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts) — adapter lifecycle, ingress/egress ports, and delivery errors.
- [`packages/types/src/channel/index.ts`](../../../packages/types/src/channel/index.ts) and [`packages/types/src/index.ts`](../../../packages/types/src/index.ts) — export surface and current package `VERSION`.
Discord evidence:
- [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) — native translation, authorization, thread routing, signed compatibility envelope, typed ingress/egress seam, retry, and health.
- [`plugins/discord/src/index.test.ts`](../../../plugins/discord/src/index.test.ts) — direct ingress, routing/thread behavior, authorization ordering, attachments, route-safe egress, retries, chunking, and health.
- [`apps/gateway/src/chat/chat.gateway.ts`](../../../apps/gateway/src/chat/chat.gateway.ts) — `/chat` namespace, service/session authentication, signed-envelope reconstruction, trusted binding selection, and raw stream egress.
- [`apps/gateway/src/chat/chat.gateway-auth.ts`](../../../apps/gateway/src/chat/chat.gateway-auth.ts) — Discord service-token and BetterAuth session validation.
- [`apps/gateway/src/plugin/discord-ingress.security.spec.ts`](../../../apps/gateway/src/plugin/discord-ingress.security.spec.ts) — signature, allowlist, replay, binding, attachment, approval, stop, and logical-agent checks.
- [`apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts`](../../../apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts) — focused control-flow test with explicit durable-session pre-enrollment; not ordinary persistence evidence.
Hosting and compatibility evidence:
- [`apps/gateway/src/plugin/plugin.interface.ts`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), and [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) — current lifecycle-only plugin host.
- [`packages/types/src/chat/events.ts`](../../../packages/types/src/chat/events.ts) — raw Socket.IO chat event contracts.
- [`apps/gateway/src/chat/chat.gateway-redaction.spec.ts`](../../../apps/gateway/src/chat/chat.gateway-redaction.spec.ts) — current per-client/per-conversation stream isolation evidence.
Telegram evidence:
- [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts) — current raw Telegraf and Socket.IO behavior.
- [`plugins/telegram/package.json`](../../../plugins/telegram/package.json) — package scripts, including `--passWithNoTests`.
@@ -0,0 +1,63 @@
# Compaction observer revocation and runtime generations
> **Status:** Current contract reference.
> **Audience:** Developer and security reviewer.
> **Evidence:** Mutator-gate and framework portability acceptance suites consume this page; live deployment gaps remain explicitly labeled below.
WI-3 connects Claude and Pi compaction/session lifecycle events to the existing authenticated lease-broker state machine. It does not add a second lease store or let runtime hooks assert identity. Each observer inherits the broker-minted session, resolves the current private runtime generation, and sends the existing `revoke_lease` action over the authenticated Unix socket.
## Observer matrix
| Runtime | Lifecycle signal | Action |
| ---------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Claude / Claudex | `PreCompact` | Revoke the current lease before compaction. A non-zero hook result blocks the lifecycle transition. |
| Claude / Claudex | `SessionStart` with matcher `compact` | Revoke again after compacted context starts. |
| Claude / Claudex | `SessionStart` with matcher `resume\|clear` | Atomically advance the private generation, then revoke the replacement incarnation. |
| Pi | `session_before_compact` | Revoke before compaction; return `{ cancel: true }` if revocation cannot be confirmed. |
| Pi | `session_compact` then the first `context` | Arm and run an independent post-compaction revoke. A failed post observer blocks later tools locally until a retry succeeds. |
| Pi | `session_start` with reason `reload`, `new`, `resume`, or `fork` | Atomically advance the private generation, then revoke the replacement incarnation before reuse. |
The first observer that reaches the broker deletes pending promotion tokens and makes the lease `UNVERIFIED`. The second compaction observer is deliberate redundancy, not a prerequisite for the first. Claudex receives the same mandatory hooks in its isolated `CLAUDE_CONFIG_DIR`; hook merging preserves unrelated isolated settings and rejects malformed or symlinked settings fail-closed.
## Private generation authority
`launch-runtime.py` still registers before `exec`, preserving the kernel-authenticated PID/starttime anchor. It now also creates `generation-<broker-session>.state` beside the broker socket. The file is owner-only mode `0600` under the broker's mode-`0700` directory. Hook descendants read that file instead of relying only on an immutable inherited environment value.
Generation changes use an exclusive file lock, validate owner/type/mode/size, increment monotonically, truncate and write the complete new value, and `fsync` before contacting the broker. Therefore reload, new-session, resume, and fork events may retain the same PID/starttime while still becoming a new broker incarnation. The higher generation causes the broker to atomically discard prior tokens and lease authority; the replacement generation inherits no VERIFIED lease.
If an observer fires while broker transport is unavailable, `revoke-lease.py` advances the private generation as a local fence before returning non-zero. Every later all-tools gate reads that higher value. When the broker is reachable again, authentication of that value performs the same old-generation revocation before authorization. Pi also keeps a process-local post-compaction/rollover failure latch that blocks tool calls. An unsafe or unreadable generation file itself makes both lifecycle revocation and tool authorization fail closed.
## Threat contract and stopping boundary
### BOUNDED RESIDUAL STALE WINDOW
If **both** pre- and post-compaction observers are missed entirely, no revocation signal exists. During the remaining unexpired lease, **within-TTL consequential actions are allowed**. Their count and timing are **bounded by lease expiry, not by the mutator gate**. WI-3 makes no claim that it bounds mutator actions inside this stale interval. The broker's monotonic lease TTL is capped at 300 seconds; after expiry, the next consequential tool is denied with `LEASE_EXPIRED`.
This is the named D2-v5 T-A residual. It is distinct from an observer that fires but cannot contact the broker: the latter creates a local generation fence and fails closed. It is also distinct from T-C total rot, where the lifecycle observers and the all-tools gate are both absent or replaced. Server-side branch protection, required CI, and independent review remain the irreducible backstop for T-C.
| Condition | Result |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Either compaction observer succeeds | Existing lease and pending promotion tokens are revoked immediately. |
| Observer runs but broker confirmation fails | Lifecycle transition is denied where supported; local generation fence and runtime latch prevent inherited authority. |
| Both observers are missed, lease unexpired | **ALLOWED** inside the bounded residual stale window. No within-window mutator bound is claimed. |
| Both observers are missed, lease expired | **DENIED** by monotonic TTL expiry. |
| Generation advances on reload/new/resume/fork | Prior incarnation revoked; replacement starts `UNVERIFIED`. |
| Lifecycle observers and all-tools gate both fail or are removed | T-C total-hook-miss residual; protected-branch controls remain required. |
## T-C server-side branch-protection posture
The required posture is that `main` is push-blocked and PR-only-merge is **MANDATORY**, regardless
of client-gate state. The client-side gate narrows the exposure window only; it is not the T-C
guarantee. The server-side protected-branch configuration is the irreducible guarantee for protected
repository actions. Status-check enforcement and approval enforcement are **RECOMMENDED**.
## Current-vs-required gap (recorded, not enacted)
The current empirical configuration is recorded here without re-probing or mutating live branch
protection. `enable_push=False` (push-block present), so the mandatory push-block/PR-only-merge core
holds. `require_approvals=0` (approvals not enforced), `enable_status_check=False` (status checks not
enforced), and `block_on_official_review=False` (official review not enforced). Those recommended
merge-quality controls are the current gap; changing them is a separate, owner-gated operations
decision and is not enacted by this documentation change.
The permanent T12b/T30 acceptance case prints both required outcomes: dual-hook miss within TTL is **ALLOWED**, and the same lease after TTL is **DENIED**. Separate real-socket tests prove each Claude observer and same-PID generation rollover; Pi lifecycle tests exercise pre/post observers, all four replacement reasons, and local failure closure.
@@ -0,0 +1,15 @@
# Architecture Decisions
> **Status:** Current decision index. A decision describes an implemented and accepted boundary; draft proposals belong under `rfcs/` or `docs/plans/`.
## Current decisions
- [Mos runtime portability M1 — logical identity and fencing](mos-runtime-portability-m1.md) — implemented lease, grant, audit, policy, and fencing boundary; connector activation remains held.
Decision pages do not override product requirements, API contracts, or executable behavior. Each page must identify implementation evidence, operational status, and explicit non-goals.
## Related
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
- [[PRD|Product requirements]]
- [[ADMIN-GUIDE/operations/mos-connector-lease-operations|Held connector lease operations]]
@@ -0,0 +1,96 @@
# Mos Runtime Portability M1 — Logical Identity and Fencing
> **Decision status:** Current implemented decision (M1).
> **Operational status:** The lease/fencing boundary is implemented and test-covered; connector activation remains held.
> **Audience:** Mosaic developers, gateway/runtime adapter authors, and security reviewers.
> **Authority:** This page records the current M1 boundary. The executable source and tests are authoritative for behavior; [`PRD.md`](../../../PRD.md) remains authoritative for requirements.
## Decision
M1 separates the logical Mosaic agent from any Claude, Pi, Codex, tmux, Matrix, or provider-native session. The core identity is:
```text
(tenant_id, logical_agent_id, binding_id)
```
`logical_agent_id` is a Mosaic logical identifier, not a runtime session identifier. The gateway derives the tenant from authenticated actor scope. The current internal service boundary normalizes the supplied logical-agent identifier and binding; a future public boundary must resolve and authorize those values server-side. Runtime-native session IDs are not part of the lease or execution-grant contract.
A connector is a replaceable holder of authority for one logical binding. It is not the logical agent identity.
## Durable lease model
The additive migration creates `logical_agent_connector_leases` with one unique row per tenant/logical-agent/binding tuple. The current row contains:
- an opaque lease UUID;
- connector ID and normalized allowed scopes;
- a positive decimal fencing epoch exposed by the application and stored as PostgreSQL `bigint`;
- acquisition, heartbeat, expiry, release, and update timestamps.
Initial acquisition is insert-only. An existing active row yields `lease_held`. An expired or released row yields `takeover_required`; ordinary acquisition does not recover it. An authorized takeover compares and swaps the expected epoch, rotates the lease UUID, and increments the epoch atomically. Heartbeat and release match the full identity, binding, connector, lease UUID, and epoch.
### Audit and enforcement precision
`connector_lease_audit_log` is an **application-level append-only contract**: the repository has an insert-only audit path and writes credential-safe lifecycle/rejection metadata. The checked-in schema and migration add a table and indexes, but do not add database triggers, permissions, or other database-level protection against `UPDATE` or `DELETE`. M1 therefore does not claim database-enforced immutability; an append-only guarantee at the database security boundary is a later hardening requirement.
Likewise, identifier, scope, and epoch normalization is performed at the application boundary by the shared types/coordinator and gateway service. The database enforces column types, non-null fields, the lease primary key, and the unique tenant/logical-agent/binding index, but it does not independently enforce all application formats or semantics such as positive epochs, canonical scope ordering/deduplication, or constrained identifier patterns. Database shape must not be mistaken for a second normalization layer.
## Execution grants and TTL boundaries
`ConnectorLeaseCoordinator` issues a short-lived internal grant only after rereading the durable current lease. It enforces hard defense-in-depth maxima of five minutes for leases and thirty seconds for grants; constructor options may only tighten those limits. A grant is bound to tenant, logical agent, binding, connector, lease UUID, scope subset, expiry, correlation ID, and epoch.
The gateway policy and coordinator have deliberately separate TTL responsibilities:
- `ConnectorLeaseService` passes the request's numeric `ttlMs` to policy as `requestedTtlMs` for acquire, takeover, heartbeat, and grant issuance. Identity and scopes are normalized before policy evaluation, but this TTL is a request input, not a coordinator-normalized or effective TTL.
- Release and read policy checks use `null` because they do not request a TTL.
- After policy authorization, the coordinator validates the TTL as a positive safe integer and rejects values above the applicable hard cap (`300000ms` for leases or `30000ms` for grants). It does not silently clamp an over-cap value.
- A policy may impose a stricter duration limit, but it cannot expand the coordinator cap. A policy implementation must validate the requested value itself if its decision depends on a bounded or canonical TTL.
Grant validation occurs immediately before adapter invocation and rereads the durable current row. Validation denies:
- grants not minted by the current gateway process, including cloned or forged objects;
- expired grants or leases;
- released leases;
- stale epochs or replaced connectors/lease UUIDs;
- missing, cross-tenant, cross-agent, or cross-binding leases; and
- scopes not authorized by both the grant and the current lease.
A gateway restart intentionally invalidates process-local grant provenance. The durable lease and epoch survive, but a fresh grant is required after current-lease and policy validation.
## Adapter boundary and activation state
The normalized `ConnectorExecutionContext` and `FencedConnectorAdapter` contract define the future adapter boundary. When a concrete adapter is activated, it must receive the context only after the coordinator's final validation and must propagate the epoch/context to downstream effect boundaries that need to fence races after gateway validation.
That contract is **not evidence of an activated adapter**. The current production module registers the lease repository, service, and deny-all policy. The coordinator and gateway integration tests use test adapters, but current production runtime providers and channel paths do not consume `ConnectorExecutionContext` or call `executeGrant`. No concrete Claude, Pi, Codex, Matrix, tmux, provider, or channel connector is activated by M1.
`ConnectorLeaseService` is the gateway-owned policy surface. It derives tenant scope from authenticated context, records policy denials, and is internal: no M1 connector-lease HTTP controller or administration endpoint is registered. The default `DenyConnectorLeasePolicy` rejects every lease and grant operation until a separately authorized server-side policy is supplied.
## Current implementation evidence
The current boundary is represented by the following checked-in surfaces:
- Contract and application normalizers: `packages/types/src/agent/connector-lease.dto.ts` and its unit specification.
- Coordinator, TTL caps, grant provenance, reread, and fencing checks: `packages/agent/src/connector-lease.ts` and its unit tests.
- Gateway policy, tenant derivation, and default deny wiring: `apps/gateway/src/agent/connector-lease.service.ts` and `agent.module.ts`.
- Durable repository and compare-and-swap mutations: `apps/gateway/src/agent/connector-lease.repository.ts`.
- Schema and additive migration artifacts: `packages/db/src/schema.ts` and `packages/db/drizzle/0016_salty_morlocks.sql`.
- Gateway integration and repository specifications: `apps/gateway/src/agent/connector-lease.integration.test.ts` and `connector-lease.repository.test.ts`.
These references establish the implemented boundary; they do not imply that a production database, connector policy, or concrete connector is currently active.
## Explicit non-goals
M1 does **not** provide or authorize:
1. Production connector activation, channel cutover, or cross-harness failover/rollback E2E.
2. Concrete Claude, Pi, Codex, Matrix, tmux, provider, or channel adapters, or integration of existing runtime providers with this lease context.
3. A connector-lease HTTP API or caller-controlled tenant authority.
4. Exactly-once connector receipts, a side-effect journal, checkpoint/handoff payloads, replay deduplication, or recovery semantics for an external effect that may already have happened.
5. Database-enforced append-only audit rows or database-enforced copies of application identifier/scope/epoch normalization.
6. A claim that gateway validation alone fences an effect after an adapter has crossed the boundary; downstream adapters and effect journals remain later work.
A valid lease or grant is therefore not a claim of exactly-once delivery, production connector readiness, or completed runtime cutover.
## Related contract
- [M1 connector lease operations — held/non-operative](../../../ADMIN-GUIDE/operations/mos-connector-lease-operations.md)
- [MOS-PORT requirements](../../../PRD.md#mos-runtime-portability-workstream-mos-port)
@@ -0,0 +1,39 @@
# Authenticated external lease broker protocol
> **Status:** Current contract reference.
> **Audience:** Developer and security reviewer.
> **Evidence:** Lease-broker acceptance suites consume this page; executable behavior remains authoritative in source and tests.
The compaction-refresh lease broker is a Linux-only, newline-framed JSON protocol over a Unix stream socket. It is runtime-neutral; M1 consumers are limited to Claude and Pi. This is an internal process boundary, not an HTTP API, so it is intentionally absent from OpenAPI.
The broker, never the caller, obtains `(pid, uid, gid)` from kernel `SO_PEERCRED`. It correlates the PID with `/proc/<pid>/stat` field 22 (`starttime`) and mints `session_id` on `register_anchor`. Presence of `session_id` in that request is refused even when its value is `null` or empty. Later requests must originate from the anchor or a descendant. The broker walks parent PIDs to the `(pid,starttime)` anchor and then rereads every walked PID's starttime before accepting the chain.
## Request and response boundary
Each connection carries exactly one UTF-8 JSON object followed by one newline, capped at 64 KiB. The protocol deliberately uses EOF to prove that there is exactly one frame: immediately after writing the newline, the client **MUST half-close its write side** with `shutdown(SHUT_WR)` (or Node `socket.end()`) before awaiting the response. A client that writes a newline but leaves its write side open receives no successful response; the broker's one-second connection deadline fails closed. Malformed, unterminated, multiple (including a delayed second frame), or oversized frames fail closed. Responses are one JSON object and one newline. Success has `{"ok":true,...}`; refusal has `{"ok":false,"code":"TYPED_CODE"}`. Requests are:
- `register_anchor`: `action`, non-negative `runtime_generation`; no `session_id` field.
- `authenticate`: `action`, broker-minted `session_id`, non-negative `runtime_generation`.
- `mint_token`: authenticated identity plus `binding` containing exactly `compaction_epoch`, `request_epoch`, `h_source`, `h_payload`, and `schema_version`.
- `consume_token`: authenticated identity plus `token`.
- `begin_verification`: authenticated identity, runtime (`claude` or `pi`), cycle `binding`, and a TTL no greater than 300 seconds. The broker revokes existing authority first, enters `PENDING_VERIFICATION`, and returns a single-use promotion token.
- `begin_recovery`: the constrained recovery entrypoint. It rejects caller-provided receipt/challenge fields and delegates to the same `begin_verification` transition, but reports `PENDING_DELIVERY` and marks the volatile cycle as recovery-owned.
- `complete_recovery`: authenticated identity only. It rejects caller-provided receipt/challenge fields, obtains the current recovery challenge only from broker state, and delegates to the same trusted-observer → evidence → consume → promote sequence. An observation failure revokes recovery authority; retry starts a fresh challenge.
The daemon owns a second protected production observer socket (mode `0600`) unless a private `--test-observer-file` fixture is selected. That transport accepts only the exact `record_runtime_observation` schema after kernel `SO_PEERCRED` plus the existing anchor/ancestry authentication; it validates the pending runtime/generation before storing one finalized assistant entry for the in-process `RuntimeReceiptObserver`. It is **not** a broker request action. Claude sends its latest assistant entry from the Stop-hook transport; Pi sends only finalized `message_end` assistant content. The public broker socket continues to reject request-supplied `latest_assistant_message` in begin, observe, and complete paths.
- `promote_lease`: authenticated identity plus the exact pending promotion token. The broker commits token consumption before making `VERIFIED` visible.
- `revoke_lease`: authenticated observer signal; deletes pending tokens and makes the session `UNVERIFIED` immediately. WI-3 Claude/Pi hooks send this existing action; `runtime` and bounded `reason` fields are diagnostic input only and never identity authority.
- `authorize_tool`: authenticated identity, runtime, and exact runtime-reported tool name. The broker returns an explicit allow/deny decision from the whole-class policy and current lease.
A higher generation for the same anchor atomically replaces the stored incarnation and deletes all prior tokens and lease authority for that session. A lower generation is stale. Runtime descendants resolve the current generation from an owner-only, locked generation file created by the register-before-exec launcher; reload/new/resume/fork observers advance and `fsync` it before broker revocation. This supports generation replacement even when PID/starttime do not change. Tokens are 256-bit values from the operating-system cryptographic RNG and are single use. At most 256 pending tokens may be persisted; another mint fails with `TOKEN_CAPACITY` before mutation. Successful consumption deletes the token, while a replay still fails with `TOKEN_REPLAY`. Live v1 token records retain the existing `consumed: false` schema.
VERIFIED leases are volatile and monotonic-time bounded: broker restart, generation change, explicit observer revocation, or expiry returns the session to `UNVERIFIED`. `begin_verification` always revokes before minting a new prerequisite. `begin_recovery` reuses that exact transition and mints a new challenge, so a normal-path receipt/challenge cannot be replayed through recovery. `promote_lease` is valid only from the matching pending cycle; persistence failure rolls token and lease state back, while post-rename durability uncertainty terminates the broker. The WI-1 token is the atomic promotion prerequisite substrate.
## Receipt boundary and T-C residual (R1)
Receipt evidence is a T-A delivery/liveness prerequisite only; it cannot replace the mechanical
mutator gate as safety authority. The receipt detects an **ABSENT** or **PREFIX-TRUNCATED** terminal
token. A **MIDDLE-DROP** that preserves the tail is a T-C contract violation that is **NOT receipt-detectable**. It is covered by server-side protected-branch controls, **NOT** by the receipt; no category-wide receipt-detection claim is made for that tail-preserving transformation.
State replacement serializes and enforces the 4 MiB maximum before opening a temporary file, then uses a mode-`0600` temporary file, `fsync`, atomic rename, and parent-directory `fsync`. Every broker mutation snapshots the prior v1 state. A commit failure before rename restores that snapshot and leaves durable state unchanged. A failure after rename makes durability uncertain, so the store is poisoned without rolling memory back and the daemon terminates rather than serving with divergent state. Existing state is opened without following symlinks, must be a bounded regular file at mode `0600`, and is fully schema- and invariant-validated before use. Persisted tokens must be unconsumed, match their session's current generation, and remain within the 256-token cap. Session identity is uniquely keyed by `(anchor_pid,anchor_starttime)`; duplicate logical sessions for one anchor refuse startup. State integrity or mode failures refuse startup. The daemon does not log session IDs or tokens.
@@ -0,0 +1,30 @@
# WI-1 lease broker security notes
> **Status:** Current contract reference.
> **Audience:** Developer and security reviewer.
> **Evidence:** The lease-broker implementation and acceptance material cross-check this boundary; deployment-review requirements remain explicitly labeled below.
- Trusted identity comes only from Linux `SO_PEERCRED` plus `/proc` starttime, never request identity fields.
- Descendant authorization is anchored to `(pid,starttime)` and uses a complete second starttime pass to fail closed on disappearance or PID-reuse races.
- Runtime generations are monotonic per anchor; a bump revokes prior-incarnation tokens before persistence commits. WI-3 stores the live generation in an owner-only locked file so same-PID Pi reload/new/resume/fork and Claude resume/clear transitions cannot inherit a VERIFIED lease.
- Session IDs and cycle tokens use the OS cryptographic RNG. `Math.random` and model output are not token sources.
- Framing and persistence failures fail closed. Sensitive tokens are not logged.
- Built-in `0700`/`0600` filesystem modes provide same-principal hardening only, not socket authenticity against the same UID. WI-1 provides no distinct-principal isolation. That stronger deployment requires an external protected proxy, ACL, or service boundary, and the boundary must preserve authenticated client identity for the broker's `SO_PEERCRED` and ancestry authorization rather than substituting a shared proxy identity.
- WI-2 whole-class authorization denies every consequential, unknown, and custom tool while UNVERIFIED; it does not inspect shell strings or trust wrapper selection. First-class Claude/Pi, both Claudex dispatch modes, PRDY, QA remediation, coord, orchestrator, and fleet starts converge on broker register-before-exec; Claudex additionally installs the mandatory all-tools hook inside its preserved isolated config and fails closed on unsafe settings.
- The permanent `check-runtime-launches.py` suite/CI guard scans production source for direct literal, absolute-path, process-API, command-array, and dynamic Claude/Pi launches. It has no bypass allowlist: an unrecognized launch form fails CI until routed through the common boundary.
- WI-2 promotion consumes a WI-1 cycle token before VERIFIED becomes visible. Observer revocation, runtime-generation replacement, broker restart, and monotonic TTL expiry remove authority.
- WI-3 wires redundant Claude `PreCompact`/`SessionStart(compact)` and Pi `session_before_compact`/post-`session_compact` `context` observers to that same revoke action. If broker confirmation fails after an observer fires, the revoker advances the private generation as a local fence; subsequent authorization revokes the stale broker incarnation before any consequential allow.
- Dual observer absence while a lease remains live is the named **bounded residual stale window**: consequential tools remain allowed until monotonic expiry, with no claimed within-window action bound. After expiry they are denied. Total observer-plus-gate absence remains T-C.
- Receipt observation, payload construction, and constrained recovery implementation remain later surfaces. A receipt can become a promotion prerequisite but is never the safety mechanism.
## Named residual: promote-lease-lost-ACK (WI-3 D2-v5)
A valid `promote_lease` can leave a session `VERIFIED` in the broker while the client never learns of it. This is a named, bounded D2-v5 T-A residual — an **authority-observability divergence, not an authority divergence, not an ALLOW-risk, and not a retry double-apply**. It is disclosed here, not laundered.
**Window — where it can occur.** The broker commits token consumption and durable `VERIFIED` state _before_ the success reply becomes visible (see the promotion order in `lease-broker-protocol.md`). The residual is confined to the interval after that commit+fsync when the broker→client reply or peer-ACK is lost — for example an extreme-contention send failure or peer disconnect after `handle()` has already mutated and persisted state (the #838 fail-closed transport path). The lease mutation is already durable broker-side; only the acknowledgement to the client is lost. No uncommitted or partially-applied state is involved: the commit either happened (and is authoritative) or it did not (and no lease exists).
**Fail-safe direction — the client can only under-claim.** Broker intent is the ceiling; client authority is always ≤ broker intent, never more. Client-side authority-belief is granted only by a _received_ acknowledgement; a lost acknowledgement conveys nothing, so the client cannot conclude "verified" and continues to treat itself as `UNVERIFIED` (it re-verifies or recovers). If the client retries `promote_lease` with the same token, the token is already consumed and the broker rejects the retry (`PROMOTION_TOKEN_MISMATCH` / `INVALID_LEASE_TRANSITION`); there is no double-apply. The committed `VERIFIED` state the broker holds is authority the lease _legitimately earned_ from a real promotion — the broker authorizing consequential tools under it is correct, not inflation. Divergence is therefore strictly toward _less_ client authority than the broker granted; it never produces authority the broker did not grant.
**Bound — TTL plus the observer/gen-bump revoke backstop, self-healing.** The orphaned `VERIFIED` lease is indistinguishable to the broker from any other legitimately verified lease, so the identical D2-v5 revocation backstops dispose of it: any compaction observer (`PreCompact` / `SessionStart(compact)` for Claude; `session_before_compact` / post-`session_compact` `context` for Pi), any same-PID runtime-generation bump (reload/new/resume/fork), broker restart, or monotonic-time expiry returns the session to `UNVERIFIED`. Monotonic TTL expiry (capped at 300 seconds) is **unconditional** — it requires no observer at all — so the maximum exposure of the orphaned lease is one TTL, ≤ 300 s, after which the next consequential tool is denied with `LEASE_EXPIRED`. Any observer that fires shortens the window further. The residual self-heals: "≥1 observer fires OR expiry ⇒ revoke" catches the lost-ACK lease on the same terms as every other stale lease. As with the dual-observer-miss stale window, WI-3 makes no claim that the mutator gate bounds actions inside the residual interval; the interval is bounded by TTL and the revoke backstop, and the server-side branch-protection / required-CI / independent-review line remains the irreducible backstop for protected-repository mutations.
Coordinator security review must rerun the real socket/peercred and mutator-gate acceptance suites on an unrestricted Linux runner and obtain the mandated independent Opus-SECREV review before integration.
@@ -0,0 +1,76 @@
# Whole mutator-class lease gate
> **Status:** Current contract reference.
> **Audience:** Developer and security reviewer.
> **Evidence:** Runtime launch-guard and mutator-gate tests cross-check this boundary; parser residuals and deployment gaps remain explicitly labeled below.
WI-2 adds the framework-native authorization boundary for Claude (including the supported Claudex overlay) and Pi. Every runtime-reported tool name reaches the lease broker before execution. The gate classifies capabilities by the whole tool class; it never parses a Bash command to decide whether that particular string looks read-only.
## Default-deny policy
While a session is not VERIFIED, only these exact classes are allowed:
- Claude: `Read`, `Grep`, `Glob`, `Ls`, `Find`
- Pi: `read`, `grep`, `find`, `ls`
- Both runtimes: the fixed `mosaic_context_recover` primitive
Every other built-in, unknown tool, and custom/MCP tool is consequential by default and is denied. This includes Claude `Bash`, `Edit`, `Write`, and `NotebookEdit`, plus Pi `bash`, `edit`, and `write`. A compromised model therefore cannot bypass Mosaic wrappers by selecting raw `git`, `curl`, `kubectl`, provider, deployment, or filesystem commands inside a generic mutator—the generic mutator itself is blocked before its input executes.
## Broker-owned transition order
The authenticated broker is the sole lease writer:
1. `begin_verification` revokes existing authority and pending tokens first, then records `PENDING_VERIFICATION` and mints one WI-1 single-use promotion token bound to the exact cycle.
2. `promote_lease` accepts only that session/generation/binding/token combination.
3. Token consumption commits before the volatile lease becomes VERIFIED. Promotion is last and cannot be reached directly from UNVERIFIED.
4. `revoke_lease`, a runtime-generation increase, broker restart, or monotonic expiry removes mutator authority.
The initial TTL is capped at the ratified 300-second maximum. A caller may request a shorter positive TTL but cannot lengthen the maximum. WI-3 installs the [compaction observer and generation lifecycle](compaction-revocation.md). Dual compaction-hook miss within an unexpired lease remains the ratified bounded T-A residual: consequential tools are allowed until expiry, with no claimed within-window action bound; once either observer revokes or TTL expires, the next consequential tool is denied.
A receipt is only a future promotion prerequisite. It is not an obedience, residency, or safety proof and never replaces this mechanical gate.
## Runtime adapters
`launch-runtime.py` registers itself with the broker and then `exec`s Claude or Pi so PID/starttime remain the authenticated parent anchor. It exports the broker-minted session ID and an owner-only generation-file reference to descendants; lifecycle hooks advance that file for same-PID replacement generations.
- Claude installs `mutator-gate.py` as an all-tools (`.*`) `PreToolUse` hook.
- `mosaic claudex` and `mosaic yolo claudex` preserve their isolated `CLAUDE_CONFIG_DIR`, merge the mandatory hook into that isolated `settings.json`, and use the same register-before-exec launcher. Malformed or symlinked isolated settings deny launch.
- Pi invokes the same executable from its `tool_call` handler.
The executable submits the runtime's actual tool name to `authorize_tool`. Missing identity, malformed input/reply, timeout, broker unavailability, or denial exits with status 2 and blocks fail-closed.
## Runtime-launch choke-point and permanent guard
Every repository-owned Claude/Pi launch entry converges on `launch-runtime.py`, either directly or through `mosaic``execLeaseGatedRuntime`. PRDY init/update and QA remediation invoke the wrapper directly so their existing prompts, dangerous-permission behavior, working directory, and environment survive without skipping broker registration. The raw Claude `--dangerously-skip-permissions` primitive is owned only by `launch-runtime.py`; callers request semantic `--dangerous` mode, and the wrapper validates Claude before injecting the primitive. `@mosaicstack/coord` rewrites direct Claude commands to `mosaic claude` and rejects unknown custom Claude launchers.
`check-runtime-launches.py` is the permanent completeness guard. It scans production shell, TypeScript/JavaScript, Python, and data launch definitions under `packages/`, `apps/`, `plugins/`, and `tools/`; direct literal, absolute-path, process-API, dynamic, command-substitution, `eval`, and variable-execution runtime launches fail. Shell comments are stripped with quote awareness, wrapper prefixes are tokenized with `shlex`, and only an invocation in command position with `--runtime` before the command separator is gated. Literal and tracked-variable command tokens use one terminal resolver after any nesting of `exec`, `command`, `nohup`, or `env` plus assignments. A direct command always wins over an inert marker on the same line. Independently, the raw dangerous primitive anywhere outside the choke-point is RED.
The command parser is a best-effort CI defense, not a complete shell interpreter. Alias/function redefinition, sourced commands, generated scripts, and encoded pipelines are intentionally residual rather than an invitation to chase an unbounded shell language. Two runtime controls backstop that residual surface: primitive ownership rejects a dangerous launch even when command identity is alias-indirected, and Claude's global `.*` `PreToolUse` hook invokes the broker gate for non-dangerous launches. Without `MOSAIC_LEASE_SESSION_ID`, representative read, mutator, and custom/MCP tools all fail closed with `GATE_UNAVAILABLE`. Hook absence or replacement remains in the documented T-C boundary.
### Parser stopping criterion
- **A — realistic parser matrix:** comments, inert strings/assignments, heredocs, continuations, chained commands, command substitution, `eval`, bare tracked variables, and quoted/unquoted tracked variables behind `exec`, `command`, `nohup`, or `env` are permanent RED regressions. Prefix-variable forms are covered in both multiline and same-line assignment shapes.
- **B — residual backstops:** a dangerous alias-indirected launch is RED solely through primitive anchoring; a parser-missed non-dangerous alias launch is paired with an acceptance test proving the global all-tools hook denies every representative tool class as `GATE_UNAVAILABLE` without a lease.
- **C — independent fresh review:** the parser class is considered complete only when reviewers find no new non-overlapping realistic evasion on the exact head. A and B are repository evidence; C is supplied by the fresh review round.
All three layers are load-bearing and complementary. The guard is mandatory in `@mosaicstack/mosaic`'s test script, so root CI fails on a future realistic bypass. Real-socket tests separately prove PRDY init/update and QA receive broker sessions and deny an unverified mutator.
The live inventory is emitted by:
```bash
python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root . --json
```
| Production launch family | Gated entries |
| ------------------------------------------------------ | ------------: |
| `@mosaicstack/coord` default/configured Claude command | 2 |
| Fleet runtime start | 1 |
| QA remediation + generated QA command | 2 |
| Orchestrator command construction/session launches | 3 |
| PRDY init/update | 2 |
| Mosaic Claude/Pi/Claudex adapter and wrapper boundary | 4 |
| **Total** | **14 / 14** |
## Assurance boundary
This closes T-A after an observer fires or lease expiry and T-B for in-runtime tool calls. Hook/extension absence, a runtime executing outside the gated launcher, ptrace/same-UID broker replacement, and other fully rotted behavior remain T-C. Server-side branch protection and required PR review/CI remain the irreducible line for protected repository mutations.
@@ -0,0 +1,15 @@
# Architecture RFCs
> **Status:** Current proposal index. RFCs are draft design material and have no operational or implementation authority until an approved decision and implementation evidence supersede them.
## Draft proposals
- [Optional AI egress gateways](optional-ai-egress-gateways.md) — proposed model-egress boundary; LiteLLM and Bifrost are not integrated, and Claudex remains experimental harness tooling.
A draft RFC must not be cited as a supported feature, deployment path, or approved architecture decision. Approved implemented boundaries belong under [`../decisions/`](../decisions/README.md).
## Related
- [[DEVELOPER-GUIDE/architecture/README|Architecture index]]
- [[DEVELOPER-GUIDE/architecture/decisions/README|Architecture decisions]]
- [[PRD|Product requirements]]
@@ -0,0 +1,371 @@
# RFC: Optional AI Egress Gateways
> **Status:** Draft / proposed — not approved, not current, and not integrated.
>
> **Authority:** Non-operative design proposal. This document does not authorize an
> integration, deployment, provider selection, credential flow, or production use.
> It is intentionally an RFC rather than an approved architecture decision.
>
> **Scope:** Optional model/inference egress only. `IProviderAdapter` and
> `AgentRuntimeProvider` are separate boundaries; this RFC does not merge them.
- **Date:** 2026-07-14
- **Related issues:** #754, #755
- **Decision owner:** Mosaic Gateway / provider-adapter architecture
## Context
The emergency Mos continuity path kept Claude Code as the harness and translated
Anthropic Messages traffic to Codex OAuth through a small localhost proxy. That
preserved the existing Claude Discord plugin and transcript, but exposed two
architectural facts:
1. Harness identity, channel entitlement, provider credentials, inference
transport, and runtime sessions are separate concerns.
2. A generic AI gateway could eventually improve model routing, budgets, and
observability, but it must not become Mosaic's identity, authorization,
tenant, connector, or orchestration boundary.
The Tess qualification work also found that provider rebinding is not, by
itself, identity-continuous failover. Any future design still needs a logical
agent identity, durable connector lease/fencing, canonical handoff/checkpoint,
exactly-once receipts, concrete runtime adapters, and cross-runtime rollback
validation.
This page is a reclassified migration of the historical egress-gateway
proposal. The move to `docs/DEVELOPER-GUIDE/architecture/rfcs/` does not promote
it to a decision and does not change runtime behavior.
## Current status and non-goals
This RFC describes a possible future integration boundary. It does **not** say
that any of the candidates below is supported by Mosaic Gateway today:
- **LiteLLM is not integrated.** It is only a candidate for a future model-egress
adapter and remains subject to the prerequisites in this RFC.
- **Bifrost is not integrated.** Its routing, virtual-key, and failover features
are research inputs only, not Mosaic authority.
- **Claudex and `claude-code-proxy` are experimental harness tooling, not
gateway egress.** The `mosaic claudex` path runs a Claude Code harness against
a local translation proxy for evaluation. It is not a Mosaic Gateway egress
integration, does not define the `IProviderAdapter` model contract, and does
not define the `AgentRuntimeProvider` runtime/session contract.
In particular, this RFC does not propose that a generic gateway receive channel
traffic directly, own Mosaic sessions, replace the runtime-provider registry,
or become a second authorization or tenant system. No database, service, or
runtime change is implied by this document.
## Corrected contract boundary
The historical proposal conflated two different adapter families. They must
remain separate.
### `IProviderAdapter`: model and inference egress
`IProviderAdapter` is the model-provider abstraction. Its current contract
covers provider registration, model discovery, provider health, and a future
direct completion stream. `ProviderService` aggregates these adapters and
connects model availability to the Pi `ModelRegistry`.
It does **not** represent an agent process or session. It does not own channel
identity, Mosaic actor or tenant identity, logical-agent ownership, runtime
session IDs, runtime attachments, handoff state, or tool authorization. An
optional LiteLLM, Bifrost, or similar model gateway would therefore be a
candidate for this model-egress path only, after approval and qualification.
### `AgentRuntimeProvider`: runtime and session boundary
`AgentRuntimeProvider` is the runtime-neutral session contract. It covers
runtime capabilities and health plus operations such as listing sessions,
reading session trees, streaming a session, sending a message, attaching or
detaching, and terminating a session. Its `RuntimeScope` is derived from
trusted actor, tenant, channel, and correlation context.
The runtime-provider service performs the gateway-side capability, authorization,
approval, and metadata-only audit boundary before invoking a runtime provider.
It is not a model-egress gateway and must not be substituted for
`IProviderAdapter`. A runtime implementation may use model selection or a model
service internally, but that dependency is an explicit composition between two
contracts; it does not make the runtime provider a model provider or make a
model gateway a session provider.
The checked-in contract and implementation references are:
- [`IProviderAdapter` and provider types](../../../../packages/types/src/provider/index.ts)
- [`ProviderService`](../../../../apps/gateway/src/agent/provider.service.ts)
- [`AgentRuntimeProvider`](../../../../packages/types/src/agent/agent-runtime-provider.ts)
- [`RuntimeProviderService`](../../../../apps/gateway/src/agent/runtime-provider-registry.service.ts)
- [`AgentRuntimeProviderRegistry`](../../../../packages/agent/src/runtime-provider-registry.ts)
### Proposed relationship
If a future implementation is approved, the model and runtime paths remain
parallel and explicitly composed at the Mosaic boundary:
```text
Discord / Matrix / CLI / web
|
v
Mosaic Gateway: authenticated actor + tenant, policy, approvals,
logical agent, connector lease/fence, audit, checkpoints,
idempotency, and side-effect receipts
|
+-----+-----------------------+
| |
v v
model request path runtime/session path
ProviderService / routing RuntimeProviderService
| |
v v
IProviderAdapter AgentRuntimeProvider
| |
v v
optional model-egress native or external runtime/session
gateway (future only) transport
|
v
upstream model/provider
```
The optional egress gateway may receive only a validated model request from the
Mosaic model path. It is not a channel ingress, runtime-session transport,
connector owner, or source of Mosaic identity.
## Draft proposal
This RFC proposes, for review only, that Mosaic could support an optional
model-egress gateway through a dedicated `IProviderAdapter` implementation or
adapter-owned model transport. Any such integration would remain subordinate
to Mosaic Gateway policy and would not add a second runtime/session boundary.
Mosaic Gateway would remain authoritative for:
- authenticated actor and tenant identity;
- logical-agent identity, connector binding, lease epoch, and stale-holder
fencing;
- authorization, approval, and model/tool policy;
- runtime/session access through the separate `AgentRuntimeProvider` boundary;
- audit correlation, redaction, retention, and operational evidence;
- canonical handoff, checkpoint, and recovery state; and
- idempotency keys, operation identity, and durable side-effect receipts.
An optional model-egress gateway must not:
- receive channel ingress directly;
- authorize tools, connector ownership, runtime sessions, or approvals;
- define Mosaic actors, tenants, logical agents, or session identity;
- treat downstream virtual keys as Mosaic principals;
- persist raw Mosaic handoffs, channel credentials, or unredacted telemetry;
- bypass adapter capability negotiation or gateway policy;
- retry or fail over an operation whose side-effect state is ambiguous; or
- silently select an unhealthy or unauthorized provider merely to return a
result.
## Candidate assessment
These dispositions are deliberately non-integrated and do not constitute
approval.
### LiteLLM
**Disposition:** Not integrated; future candidate for a formal
`IProviderAdapter` model-egress prototype only.
Potentially useful features include broad provider routing, virtual keys,
budgets, observability, and OpenAI/Anthropic-compatible surfaces. A future
review must verify, rather than assume, the exact ChatGPT subscription OAuth
flow, supported models, provider terms, token storage, encryption, revocation,
refresh, scope, and incident response.
A prototype would also have to prove streaming, tool calls, reasoning controls,
cancellation, tenant isolation, audit-correlation preservation, retry behavior,
and idempotency. Virtual keys must remain downstream credentials and must not
become Mosaic actors or tenants. Channel ingress and connector credentials
would remain outside LiteLLM.
Source references:
- [LiteLLM ChatGPT subscription provider](https://docs.litellm.ai/docs/providers/chatgpt)
- [LiteLLM providers](https://docs.litellm.ai/docs/providers)
### Bifrost
**Disposition:** Not integrated; future candidate for governance and routing
research, with subscription OAuth compatibility unverified.
Virtual keys, budgets, rate limits, weighted load balancing, and provider
failover may inform a future Mosaic egress design, but they are not Mosaic
authority. A future review must verify Codex/ChatGPT subscription OAuth rather
than assume API-key compatibility, map all policy to server-derived Mosaic
tenants, prove that failover preserves connector leases and approvals, and
redact request/response telemetry before persistence.
Automatic failover must be disabled or constrained whenever policy, approval,
or side-effect state is ambiguous.
Source references:
- [Bifrost overview](https://docs.getbifrost.ai/overview)
- [Bifrost repository](https://github.com/maximhq/bifrost)
### Claudex / `claude-code-proxy`
**Disposition:** Experimental harness overlay and local translation proxy; not
Mosaic Gateway egress and not an approved provider integration.
The reviewed path runs GPT models inside the Claude Code harness through a
local `claude-code-proxy` that translates Anthropic Messages traffic to a
ChatGPT-subscription (Codex OAuth) backend. It is intended for evaluation and
must not be described as a current Mosaic model gateway, an `IProviderAdapter`
implementation, or an `AgentRuntimeProvider` implementation.
Its constraints remain important: it is not a multi-tenant control plane, it
must not own connector leasing or canonical handoff, and local proxy
credentials and listener exposure require isolation. The current Mosaic
launcher documents its experimental status and isolated Claude configuration:
- [`mosaic` claudex documentation](../../../../packages/mosaic/README.md)
- [`claudex` launch composition](../../../../packages/mosaic/src/commands/claudex.ts)
- [`claude-code-proxy`](https://github.com/raine/claude-code-proxy)
### `teremterem/claude-code-gpt-5-codex`
**Disposition:** Historical recipe; not selected and not integrated.
The reviewed repository uses `OPENAI_API_KEY`, tells previously authenticated
Claude users to log out, and documents a Claude Web Search schema
incompatibility. That does not satisfy the subscription-OAuth plus built-in-
channel continuity requirement observed in the Mos cutover.
Source references:
- [Repository](https://github.com/teremterem/claude-code-gpt-5-codex)
- [Environment template](https://github.com/teremterem/claude-code-gpt-5-codex/blob/main/.env.template)
## Prerequisites before approval or integration
No candidate may be integrated, enabled as a Mosaic feature, or treated as a
current supported path until all of the following are complete for the exact
provider, adapter, configuration, and deployed revision.
### 1. Recorded approval and governance
- An architecture decision explicitly approves the model-egress scope and
records that `IProviderAdapter` remains separate from `AgentRuntimeProvider`.
- Product/operations ownership, provider terms review, and independent security
review are recorded against the exact change and provider revision.
- The approval identifies allowed providers, models, regions, data handling,
rollback owner, support status, and a time-bounded experimental or production
phase. This RFC itself is not that approval.
- No candidate is advertised as integrated, current, or production-ready while
the approval record is absent or expired.
### 2. Security and credential controls
- A threat model covers prompt/tool data, model output, streaming, cancellation,
retries, failover, SSRF, endpoint authentication, TLS, egress allowlists,
supply-chain risk, provider terms, and denial-of-service behavior.
- OAuth tokens, API keys, virtual keys, refresh tokens, and proxy credentials
have documented ownership, storage, encryption, rotation, revocation,
expiry, least-privilege scope, and incident-response procedures. Secrets do
not enter prompts, logs, traces, audit payloads, or commits.
- Request, response, tool-schema, and error telemetry is classified and
redacted before persistence or external channel delivery. Retention and
deletion are tenant-scoped.
- The adapter fails closed on missing, expired, revoked, malformed, or
unauthorized credentials and on uncertain provider health. Loopback-only
experimental proxies remain process-isolated and cannot become a gateway
bypass.
### 3. Actor and tenant isolation
- Actor and tenant identity are derived from authenticated Mosaic context at
the Gateway; callers cannot select a tenant, logical agent, credential, or
provider principal by supplying an ID.
- Downstream virtual keys and provider account identifiers are mapped to
server-owned tenant policy. They never grant Mosaic authorization and never
replace RBAC, approvals, connector leases, or runtime scope.
- Model configuration, budgets, rate limits, data residency, credential use,
logs, caches, and failure handling are isolated per tenant. Cross-tenant
reads, writes, cache hits, telemetry, and failover are denied and tested.
- The egress adapter receives only the minimum scoped request needed for the
authorized model operation; channel ingress and runtime/session control stay
in Mosaic-owned boundaries.
### 4. Idempotency and side-effect safety
- Mosaic assigns a durable operation ID and idempotency key before an egress
request can cause a tool or external side effect. The provider gateway must
preserve the correlation and idempotency metadata or be wrapped by an
adapter that does so.
- Durable receipts record request, attempt, provider, outcome, and replay state
without storing unredacted content. Retries and failover are allowed only
when the operation contract proves they cannot duplicate a side effect.
- Ambiguous timeout, disconnect, stream-resume, cancellation, and provider
failover outcomes fail closed until the receipt is reconciled. The egress
gateway must not claim exactly-once behavior that Mosaic has not proven.
- Failure injection demonstrates no duplicate tool, connector, channel, or
external side effect across gateway, adapter, proxy, and provider retries.
### 5. Contract and rollback qualification
- Separate contract tests pass for `IProviderAdapter` model registration,
health, streaming, tools, reasoning controls, cancellation, errors, and
audit correlation.
- Separate `AgentRuntimeProvider` contract tests continue to pass for runtime
capability negotiation, session ownership, approval, attach/detach,
termination, and normalized events. A model-egress candidate must not be
used as evidence for runtime/session compatibility.
- A verified rollback to the prior model path is exercised, including revoked
credentials, unhealthy providers, partial streams, and an adapter version
mismatch.
- The exact deployed revision receives independent code and security review,
with terminal-green CI and operator recovery evidence before any activation.
## Security consequences
- Subscription OAuth grants are high-value credentials and require the same
lifecycle controls as service credentials.
- Downstream virtual keys reduce provider-key exposure but do not establish
user, tenant, agent, or runtime authority.
- Automatic retry or failover can duplicate tool and external side effects
unless Mosaic owns operation IDs, durable receipts, and reconciliation.
- Gateway telemetry can contain prompts, tool schemas, and model output;
redaction and retention policy must apply before persistence.
- A localhost translation endpoint must remain loopback-only, process-isolated,
authenticated where applicable, and outside the Mosaic Gateway ingress path.
- A model-egress gateway outage must not weaken Mosaic authorization, lease
fencing, tenant isolation, approval, or runtime-session boundaries.
## Acceptance before any production use
The following are minimum exit criteria for a future, separately approved
implementation; they are not satisfied by this RFC:
1. The recorded approval and provider-terms review cover the exact integration.
2. Credential lifecycle, revocation, rotation, redaction, and incident drills
are documented and exercised.
3. Tenant-bound authorization remains entirely in Mosaic Gateway and passes
cross-tenant negative tests.
4. `IProviderAdapter` contract tests pass without treating
`AgentRuntimeProvider` tests as substitutes.
5. Failure injection proves no duplicate side effects across retries or
provider failover, with durable idempotency receipts.
6. Streaming, tools, cancellation, reasoning policy, errors, and audit
correlation are qualified for the exact model path.
7. Rollback to the prior provider path is exercised and operator-verifiable.
8. Independent code and security reviews approve the exact deployed revision.
## Follow-up
- #754 owns cross-runtime logical identity, checkpoint, receipt, adapter, and
failover work.
- #755 / PR #757 implements the first logical identity and connector
lease/fencing boundary.
- A later issue may prototype LiteLLM or Bifrost behind the model-provider
boundary only after the recorded approval, security, tenant, idempotency,
contract, and rollback prerequisites are met.
- The page remains **Draft / proposed** until an explicit decision changes its
status. Until then, it must not be cited as an approved architecture,
current integration, or operational runbook.
@@ -0,0 +1,181 @@
# Channel adapters
> **Status:** Current shared channel types plus the Discord reference/compatibility implementation. A shared gateway adapter registry, Telegram parity, and Matrix channel integration remain unimplemented or unproven.
>
> **Last verified:** 2026-08-10 against the source and focused tests listed in [Evidence](#evidence).
>
> **Audience:** Developers implementing or reviewing channel integrations.
The gateway remains the policy and runtime boundary. Channel code translates native events, applies its native admission checks, and delivers normalized ingress/egress; it must not choose a provider, harness, process, or native runtime session on behalf of the gateway.
## Current source boundaries
| Boundary | Current authority | Current claim |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Shared contract | [`packages/types/src/channel/channel.dto.ts`](../../../packages/types/src/channel/channel.dto.ts), [`channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts) | DTOs, ports, lifecycle health, and delivery error codes exported by `@mosaicstack/types`. |
| Discord translation | [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) | First adapter implementing the shared lifecycle/egress seam, with an optional direct ingress port and a tested Socket.IO compatibility path. |
| Discord behavior | [`plugins/discord/src/index.test.ts`](../../../plugins/discord/src/index.test.ts) | Current allowlist, pairing, role, thread, route, attachment, replay-envelope, egress, retry, and health behavior. |
| Gateway compatibility | [`apps/gateway/src/chat/chat.gateway.ts`](../../../apps/gateway/src/chat/chat.gateway.ts), [`chat.gateway-auth.ts`](../../../apps/gateway/src/chat/chat.gateway-auth.ts) | `/chat` Socket.IO service/session authentication, signed Discord envelope validation, trusted binding selection, raw chat dispatch, and raw stream egress. |
| Host registry | [`apps/gateway/src/plugin/plugin.interface.ts`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) | Lifecycle-only `IChannelPlugin[]` hosting. This is not a universal `OfficialChannelAdapter` registry. |
| Telegram | [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts), [`package.json`](../../../plugins/telegram/package.json) | Raw legacy Telegraf/Socket.IO behavior only; no shared-contract or authenticated gateway parity. |
| Matrix | No current channel adapter boundary in the cited implementation | Matrix-related fleet/runtime code is not evidence of a gateway channel adapter. |
Executable source and tests outrank older architecture or quarantine pages. The canonical architecture summary is [`architecture/channel-protocol.md`](../architecture/channel-protocol.md).
## Shared contract
`@mosaicstack/types` currently exports the channel types through [`packages/types/src/channel/index.ts`](../../../packages/types/src/channel/index.ts) and [`packages/types/src/index.ts`](../../../packages/types/src/index.ts).
The lifecycle and port seams are:
```typescript
interface OfficialChannelAdapter {
readonly name: string;
start(): Promise<void>;
stop(): Promise<void>;
health(): Promise<ChannelAdapterHealthDto>;
}
interface ChannelIngressPort {
receive(ingress: ChannelIngressDto): Promise<void>;
}
interface ChannelEgressPort {
send(egress: ChannelEgressDto): Promise<void>;
}
```
The current DTO boundary includes:
- `ChannelMessageDto` for normalized native messages and JSON-safe metadata;
- `ChannelAttachmentDto` for bounded external attachment references;
- `ChannelAuthorizedPrincipalDto` for the already-authorized channel actor and `viewer`/`operator`/`admin` role;
- `ChannelBindingDto` for configuration-owned workspace/channel/logical-agent binding;
- `ChannelResponseTargetDto` for a channel and optional thread;
- `ChannelConversationRouteDto` for binding, logical agent, stable conversation ID, authorization channel, and response target;
- `ChannelIngressDto` for correlation, native message ID, operation, principal, message, and route; and
- `ChannelEgressDto` for correlation, normalized output, and route.
Current operations are `message.send`, `approval.create`, and `session.stop`. Current delivery errors are `invalid_route`, `destination_unavailable`, and `delivery_failed`; there is no shared revoked-auth error code or executable protocol-version contract in this surface.
### Stable route rule
The Discord adapter derives:
```text
<logical-agent-id>:discord:<response-channel-id>
```
The binding address is derived from the configured guild, parent channel, and logical-agent instance. The route intentionally omits runtime provider, harness, model, process, and native runtime-session identifiers. Gateway provider/runtime layers own those identities; durable-session ownership applies only after explicit enrollment and is not implied by the external route.
This is a route-integrity rule, not proof that the gateway has a universal channel session API. A future adapter must derive its route from trusted configuration and native channel/thread identity; it must not accept a caller-selected logical agent or runtime target.
## Current Discord implementation
### Native ingress
`DiscordPlugin` currently:
1. ignores bot-authored and non-guild messages;
2. resolves a thread's actual parent text channel, while leaving normal category parents out of authorization;
3. applies default-deny guild, parent-channel, and user allowlists;
4. resolves configuration-owned `instanceId`/`agentConfigId` bindings and paired-user roles before thread creation or dispatch;
5. applies message and mention-thread limits before side effects;
6. creates/reuses a mention thread or preserves an existing thread target; and
7. maps the event to a `ChannelIngressDto` when an `ingressPort` dependency is supplied.
The normalized message uses `channelName: "discord"`, the response target as `channelId`, `senderKind: "user"`, `markdown` for non-empty text, `image`/`file` for attachment-only input, mapped attachments, and metadata containing the native channel message ID and guild ID. It does not currently claim a universal metadata shape for mentions, embeds, channel type, or replies.
### Socket.IO compatibility path
The gateway-hosted plugin is currently constructed without a direct `ChannelIngressPort` in [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), so it uses the established compatibility path:
1. `start()` connects to `${DISCORD_GATEWAY_URL}/chat` with `auth.discordServiceToken`.
2. Normal sends emit a signed `message` envelope; approval and stop emit `discord:approve` and `discord:stop` envelopes.
3. The HMAC-SHA-256 signature covers the ordered Discord payload using `DISCORD_SERVICE_TOKEN`.
4. The gateway verifies the service token/signature, allowlists, binding/operation role, stable conversation ID, attachment bounds, and replay key before dispatch.
5. The gateway selects the binding's trusted `agentConfigId` and verifies its name equals the logical-agent instance. It attempts normal conversation binding/persistence before dispatch, but the external route is not a UUID and no route-to-UUID mapping currently proves durable ordinary-chat history.
6. Agent output currently returns as raw Socket.IO `agent:start`, `agent:text`, and `agent:end` events. The plugin buffers the text and calls its typed Discord egress at stream end.
This compatibility path carries enough normalized identity to preserve current security and routing behavior, but it is not a gateway-produced `ChannelIngressDto`/`ChannelEgressDto` flow through a shared host registry. The gateway can continue live dispatch after persistence failure; adapter authors must not claim durable continuity until external routes map to UUID conversations and fresh-message restart tests pass.
### Egress and health
`DiscordPlugin.send()` is a typed `ChannelEgressPort` implementation. It validates the route and message alignment before destination lookup, sends at a 1,900-character boundary, retries transient 429/5xx/network failures up to three times, uses one deterministic enforced nonce per correlation/chunk, and does not retry permanent failures. It reports `connected`, `degraded`, or `disconnected` from Discord client readiness and gateway socket connectivity.
The host wrapper currently exposes only `name`, `start`, `stop`, and optional project provisioning. It does not expose `health()`, inject the direct ingress port, produce `ChannelEgressDto` values from the gateway, or provide `healthAll`/`getAdapter` semantics.
## Adapter authoring boundary
For a future official adapter, preserve these current architectural boundaries:
1. Normalize native messages to the shared DTOs and preserve native message ID, correlation ID, channel/thread identity, attachments, and response target.
2. Enforce native guild/room/channel/user/pairing/role policy before thread/room creation or gateway dispatch.
3. Select the logical agent from trusted configuration; never from a caller-controlled provider, model, harness, or route field.
4. Keep the route stable when the gateway changes runtime provider or harness.
5. Validate egress route and destination before sending; bound chunks, retries, and attachment metadata.
6. Return sanitized errors and expose non-throwing lifecycle health for ordinary disconnected state.
7. Add happy-path and failure-path tests for authorization ordering, replay/idempotency, route integrity, native side effects, egress, reconnect, and health.
These are implementation constraints for future work, not evidence that the missing shared registry already exists.
## Parity status
### Telegram: raw legacy adapter, not shared parity
The current Telegram source:
- launches Telegraf and a Socket.IO client;
- reads `TELEGRAM_BOT_TOKEN` and `TELEGRAM_GATEWAY_URL` through the gateway plugin factory, whose URL default is `http://localhost:14242`;
- accepts text messages only and ignores attachment-only messages;
- maps each Telegram `chat.id` to `telegram-<chatId>`;
- emits a raw `{ conversationId, content, role: "user" }` object rather than `ChannelIngressDto`;
- has no shared DTO import, channel binding, principal/role policy, native message ID, attachment mapping, route-safe egress, or health method; and
- has no package test file in this checkout; its script is `vitest run --passWithNoTests`.
Its Socket.IO connection does not send the Discord service token or a BetterAuth session. A configured `TELEGRAM_BOT_TOKEN` therefore must not be described as an authenticated official channel. Shared Telegram parity requires a separate implementation and focused security/contract tests.
### Matrix: no current channel adapter
No current gateway adapter, shared-port wiring, channel binding, identity resolver, persistence boundary, authentication path, or focused channel test establishes Matrix as a Mosaic channel. Matrix code elsewhere in the repository belongs to other transport/runtime work and must not be promoted into channel-adapter instructions without a separate contract and evidence.
### Shared registry: draft/unimplemented
The current `PLUGIN_REGISTRY` is an array of `IChannelPlugin` lifecycle wrappers. It is not a registry of `OfficialChannelAdapter` instances and does not inject `ChannelIngressPort`/`ChannelEgressPort`, aggregate adapter health, or remove the gateway's Discord-specific auth/envelope/approval/stop/replay branches.
A future registry must specify binding and credential ownership, ingress/egress injection, health/error semantics, compatibility with the existing Socket.IO clients, and tests proving that adapters cannot bypass gateway authorization or route validation before it can be documented as current architecture.
## Safe verification commands
The focused package commands used by the current Discord evidence are:
```bash
pnpm --filter @mosaicstack/types build
pnpm --filter @mosaicstack/types typecheck
pnpm --filter @mosaicstack/discord-plugin typecheck
pnpm --filter @mosaicstack/discord-plugin lint
pnpm --filter @mosaicstack/discord-plugin test
cd apps/gateway && pnpm exec vitest run \
src/plugin/discord-ingress.security.spec.ts \
src/chat/chat.gateway-redaction.spec.ts \
src/__tests__/integration/tess-cross-surface.integration.test.ts
```
These commands do not require starting Gateway, Discord, Telegram, Matrix, a queue, or a database. The Discord package test includes its configured coverage thresholds; the gateway command is a focused Vitest run rather than a claim of full repository integration coverage.
## Evidence
- [`packages/types/src/channel/channel.dto.ts`](../../../packages/types/src/channel/channel.dto.ts) — shared DTOs, operations, route fields, and metadata types.
- [`packages/types/src/channel/channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts) — adapter lifecycle, ingress/egress ports, and delivery errors.
- [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) — native translation, auth ordering, compatibility envelope, route-safe egress, retry, and health.
- [`plugins/discord/src/index.test.ts`](../../../plugins/discord/src/index.test.ts) — focused Discord contract and behavior tests.
- [`apps/gateway/src/chat/chat.gateway.ts`](../../../apps/gateway/src/chat/chat.gateway.ts) — service/session auth, signed envelope validation, replay, trusted agent selection, and raw stream events.
- [`apps/gateway/src/chat/chat.gateway-auth.ts`](../../../apps/gateway/src/chat/chat.gateway-auth.ts) — timing-safe Discord service-token and BetterAuth session checks.
- [`apps/gateway/src/plugin/discord-ingress.security.spec.ts`](../../../apps/gateway/src/plugin/discord-ingress.security.spec.ts) — gateway security and privileged-operation tests.
- [`apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts`](../../../apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts) — control-flow test with explicit durable-session pre-enrollment; not ordinary Discord persistence evidence.
- [`apps/gateway/src/plugin/plugin.interface.ts`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), and [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) — lifecycle-only host registry.
- [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts) and [`plugins/telegram/package.json`](../../../plugins/telegram/package.json) — raw Telegram behavior and no-test script.
- [Canonical channel protocol architecture](../architecture/channel-protocol.md) — shared contract and explicit current/draft boundary.
- [Discord ingress security](../../ADMIN-GUIDE/security/discord-ingress.md) — administrator-facing config and auth claims.
- [Discord conversations](../../USER-GUIDE/workflows/discord-conversations.md) — user-facing routing behavior.
- [Developer Guide](../README.md)
@@ -0,0 +1,292 @@
---
title: Lease-broker operations
type: runbook
audience: developer
status: current
source_of_truth: false
---
# Lease-broker operations
> **Status:** Current for repository inspection and test verification. Live broker
> startup, recovery, lease mutation, service management, and cleanup remain
> **held/non-operative**.
>
> **Operational authority:** This page authorizes only the non-mutating
> inspection and test commands in [Safe static inspection](#safe-static-inspection).
> It does not authorize starting a daemon or systemd unit, connecting to a live
> socket, invoking recovery, promoting or revoking a lease, executing a
> consequential runtime tool, or changing a database.
The lease broker is a Linux-only internal process boundary. Its executable
behavior is authoritative in the [shipped broker daemon](../../../packages/mosaic/framework/tools/lease-broker/daemon.py)
and tests, not in this page. The current architecture references are:
- [Broker protocol](../architecture/lease-broker-protocol.md) — framing,
kernel identity, ancestry, generations, persistence, and state transitions.
- [Lease-broker security](../architecture/lease-broker-security.md) — trust
boundaries, filesystem hardening, observer behavior, and residuals.
- [Whole mutator-class gate](../architecture/mutator-class-gate.md) — default
deny, launch choke points, and broker-owned promotion order.
- [Compaction revocation](../architecture/compaction-revocation.md) — Claude
and Pi lifecycle observers, generation fencing, and the bounded residual.
## Safe static inspection
These are the only operative commands documented here. They inspect checked-in
files or run isolated tests; they do not start a user service, activate a
runtime, connect to PostgreSQL, or mutate repository/product state.
### Source and launch inventory
From the repository root, inspect the current implementation and its permanent
runtime-launch inventory:
```bash
find packages/mosaic/framework/tools/lease-broker packages/mosaic/src/lease-broker packages/mosaic/src/mutator-gate -maxdepth 1 -type f -print | sort
python3 packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py --root . --json
```
The launch inventory is a static completeness guard. A clean result means the
checked-in production launch sites are classified by the guard; it does not
prove that a broker, runtime, or service is running.
### Safe unit and static contract tests
The focused standard-library tests can be run directly:
```bash
python3 packages/mosaic/src/lease-broker/daemon_deadline_unittest.py
python3 packages/mosaic/src/lease-broker/normative_fragments_unittest.py
python3 packages/mosaic/src/lease-broker/receipt_challenge_unittest.py
python3 packages/mosaic/src/lease-broker/context_recovery_unittest.py
python3 packages/mosaic/src/lease-broker/state_store_unittest.py
python3 packages/mosaic/src/lease-broker/framework_skill_portability_unittest.py
python3 packages/mosaic/src/mutator-gate/runtime_tools_unittest.py
python3 packages/mosaic/src/mutator-gate/runtime_launch_guard_unittest.py
python3 packages/mosaic/src/mutator-gate/version_coupling_unittest.py
```
`recovery_runtime_unittest.py` and `recovery_b1_adversarial_unittest.py` are
also safe when run as tests: they use private temporary daemons, sockets, and
fixtures, never the installed user service or a model stream. They are not
operator recovery instructions:
```bash
python3 packages/mosaic/src/lease-broker/recovery_runtime_unittest.py
python3 packages/mosaic/src/lease-broker/recovery_b1_adversarial_unittest.py
```
The [lease-broker Vitest acceptance suite](../../../packages/mosaic/src/lease-broker/lease-broker.acceptance.spec.ts)
and [mutator-gate Vitest acceptance suite](../../../packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts)
provide additional private-fixture coverage. Test-created child processes and
Unix sockets are disposable test fixtures, not live-service authority.
## Current implementation facts
### Protected paths and persistence
The broker and supervisor source establish these invariants:
| Object | Current contract |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Parent directory | The parent containing the socket and state must already exist and have exactly mode `0700`. |
| Broker socket | The daemon refuses an existing path or symlink, binds a new Unix socket, sets it to `0600`, and removes only the inode it created during normal shutdown. |
| State file | A present state file must be a regular, non-symlink file protected as mode `0600`, no larger than 4 MiB, and valid state-version-1 JSON. Corrupt, incompatible, oversized, or unsafe state refuses startup. |
| State writes | Changes are serialized through a mode-`0600` temporary file, complete-write loop, `fsync`, atomic replace, and parent-directory `fsync`. Post-replace durability uncertainty poisons the store and terminates service processing. |
| Volatile authority | `VERIFIED` leases are not restored as live authority after broker restart. Persisted session identity and valid pending-token state are separate from volatile lease state. |
| Runtime generation | `launch-runtime.py` creates `generation-<broker-session>.state` beside the socket. It is an owner-only, locked, monotonic generation file; unsafe, non-regular, oversized, or non-private state fails closed. |
The resolved socket path is, in order: explicit
`MOSAIC_LEASE_BROKER_SOCKET`, `$XDG_RUNTIME_DIR/mosaic-lease/broker.sock`, or
`/run/user/<uid>/mosaic-lease/broker.sock`. The state file is `state.json` next
to that resolved socket. The production observer transport is a separate
`receipt-observer.sock` by default; `--test-observer-file` is a private test
fixture option, not a production deployment path.
The supervisor implementation can materialize a user unit, wrapper, and
co-located daemon sources under caller-supplied paths, but
[`applyBrokerSupervisor`](../../../packages/mosaic/src/lease-broker/broker-supervisor.ts)
never runs `systemctl`, starts `daemon.py`, or enables the unit. The checked-in
unit and [`start-lease-broker.sh`](../../../packages/mosaic/framework/tools/lease-broker/start-lease-broker.sh)
are therefore implementation inputs, not live activation authority for this
page.
### Protocol and identity boundary
The protocol accepts one UTF-8 JSON object followed by one newline, capped at
64 KiB. The client must half-close its write side after the newline and before
waiting for the response (`shutdown(SHUT_WR)` for POSIX clients or
`socket.end()` for Node). Unterminated, multiple, delayed-second, malformed,
oversized, or deadline-exceeded requests fail closed. This is an internal Unix
socket protocol, not an HTTP/OpenAPI endpoint.
The broker obtains `(pid, uid, gid)` from kernel `SO_PEERCRED`, binds the
session to the anchor's `/proc/<pid>/stat` starttime, and revalidates every
walked ancestor's starttime. A caller cannot choose `session_id`; a sibling or
unrelated process cannot authenticate with another process's session. A higher
runtime generation replaces the prior incarnation and revokes its tokens and
lease authority; a lower generation is stale.
### Lease and tool authorization
The broker is the sole lease writer. The effective default-deny policy is:
- Claude read-only classes: `Read`, `Grep`, `Glob`, `Ls`, and `Find`.
- Pi read-only classes: `read`, `grep`, `find`, and `ls`.
- Both runtimes expose the fixed `mosaic_context_recover` identity as the
constrained recovery exception.
- Every other built-in, unknown, custom, MCP, shell, edit, write, deployment,
provider, or filesystem mutator is consequential and is denied while the
session is not `VERIFIED`.
The normal broker transition is revoke-first and promote-last:
1. `begin_verification` authenticates the broker-minted session and current
generation, revokes existing authority and pending tokens, validates the
exact source construction and binding, and enters a pending state.
2. The receipt challenge and binding are broker-generated. A trusted observer
must provide the exact current-cycle assistant entry; caller-supplied
`latest_assistant_message` is rejected on the public broker socket.
3. Promotion consumes the evidence-backed one-time token before volatile
`VERIFIED` becomes visible. A token or receipt cannot be replayed against a
later cycle.
4. `revoke_lease`, generation replacement, broker restart, or monotonic TTL
expiry removes consequential-tool authority. TTL is positive and capped at
300 seconds.
`launch-runtime.py` is the register-before-`exec` choke point for Claude and
Pi. It performs the activation-capability version check, registers the anchor,
creates the private generation file, exports the broker identity to descendants,
and only then executes the requested runtime. Registration, capability,
generation-file, broker-reply, or `exec` failure denies launch. Claude's raw
`--dangerously-skip-permissions` flag is owned by this wrapper; callers request
only its semantic dangerous mode.
Claude's all-tools `PreToolUse` hook and Pi's `tool_call` handler submit the
runtime-reported tool name to the gate. The gate does not inspect a shell string
to decide that one command is safe. Missing identity, malformed input or reply,
timeout, broker unavailability, unsafe generation state, and denial all fail
closed.
### Lifecycle and recovery boundaries
Claude and Pi lifecycle observers use the same authenticated session and
broker state machine. Compaction revocation and same-PID replacement behavior
are summarized below:
| Runtime event | Current source-backed behavior |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Claude `PreCompact` | Revoke before compaction; a failed hook blocks the lifecycle transition. |
| Claude `SessionStart` matcher `compact` | Revoke again after compacted context starts. |
| Claude `SessionStart` matcher `resume\|clear` | Bump the private generation, then revoke the replacement incarnation. |
| Pi `session_before_compact` | Revoke before compaction; failure cancels the transition. |
| Pi `session_compact` then first `context` | Run one independent post-compaction revoke; a failed post-observer locally blocks later tools until retry. |
| Pi `session_start` reason `reload`, `new`, `resume`, or `fork` | Bump the private generation before revoking and reusing the replacement session. |
| Fired observer with unavailable broker | Advance the private generation as a local fence and return failure; do not continue consequential work. |
The [source-resident context-refresh skill](../../../packages/mosaic/framework/skills/mosaic-context-refresh/SKILL.md)
is a thin adapter over `recover-context.py`. Recovery begins with a validated
normative-fragment construction, broker-side revoke-first transition, and a
fresh `PENDING_DELIVERY` challenge. The trusted observer records only finalized
Claude Stop-hook or Pi `message_end` content. Completion supplies no receipt or
challenge argument; the broker obtains its current challenge, checks the exact
observed entry, consumes evidence, and promotes last. Recovery does not accept
normal-path receipt text as replayable authority.
A receipt is only a delivery/liveness prerequisite. It is not a safety,
obedience, comprehension, or residency proof. Absent, malformed,
prefix-truncated, and observably mutated terminal receipts do not promote. A
tail-preserving middle drop is explicitly not receipt-detectable and remains a
server-side/T-C residual.
### Named residual
If both compaction observers are missed while a lease remains unexpired,
consequential tools are **ALLOWED** inside the bounded residual stale window.
The mutator gate makes no within-window action-count or timing claim. After the
monotonic TTL expires, the next consequential tool is **DENIED**. This is
separate from a fired observer that cannot reach the broker, which fails closed
through lifecycle cancellation, a generation fence, and/or the Pi local latch.
Protected-branch controls and required review/CI remain the irreducible
server-side backstop for the T-C total-hook-miss boundary.
## Held future procedure — non-operative
There is no current command authority for the following live procedures. The
sequence below records source-backed intent for a separately approved
activation/recovery work package; it must not be copied into an operator shell.
### Startup and restart outline (held)
1. An activation owner would first materialize the exact framework unit,
wrapper, and co-located source copies, then verify the resolved parent,
socket, state, observer, and generation paths and their no-symlink/private
posture.
2. The approved supervisor would start the daemon only after confirming that
the exact socket path is not owned by another process. `READY` from the
daemon and a live Unix socket would be health evidence; a unit file alone
would not be healthy.
3. A broker crash would require preserving the state file, identifying the
socket owner, and making an explicit restart decision. Restart intentionally
clears volatile `VERIFIED` leases; it is not a way to restore authority.
4. A leftover socket would be handled only after the owning service is
confirmed stopped and the exact path is deliberately reviewed. This page
supplies no deletion, `systemctl`, enablement, or start command.
The source's `daemon.py` argument parser and the user unit show how a future
activation is wired, but neither source file grants this page authority to
invoke that wiring. `applyBrokerSupervisor` is materialization only; the
supervisor source explicitly leaves enable/start as a separate held step.
### Recovery outline (held)
The future adapter flow is: runtime supplies validated construction and current
non-negative epochs; broker performs `begin_recovery` revoke-first and returns
one fresh receipt; the adapter delivers that exact receipt; the authenticated
observer records the finalized assistant entry; and the adapter requests
completion without presenting receipt text or a challenge. Any absent,
malformed, stale, duplicated, or mismatched observation leaves the session
`UNVERIFIED`; retry starts a new recovery cycle.
Claude's adapter is restricted to the exact literal recovery argv shape checked
by the gate. Pi uses only the registered `mosaic_context_recover` tool; Pi
`bash` and all other tools remain gated. Do not manually invoke the revoker to
restore authority, send assistant text through the public broker request, or
reuse a normal-path receipt. The [context-refresh skill](../../../packages/mosaic/framework/skills/mosaic-context-refresh/SKILL.md)
and its [runtime boundary tests](../../../packages/mosaic/src/lease-broker/recovery_runtime_unittest.py)
are references for future adapter qualification, not an active operator route.
### Lease, mutator, and incident handling outline (held)
- There is no supported operator CLI or HTTP endpoint for sending raw
`begin_verification`, `promote_lease`, `revoke_lease`, or `authorize_tool`
requests. Do not hand-craft JSON frames, mint tokens, replay receipts, or
treat a successful read-only authorization as a promotion.
- After a runtime exits, its generation file may be removed only after an
approved check establishes that no process for that broker-minted session
remains. Retain stale files during incident analysis; they carry no lease
authority by themselves.
- Corrupt, oversized, symlinked, or non-regular state must be preserved for
review and not overwritten in place. Establishing new state is an explicit
operational decision that invalidates prior sessions and tokens; no recovery
command is supplied here.
- Directory `0700` and socket/state `0600` are same-principal hardening only.
They do not prevent the same UID from unlinking and replacing a socket. A
stronger deployment needs an external protected proxy, ACL, or service
boundary that preserves the peer identity required by `SO_PEERCRED` and
ancestry checks. No such deployment procedure is current here.
## Related source and tests
- [Broker daemon](../../../packages/mosaic/framework/tools/lease-broker/daemon.py)
- [Register-and-exec launcher](../../../packages/mosaic/framework/tools/lease-broker/launch-runtime.py)
- [Mutator gate](../../../packages/mosaic/framework/tools/lease-broker/mutator-gate.py)
- [Recovery command](../../../packages/mosaic/framework/tools/lease-broker/recover-context.py)
- [Lease broker acceptance tests](../../../packages/mosaic/src/lease-broker/lease-broker.acceptance.spec.ts)
- [Mutator gate acceptance tests](../../../packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts)
- [Pi lifecycle tests](../../../packages/mosaic/src/mutator-gate/pi-compaction-lifecycle.spec.ts)
This migration changes documentation placement and verified wording only. It does
not start or change the broker, a runtime, systemd, PostgreSQL, or any other
service.