feat(comms): P1 presence — minimal Synapse + fleet presence room + mosaic.presence heartbeat + liveness
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
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:
89
tools/matrix-presence-harness/provision.ts
Normal file
89
tools/matrix-presence-harness/provision.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user