style(rfc): apply repo prettier formatting to the two RFCs
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Whitespace/table-padding/emphasis-marker-style only (prettier --write),
to satisfy pnpm format:check (globs **/*.md). No wording, numbers,
[VERIFY] flags, or disclaimers changed — verified against the source
files line-for-line modulo formatting.
This commit is contained in:
mosaic-coder
2026-07-24 19:39:57 -05:00
parent 01dd2fadb3
commit 5555cc7595
2 changed files with 274 additions and 207 deletions

View File

@@ -25,7 +25,7 @@ The **first standalone shippable slice is presence** (P1).
### 1.1 Goals
- **G1 — Presence & liveness first.** A Team Lead must be able to answer "is my coordinator online, away, or dead?" in seconds, not by polling for 13 hours. Presence is the P1 slice and ships before anything else.
- **G2 — A native backbone we own.** Replace *Hermes-as-backbone* with a self-hosted Synapse + a Mosaic-controlled appservice. External chat bridging becomes an optional edge, not the spine.
- **G2 — A native backbone we own.** Replace _Hermes-as-backbone_ with a self-hosted Synapse + a Mosaic-controlled appservice. External chat bridging becomes an optional edge, not the spine.
- **G3 — Turnkey harness enrollment.** `mosaic enroll` auto-detects the harness and self-registers the agent via the appservice on spin. No hand-rolled per-bot identity juggling.
- **G4 — A real protocol (MACP v1).** Structured, versioned event schema over Matrix custom event types; a documented routing contract for tmux vs Matrix; a documented escalation policy.
- **G5 — No central SPOF.** Per-site homeservers federated over TLS/DNS we already control, so one site going dark cannot take the fleet's comms with it.
@@ -34,11 +34,11 @@ The **first standalone shippable slice is presence** (P1).
### 1.2 Non-Goals
- **NG1 — Do NOT rip out working comms mid-MVP.** tmux fast-path and the existing `mos-comms` git-branch channel keep working until their replacement is proven at parity. This RFC is strangler, not big-bang.
- **NG2 — tmux is NOT being replaced.** tmux inter-agent comms remains **P0**. Matrix is *above* it, not instead of it. MACP defines the boundary; it does not move it.
- **NG3 — Not adopting Buzz/Nostr as transport.** We adopt Buzz's *patterns*; the wire is Matrix.
- **NG2 — tmux is NOT being replaced.** tmux inter-agent comms remains **P0**. Matrix is _above_ it, not instead of it. MACP defines the boundary; it does not move it.
- **NG3 — Not adopting Buzz/Nostr as transport.** We adopt Buzz's _patterns_; the wire is Matrix.
- **NG4 — Not building a new chat client in P1P4.** HIL uses an existing Matrix client (Element or equivalent) until/unless a custom client is justified (open question, §11).
- **NG5 — Not federating to the public Matrix network.** Federation is Mosaic-site-to-Mosaic-site over infrastructure we control. Public `matrix.org` federation is out of scope (and should likely be firewalled off).
- **NG6 — Not a Hermes feature-clone.** We reach *parity on the channels that matter* (§9 checklist), not bug-for-bug Hermes compatibility.
- **NG6 — Not a Hermes feature-clone.** We reach _parity on the channels that matter_ (§9 checklist), not bug-for-bug Hermes compatibility.
---
@@ -116,7 +116,7 @@ Orchestrator Harness (mosaic enroll) Mosaic Appservice
Notes on the steps that matter:
- **Step 4/5** use the **Application Service API**: the appservice can register users inside its namespace (`@mosaic_*:site`) and act on their behalf. Two viable modes: (a) mint a real per-agent `access_token` via appservice login, or (b) have the appservice **masquerade** using `user_id` query param on C-S calls with the `as_token`. **Recommendation: mint per-agent tokens** for P2 so the agent process holds only its own credential (blast-radius containment, §8); reserve masquerade for bulk/bootstrap operations the appservice itself performs. **[VERIFY]** exact token-lifetime and refresh behavior against the running Synapse version.
- **Step 9** — the introduction is both a human-readable `m.room.message` *and* a structured `mosaic.introduction` custom event (so other agents can machine-parse capabilities without scraping prose).
- **Step 9** — the introduction is both a human-readable `m.room.message` _and_ a structured `mosaic.introduction` custom event (so other agents can machine-parse capabilities without scraping prose).
- **Step 10/11** — presence goes online immediately, then a **heartbeat** keeps liveness fresh. Native Matrix presence auto-decays to `unavailable`/`offline`, but we do **not** rely solely on it (Synapse presence timeouts are coarse and federation presence is lossy **[VERIFY]**); MACP adds an explicit `mosaic.presence` heartbeat event for deterministic liveness (§4.5, §5).
---
@@ -125,19 +125,19 @@ Notes on the steps that matter:
The core tension: **product monorepo** (`mosaicstack/stack`, this checkout `/src/mosaic-stack`) vs **framework** (`~/.config/mosaic`, the agent/harness runtime that every agent shares regardless of product). The boundary rule I am ratifying:
> **Product-monorepo owns the deployed *services and libraries*. Framework owns the *agent/harness contract* — anything an agent needs the moment it spins, before any product code is checked out.**
> **Product-monorepo owns the deployed _services and libraries_. Framework owns the _agent/harness contract_ — anything an agent needs the moment it spins, before any product code is checked out.**
Applying that rule:
| Piece | Home | Rationale |
|---|---|---|
| **Synapse deployment** (compose/helm, config, TLS, `.well-known`, pipelines) | **Product monorepo**`infra/matrix/` + CI in the monorepo's pipeline dir | It is deployed infrastructure with the same lifecycle/observability as Gateway/Web/DB. Lives beside other `infra/` and Woodpecker pipelines. |
| **Mosaic Appservice** | **Product monorepo**`apps/matrix-appservice` | It is a first-class deployed service (NestJS-style app, same conventions: ESM, `@Inject()`, DTOs, OTEL-before-bootstrap). It talks to Synapse, holds `hs_token`/`as_token`, and is operated like any other app. It is **controlled by** the orchestrator but **is not** the orchestrator. |
| **Client SDK** | **Product monorepo**`packages/comms` | A workspace library consumed by product apps *and* by harnesses. Published/linked like `packages/queue`, `packages/db`. Versioned with the product. |
| **MACP spec** (the standard doc + JSON schemas) | **Framework**`~/.config/mosaic/spec/macp/` (mirrored/vendored into `packages/comms` at build) | The protocol is an agent-level contract that must exist independent of any one product checkout. Framework is the source of truth; `packages/comms` vendors a pinned copy so the SDK and spec cannot drift silently. |
| **`mosaic enroll` harness glue** (auto-detect, spin hook) | **Framework**`~/.config/mosaic/tools/enroll/` | Agents/harnesses are framework-level. Enrollment must run *before* the agent has product context; it cannot depend on `/src/mosaic-stack` being present. This is exactly parallel to the existing `~/.config/mosaic/tools/*` wrappers. |
| Piece | Home | Rationale |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Synapse deployment** (compose/helm, config, TLS, `.well-known`, pipelines) | **Product monorepo**`infra/matrix/` + CI in the monorepo's pipeline dir | It is deployed infrastructure with the same lifecycle/observability as Gateway/Web/DB. Lives beside other `infra/` and Woodpecker pipelines. |
| **Mosaic Appservice** | **Product monorepo**`apps/matrix-appservice` | It is a first-class deployed service (NestJS-style app, same conventions: ESM, `@Inject()`, DTOs, OTEL-before-bootstrap). It talks to Synapse, holds `hs_token`/`as_token`, and is operated like any other app. It is **controlled by** the orchestrator but **is not** the orchestrator. |
| **Client SDK** | **Product monorepo**`packages/comms` | A workspace library consumed by product apps _and_ by harnesses. Published/linked like `packages/queue`, `packages/db`. Versioned with the product. |
| **MACP spec** (the standard doc + JSON schemas) | **Framework**`~/.config/mosaic/spec/macp/` (mirrored/vendored into `packages/comms` at build) | The protocol is an agent-level contract that must exist independent of any one product checkout. Framework is the source of truth; `packages/comms` vendors a pinned copy so the SDK and spec cannot drift silently. |
| **`mosaic enroll` harness glue** (auto-detect, spin hook) | **Framework**`~/.config/mosaic/tools/enroll/` | Agents/harnesses are framework-level. Enrollment must run _before_ the agent has product context; it cannot depend on `/src/mosaic-stack` being present. This is exactly parallel to the existing `~/.config/mosaic/tools/*` wrappers. |
**Boundary summary:** *the wire and the services are product; the contract and the spin-time glue are framework.* The one deliberate coupling is **MACP**: framework is authoritative, but `packages/comms` pins a vendored copy and CI fails if they diverge, so an agent enrolling via framework and a service validating via `packages/comms` agree on the schema by construction.
**Boundary summary:** _the wire and the services are product; the contract and the spin-time glue are framework._ The one deliberate coupling is **MACP**: framework is authoritative, but `packages/comms` pins a vendored copy and CI fails if they diverge, so an agent enrolling via framework and a service validating via `packages/comms` agree on the schema by construction.
**Rejected alternative:** putting the appservice in the framework. Rejected because the appservice is a stateful, deployed, secret-holding network service that needs the product's CI/observability/secret plumbing; burying it in `~/.config/mosaic` would split its operational story from every other Mosaic service.
@@ -182,15 +182,15 @@ All MACP events carry a common envelope in `content`:
Event types (Matrix `type` shown; timeline events use `m.room.message` with a custom `msgtype` where a human-visible fallback is desirable, state events use a dotted custom `type`):
| MACP type | Matrix carrier | Purpose | Notable fields |
|---|---|---|---|
| **message** | `m.room.message`, `msgtype: "mosaic.message"` (+ `body` fallback) | ordinary agent/human chat | `body`, `thread?` |
| **presence** | `m.room.message` msgtype `mosaic.presence` in fleet room **or** custom EDU-backed state | heartbeat/liveness ping | `status: online\|away\|offline`, `seq`, `interval_ms` |
| **workflow-step** | state event `mosaic.workflow.step` (state_key = step id) | durable record of a workflow/mission step | `step`, `status: started\|blocked\|done\|failed`, `detail` |
| **review / approval** | `m.room.message` msgtype `mosaic.review` | a review verdict or merge/approval | `subject` (PR/commit ref), `verdict: approve\|reject\|request-changes`, `signature` (REQUIRED) |
| **escalation** | `m.room.message` msgtype `mosaic.escalation` | raise a stuck/dark condition to HIL/fallback | `reason`, `target`, `severity`, `since_ts` |
| MACP type | Matrix carrier | Purpose | Notable fields |
| --------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **message** | `m.room.message`, `msgtype: "mosaic.message"` (+ `body` fallback) | ordinary agent/human chat | `body`, `thread?` |
| **presence** | `m.room.message` msgtype `mosaic.presence` in fleet room **or** custom EDU-backed state | heartbeat/liveness ping | `status: online\|away\|offline`, `seq`, `interval_ms` |
| **workflow-step** | state event `mosaic.workflow.step` (state_key = step id) | durable record of a workflow/mission step | `step`, `status: started\|blocked\|done\|failed`, `detail` |
| **review / approval** | `m.room.message` msgtype `mosaic.review` | a review verdict or merge/approval | `subject` (PR/commit ref), `verdict: approve\|reject\|request-changes`, `signature` (REQUIRED) |
| **escalation** | `m.room.message` msgtype `mosaic.escalation` | raise a stuck/dark condition to HIL/fallback | `reason`, `target`, `severity`, `since_ts` |
Rationale for the carrier split: **timeline events** (`m.room.message` variants) are the durable, receipted, replayable log (this *is* Buzz's "unified event log," §7). **State events** (`mosaic.workflow.step`, presence-as-state) give last-writer-wins current status that a newly-joined agent reads instantly from room state without replaying history.
Rationale for the carrier split: **timeline events** (`m.room.message` variants) are the durable, receipted, replayable log (this _is_ Buzz's "unified event log," §7). **State events** (`mosaic.workflow.step`, presence-as-state) give last-writer-wins current status that a newly-joined agent reads instantly from room state without replaying history.
Every custom event is validated against a JSON Schema shipped in the MACP spec (§3). Unknown/newer `macp_version` → consumers MUST degrade gracefully (render `body`, ignore unknown fields).
@@ -218,18 +218,18 @@ Three visible states plus an explicit heartbeat:
- **away** — presence `unavailable`, or heartbeats late but < dark threshold.
- **offline / dark** — no heartbeat for `dark_threshold` (default **N minutes**, policy value, §5/§11) OR presence `offline`.
Why not rely on native Matrix presence alone: Synapse presence is (a) coarse-grained, (b) can be disabled for load reasons, and (c) **degrades across federation** **[VERIFY]**. So MACP layers an explicit heartbeat carried as a lightweight timeline/state event in the **fleet presence room**, giving a deterministic, federation-safe liveness signal the escalation watchdog (§5) can reason about. Native presence EDUs are still emitted (they make Element show the right dot for humans) but the *authoritative* liveness source is the heartbeat.
Why not rely on native Matrix presence alone: Synapse presence is (a) coarse-grained, (b) can be disabled for load reasons, and (c) **degrades across federation** **[VERIFY]**. So MACP layers an explicit heartbeat carried as a lightweight timeline/state event in the **fleet presence room**, giving a deterministic, federation-safe liveness signal the escalation watchdog (§5) can reason about. Native presence EDUs are still emitted (they make Element show the right dot for humans) but the _authoritative_ liveness source is the heartbeat.
### 4.6 Room / channel taxonomy (orchestrator-owned)
The **orchestrator** (via the appservice) owns room lifecycle. Agents never create backbone rooms ad hoc.
| Room | Scope | Membership | Purpose |
|---|---|---|---|
| **Fleet presence room** | one per site (federated view across sites) | every enrolled agent + HIL | heartbeats, the single "who's alive" board. This is the P1 deliverable. |
| **Per-mission room** | one per mission (e.g. `#mission-KBN-101`) | agents on that mission + Team Lead + HIL | workflow-steps, mission chat, reviews for that mission |
| **Per-team room** | one per team | team members + Team Lead | intra-team coordination |
| **HIL room** | one (or one per site) | humans + escalation-privileged agents | where escalations land; Jason's pane on the fleet |
| Room | Scope | Membership | Purpose |
| ----------------------- | ------------------------------------------ | ---------------------------------------- | ----------------------------------------------------------------------- |
| **Fleet presence room** | one per site (federated view across sites) | every enrolled agent + HIL | heartbeats, the single "who's alive" board. This is the P1 deliverable. |
| **Per-mission room** | one per mission (e.g. `#mission-KBN-101`) | agents on that mission + Team Lead + HIL | workflow-steps, mission chat, reviews for that mission |
| **Per-team room** | one per team | team members + Team Lead | intra-team coordination |
| **HIL room** | one (or one per site) | humans + escalation-privileged agents | where escalations land; Jason's pane on the fleet |
Rooms are created with orchestrator-controlled power levels: appservice = admin (PL100), Team Leads elevated, worker agents default. Room aliases (`#mission-KBN-101:site-a`) are stable handles.
@@ -237,14 +237,14 @@ Rooms are created with orchestrator-controlled power levels: appservice = admin
MACP mandates this decision per message. **Default bias: if it must survive the agent, be seen by an offline party, cross a host, or be audited — Matrix. If it is same-host, synchronous, and ephemeral — tmux.**
| Signal | Route | Why |
|---|---|---|
| Same-host, live pane-to-pane prompt/nudge, sub-second need | **tmux (P0)** | lowest latency, no server round-trip; this is the working fast path we keep |
| Recipient may be offline / on another host | **Matrix** | durability + store-and-forward; tmux can't reach a dark or remote pane |
| Presence / heartbeat / liveness | **Matrix** | must be observable fleet-wide, including by the watchdog and HIL |
| Workflow-step, review/approval, escalation | **Matrix** | must be durable, receipted, auditable, signed (gate acts) |
| Cross-site anything | **Matrix (federated)** | tmux is same-host only |
| Bulk log spew / high-frequency scratch between co-located agents | **tmux**, with periodic **Matrix** checkpoints | avoid flooding the durable log; keep an audit checkpoint |
| Signal | Route | Why |
| ---------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| Same-host, live pane-to-pane prompt/nudge, sub-second need | **tmux (P0)** | lowest latency, no server round-trip; this is the working fast path we keep |
| Recipient may be offline / on another host | **Matrix** | durability + store-and-forward; tmux can't reach a dark or remote pane |
| Presence / heartbeat / liveness | **Matrix** | must be observable fleet-wide, including by the watchdog and HIL |
| Workflow-step, review/approval, escalation | **Matrix** | must be durable, receipted, auditable, signed (gate acts) |
| Cross-site anything | **Matrix (federated)** | tmux is same-host only |
| Bulk log spew / high-frequency scratch between co-located agents | **tmux**, with periodic **Matrix** checkpoints | avoid flooding the durable log; keep an audit checkpoint |
Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `ephemeral+same-host` short-circuits to tmux, everything else goes Matrix. A message can be **dual-routed** (tmux for immediacy + a Matrix checkpoint) when both speed and durability matter.
@@ -256,20 +256,21 @@ Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `eph
**The fix — presence-driven, policy-encoded escalation:**
1. **Deterministic liveness (§4.5).** Every agent heartbeats into the fleet presence room. The appservice **escalation watchdog** subscribes and maintains `last_seen` per agent. No polling by the Team Lead — it *subscribes* (Matrix `/sync` long-poll) and is pushed state changes.
1. **Deterministic liveness (§4.5).** Every agent heartbeats into the fleet presence room. The appservice **escalation watchdog** subscribes and maintains `last_seen` per agent. No polling by the Team Lead — it _subscribes_ (Matrix `/sync` long-poll) and is pushed state changes.
2. **Encoded policy in MACP:** a machine-readable escalation policy attached to each agent/role, e.g.:
```jsonc
{
"role": "team-lead",
"coordinator": "@mosaic_coordinator-1:site-a",
"dark_threshold_min": 10, // OPEN QUESTION §11 — Jason/Mos to set N
"dark_threshold_min": 10, // OPEN QUESTION §11 — Jason/Mos to set N
"on_coordinator_dark": {
"action": "escalate",
"fallback": "@mosaic_coordinator-2:site-b", // cross-site fallback
"fallback": "@mosaic_coordinator-2:site-b", // cross-site fallback
"then": "notify-HIL",
"hil_room": "#hil:site-a"
}
"hil_room": "#hil:site-a",
},
}
```
@@ -277,9 +278,9 @@ Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `eph
- emits a `mosaic.escalation` event into the mission room and the HIL room (`reason: "coordinator dark", since_ts, severity`),
- **re-routes** to the declared fallback coordinator (possibly on another site — this is why federation matters),
- if no fallback answers within a second threshold, pages **HIL** (Jason) in the HIL room.
The Team Lead **never sits blocked polling**; a dark coordinator is a *pushed event*, and the fallback/HIL path is automatic.
The Team Lead **never sits blocked polling**; a dark coordinator is a _pushed event_, and the fallback/HIL path is automatic.
4. **Homelab-dark specifically:** because heartbeats are federated into a cross-site fleet room, a whole *site* going dark is visible from other sites — the watchdog on site-B sees site-A's coordinator stop heartbeating and triggers the same escalation. A dark homelab can no longer silently strand its agents, because the liveness signal and the fallback live *off that host*.
4. **Homelab-dark specifically:** because heartbeats are federated into a cross-site fleet room, a whole _site_ going dark is visible from other sites — the watchdog on site-B sees site-A's coordinator stop heartbeating and triggers the same escalation. A dark homelab can no longer silently strand its agents, because the liveness signal and the fallback live _off that host_.
**Design invariant:** liveness authority and fallback targets must never be co-located with the thing they monitor. The watchdog for site-A's coordinator should also run (or be mirrored) on site-B.
@@ -290,11 +291,13 @@ Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `eph
**Model:** each site runs its **own Synapse homeserver** with its **own Mosaic appservice** and its own agents. Sites **federate** with each other over the standard Matrix server-to-server (S2S) API, restricted to Mosaic sites.
**Why per-site, not one central server:**
- **No SPOF.** The homelab going dark is the founding trauma of this program. A single central homeserver would recreate exactly that risk at fleet scale. Per-site means a site outage is contained: its agents drop, but every other site's comms and the cross-site fleet room survive.
- **Locality.** Same-site agents get low-latency local homeserver traffic; only cross-site events pay the federation cost.
- **Blast radius.** A compromised or misbehaving site can be defederated without touching the rest.
**How federation is wired (real Matrix mechanics):**
- **Server discovery** via `https://<domain>/.well-known/matrix/server` returning `{"m.server": "matrix.<domain>:443"}`, and/or an `_matrix._tcp` **SRV** record. We control the DNS/domains, so we control the federation graph. **[VERIFY]** current `.well-known` vs SRV precedence for the deployed Synapse version.
- **TLS:** federation requires valid TLS on the federation endpoint; we terminate with certs from our own CA/ACME on domains we own.
- **Allowlist:** use Synapse `federation_domain_whitelist` to restrict federation to the set of Mosaic site domains — **no public-network federation** (NG5). This is a hard security boundary.
@@ -308,14 +311,14 @@ Rule of thumb encoded in the SDK: `comms.send()` takes a `durability` hint; `eph
We adopt Buzz's **ideas**, on Matrix rails, phased:
| Buzz idea | Adopt? | How, on Matrix | Phase |
|---|---|---|---|
| **Harness auto-detect / enroll** | **Yes** | `mosaic enroll` detects Claude/Codex/Pi/Goose and self-registers via the appservice (§4.1) | P2 (enroll v1); scan-machine-and-offer-integrate is **v2** |
| **Keypair signed identity** | **Yes, scoped** | Ed25519 signed-authorship on **gate actions only** (reviews/merges/approvals), pubkey in profile state (§4.4) | P5 (hardening); keys issued at enroll from P2 |
| **Unified event log (humans + agents, one log)** | **Yes** | Matrix room timeline *is* the unified, receipted, replayable event log; MACP custom events are first-class entries (§4.2) | P2→P3 |
| **Humans and agents on the same surface** | **Yes** | HIL uses the same Matrix rooms via Element/custom client; escalations land where Jason already is (§4.6, §5) | P1 (fleet room) → P2 |
| **Scan machine, offer to integrate existing tools** | **Deferred (v2)** | enroll v1 auto-detects the *harness*; scanning a host for other integratable tools is explicitly **enroll v2**, post-P5 | v2 |
| **Buzz/Nostr as transport** | **No** | transport is Matrix; only patterns are borrowed (NG3) | — |
| Buzz idea | Adopt? | How, on Matrix | Phase |
| --------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| **Harness auto-detect / enroll** | **Yes** | `mosaic enroll` detects Claude/Codex/Pi/Goose and self-registers via the appservice (§4.1) | P2 (enroll v1); scan-machine-and-offer-integrate is **v2** |
| **Keypair signed identity** | **Yes, scoped** | Ed25519 signed-authorship on **gate actions only** (reviews/merges/approvals), pubkey in profile state (§4.4) | P5 (hardening); keys issued at enroll from P2 |
| **Unified event log (humans + agents, one log)** | **Yes** | Matrix room timeline _is_ the unified, receipted, replayable event log; MACP custom events are first-class entries (§4.2) | P2→P3 |
| **Humans and agents on the same surface** | **Yes** | HIL uses the same Matrix rooms via Element/custom client; escalations land where Jason already is (§4.6, §5) | P1 (fleet room) → P2 |
| **Scan machine, offer to integrate existing tools** | **Deferred (v2)** | enroll v1 auto-detects the _harness_; scanning a host for other integratable tools is explicitly **enroll v2**, post-P5 | v2 |
| **Buzz/Nostr as transport** | **No** | transport is Matrix; only patterns are borrowed (NG3) | — |
---
@@ -329,18 +332,19 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- **Signed-authorship keys:** Ed25519 private keys should be generated agent-side and only the public key leaves the agent (best custody: private key never transits the network). Where agents are ephemeral, keys are minted per-spin and discarded on teardown; the pubkey-in-profile record remains for audit.
- **Rotation:** tokens are short-lived where the Synapse version supports refresh **[VERIFY]**; otherwise the appservice supports explicit re-issue on re-enroll. A rotation runbook is a P2 deliverable.
- **Federation TLS.** Valid certs on federation endpoints; whitelist-only federation; monitor for cert expiry (a cert lapse silently defederates a site — add to observability).
- **Interaction with existing Vault/secrets.** The appservice's `hs_token`/`as_token` and the enroll bootstrap secret are stored in the existing secret manager (Vault or the project's chosen store — **open question §11**) and injected at deploy, consistent with how Gateway/DB secrets are handled today. No new bespoke secret store. Per-agent tokens are *transient runtime* secrets, not persisted to Vault (they're re-mintable). **[VERIFY]** current Mosaic secret-management choice — the CLAUDE.md notes secrets/KBN work in flight, so align with whatever KBN-101 lands.
- **Interaction with existing Vault/secrets.** The appservice's `hs_token`/`as_token` and the enroll bootstrap secret are stored in the existing secret manager (Vault or the project's chosen store — **open question §11**) and injected at deploy, consistent with how Gateway/DB secrets are handled today. No new bespoke secret store. Per-agent tokens are _transient runtime_ secrets, not persisted to Vault (they're re-mintable). **[VERIFY]** current Mosaic secret-management choice — the CLAUDE.md notes secrets/KBN work in flight, so align with whatever KBN-101 lands.
---
## 9. Migration — strangler off Hermes
**Principle:** stand the native layer up *alongside* Hermes, move channels over **as each is proven at parity**, retire Hermes only when the parity checklist is green. Never a flag-day cutover (NG1).
**Principle:** stand the native layer up _alongside_ Hermes, move channels over **as each is proven at parity**, retire Hermes only when the parity checklist is green. Never a flag-day cutover (NG1).
**Sequence:**
1. **Stand alongside.** Native Synapse + appservice + fleet presence room live in parallel; Hermes still carries everything it carries today. Presence (P1) is *additive* — it gives us something Hermes never had, at zero risk to existing flows.
1. **Stand alongside.** Native Synapse + appservice + fleet presence room live in parallel; Hermes still carries everything it carries today. Presence (P1) is _additive_ — it gives us something Hermes never had, at zero risk to existing flows.
2. **Move channels as proven.** Per channel (fleet presence → mission coordination → reviews/approvals → external-chat edge), cut traffic to Matrix, keep Hermes as hot fallback until the channel meets parity for a soak period.
3. **Retire at parity.** When every checklist item is green and soaked, decommission the Hermes MCP bridge as backbone. (External chat platforms, if still needed, can be re-attached as a *bridge at the edge* of Matrix rather than the spine.)
3. **Retire at parity.** When every checklist item is green and soaked, decommission the Hermes MCP bridge as backbone. (External chat platforms, if still needed, can be re-attached as a _bridge at the edge_ of Matrix rather than the spine.)
**Parity checklist (must all be green before Hermes retires):**
@@ -361,9 +365,11 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
> This section is the decomposition surface: each phase → one or more missions for the orchestrator.
### P1 — Presence / availability on a minimal single-site Synapse *(the first standalone shippable slice)*
### P1 — Presence / availability on a minimal single-site Synapse _(the first standalone shippable slice)_
**Scope:** one Synapse homeserver, a **minimal** appservice (or even a scripted provisioner) whose only job is: register a handful of agent MXIDs, create the **fleet presence room**, and carry heartbeats; a minimal `packages/comms` slice that sets presence and heartbeats; native presence surfaced to a human via Element.
**Acceptance criteria:**
- A1. Single-site Synapse deployed (`infra/matrix/`), reachable over TLS, open registration OFF.
- A2. ≥3 agents enroll (even if semi-manually) and appear in a **fleet presence room** with a live online/away/offline indicator.
- A3. `mosaic.presence` heartbeat implemented; an agent killed hard flips to **offline/dark** within `dark_threshold` deterministically (not dependent on native presence timeout alone).
@@ -371,8 +377,10 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- A5. Zero impact to existing tmux + `mos-comms` flows (they still work untouched).
### P2 — Native appservice + orchestrator auto-enroll / room-provisioning
**Scope:** full **`apps/matrix-appservice`** registered with Synapse (`hs_token`/`as_token`, namespaces); `mosaic enroll` harness auto-detect; orchestrator-owned room taxonomy; per-agent token minting; introductions.
**Acceptance criteria:**
- B1. Appservice registered with Synapse via registration file; owns `@mosaic_*` user namespace and room-alias namespace.
- B2. `mosaic enroll` auto-detects all four harnesses (Claude/Codex/Pi/Goose) and self-registers on spin, idempotently.
- B3. On spin, an agent is provisioned an MXID, minted its **own** access token, joined to the correct mission/team/fleet rooms, and posts a `mosaic.introduction`.
@@ -381,8 +389,10 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- B6. Appservice + Synapse traced into OTEL/Jaeger.
### P3 — MACP v1 spec ratified
**Scope:** freeze the standard (§4): envelope, event types + JSON Schemas, identity model, presence model, room taxonomy, tmux↔Matrix routing rules. Spec lives in framework (`~/.config/mosaic/spec/macp`), vendored+pinned into `packages/comms` with CI drift-check.
**Acceptance criteria:**
- C1. MACP v1 document ratified (MS-LEAD sign-off, Jason veto window closed).
- C2. JSON Schemas for all five event types published; `packages/comms` validates outbound/inbound against them.
- C3. CI fails if framework spec and vendored `packages/comms` copy diverge.
@@ -390,8 +400,10 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- C5. Unknown-`macp_version` graceful-degrade behavior tested.
### P4 — Federation
**Scope:** a second site homeserver + appservice; S2S federation over our DNS/TLS; cross-site fleet room; whitelist-only federation; cross-site escalation.
**Acceptance criteria:**
- D1. Two sites federate via `.well-known`/SRV over TLS we control; `federation_domain_whitelist` restricts to Mosaic sites (no public federation).
- D2. A cross-site fleet presence room shows agents from both sites; heartbeats propagate as events across federation.
- D3. **Homelab-dark test:** killing site-A's coordinator is observed from site-B within `dark_threshold`, and the escalation/fallback fires cross-site (§5).
@@ -399,8 +411,10 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- D5. Defederation runbook proven (a site can be cut off cleanly).
### P5 — Buzz-hardening + signed-authorship + Hermes retired
**Scope:** Ed25519 signed-authorship enforced on gate actions; security hardening pass; complete the strangler and retire Hermes at parity.
**Acceptance criteria:**
- E1. Every merge/approve/review gate action is signed; unsigned or bad-sig gate events are rejected by the appservice watcher and by consuming agents.
- E2. Token/key rotation runbooks executed at least once in anger (rotate `as_token`, rotate a per-agent key).
- E3. Security review complete: homeserver hardening flags, token custody, federation TLS/whitelist all verified.
@@ -424,7 +438,7 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
## Appendix A — Real Matrix concepts this RFC leans on (quick reference)
- **Application Service (AS) API** — a privileged service registered with the homeserver via a registration YAML declaring `id`, `url`, `as_token`, `hs_token`, and `namespaces` (users/aliases/rooms regexes). The homeserver pushes events to the AS in transactions; the AS can register/act-as users in its namespace. *(This is our appservice backbone.)*
- **Application Service (AS) API** — a privileged service registered with the homeserver via a registration YAML declaring `id`, `url`, `as_token`, `hs_token`, and `namespaces` (users/aliases/rooms regexes). The homeserver pushes events to the AS in transactions; the AS can register/act-as users in its namespace. _(This is our appservice backbone.)_
- **`hs_token` / `as_token`** — `hs_token`: homeserver→AS authentication on pushed transactions; `as_token`: AS→homeserver authentication, grants acting-as any namespaced user. Both are high-value secrets (§8).
- **Masquerade (`user_id` query param)** — the AS may act as a namespaced user on C-S calls using `as_token` + `?user_id=`. We prefer per-agent tokens for blast-radius; masquerade for AS-internal bulk ops.
- **Custom event types** — timeline events via `m.room.message` with a custom `msgtype` (keeps a human-visible `body` fallback) and/or fully custom `type` (dotted, e.g. `mosaic.workflow.step`); **state events** for last-writer-wins current status readable from room state without history replay.
@@ -432,4 +446,4 @@ We adopt Buzz's **ideas**, on Matrix rails, phased:
- **Federation (S2S API)** — server-to-server over TLS; discovery via `/.well-known/matrix/server` and/or `_matrix._tcp` SRV; restrictable with `federation_domain_whitelist`.
- **Synapse config knobs cited** — `enable_registration`, `federation_domain_whitelist`, appservice registration file, rate-limiting, admin API binding. **[VERIFY]** exact flags/paths against the deployed Synapse version at implementation time.
*All Matrix mechanics above are cited from architecture knowledge and MUST be re-verified against the actual deployed Synapse version during P1 — every **[VERIFY]** in this document is a checkpoint, not an assumption.*
_All Matrix mechanics above are cited from architecture knowledge and MUST be re-verified against the actual deployed Synapse version during P1 — every **[VERIFY]** in this document is a checkpoint, not an assumption._