feat(comms): P1 presence — minimal Synapse + fleet presence room + mosaic.presence heartbeat + liveness
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Implements RFC-001 P1 (the first shippable slice): deterministic Matrix
presence/liveness on a single-instance dev Synapse.

- infra/matrix/: DEV Synapse (RFC-002 Mode B, federation OFF, self-signed TLS,
  enable_registration:false, appservice registration wired). Rendered from
  parameterized templates — zero hardcoded topology. .data is gitignored.
- packages/comms/: minimal MACP presence SDK — set presence, run the
  mosaic.presence heartbeat (seq + interval per RFC-001 §4.5), and a
  deterministic liveness reader (online/away/offline from heartbeat age, NOT
  native-presence-timeout). Liveness core written RED-FIRST. 19 vitest tests.
- tools/matrix-presence-harness/: minimal provisioner (registers >=3 agent
  MXIDs, creates the fleet presence room, joins them — reuses the existing
  @mosaicstack/appservice intent lib) + an E2E validation harness proving
  A2/A3/A4 against a real Synapse.
- eslint.config.mjs: register packages/comms/vitest.config.ts with the
  type-aware project service (same as other packages' vitest configs).

DEV-compose validated only; production deploy is a separate coordinated step
(deploy-holds respected).

Part of the comms-evolution program (RFC-001 P1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
mosaic-coder
2026-07-24 20:18:18 -05:00
parent 529c177830
commit 2e7c124e4d
33 changed files with 2140 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
/**
* tools/matrix-presence-harness/provision.ts
*
* *** DEV harness — LOCAL SANDBOX ONLY. ***
*
* The MINIMAL P1 provisioner (RFC-001 §10 P1): its ONLY job is to register a
* few agent MXIDs, create the fleet presence room, and join the agents. It is
* deliberately NOT the P2 appservice (no auto-enroll, no room taxonomy, no
* token minting, no introductions).
*
* It reuses the existing, tested `@mosaicstack/appservice` core library
* (AppserviceIntent: register / createRoom / join over the AS API) rather than
* reinventing Matrix plumbing.
*
* ESM / NodeNext: .js import extensions.
*/
import { AppserviceIntent, type AppserviceConfig } from '@mosaicstack/appservice';
export interface ProvisionOptions {
homeserverUrl: string;
serverName: string;
asToken: string;
hsToken: string;
agentSlugs: string[];
fleetAlias?: string; // localpart, default "mosaic-fleet"
}
export interface ProvisionResult {
roomId: string;
senderUserId: string;
agents: { slug: string; mxid: string }[];
}
/** Register agents, create the fleet presence room, join everyone. Idempotent. */
export async function provision(opts: ProvisionOptions): Promise<ProvisionResult> {
const cfg: AppserviceConfig = {
homeserverUrl: opts.homeserverUrl,
domain: opts.serverName,
asToken: opts.asToken,
hsToken: opts.hsToken,
};
const intent = new AppserviceIntent(cfg);
const fleetAlias = opts.fleetAlias ?? 'mosaic-fleet';
// 1. Register the virtual agent MXIDs (bypasses enable_registration:false).
const agents = [];
for (const slug of opts.agentSlugs) {
const mxid = await intent.ensureRegistered(slug);
await intent.setDisplayName(slug, `agent ${slug} (DEV)`);
agents.push({ slug, mxid });
}
// 2. Create the single fleet presence room, inviting all agents (RFC-001 §4.6).
// Idempotent: if the alias already exists, reuse that room instead.
let roomId: string;
try {
const created = await intent.createRoom({
name: 'Fleet Presence (DEV)',
alias: fleetAlias,
topic: 'RFC-001 P1 — mosaic.presence heartbeats. Who is alive?',
invite: agents.map((a) => a.mxid),
});
roomId = created.roomId;
} catch (err) {
// Alias already taken (re-run): resolve the existing room id.
const aliasFq = `#${fleetAlias}:${opts.serverName}`;
roomId = await resolveAlias(opts, aliasFq);
void err;
}
// 3. Join every agent into the fleet room (invite + join; idempotent).
for (const { slug } of agents) {
await intent.ensureJoined(roomId, slug);
}
return { roomId, senderUserId: intent.senderUserId, agents };
}
async function resolveAlias(
opts: Pick<ProvisionOptions, 'homeserverUrl' | 'asToken'>,
aliasFq: string,
): Promise<string> {
const url = `${opts.homeserverUrl.replace(/\/$/, '')}/_matrix/client/v3/directory/room/${encodeURIComponent(aliasFq)}`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${opts.asToken}` } });
if (!res.ok) throw new Error(`resolveAlias ${aliasFq} -> ${res.status}`);
const data = (await res.json()) as { room_id?: string };
if (!data.room_id) throw new Error(`resolveAlias ${aliasFq} returned no room_id`);
return data.room_id;
}