Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,9 @@
"generate:server-json": "tsx scripts/sync-release-metadata.ts",
"publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts",
"lint": "eslint .",
"test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/identity-device-id.test.js dist/test/wire-mesh-identity.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/tls-transport.integration.test.js dist/test/peer-id-verification.integration.test.js dist/test/become-coordinator-actual-port.integration.test.js dist/test/state-sync-convergence.test.js dist/test/downtime-replay.test.js dist/test/downtime-replay.integration.test.js dist/test/filestore.test.js dist/test/handshake.test.js dist/test/approval.integration.test.js dist/test/listener-policy.integration.test.js dist/test/mesh-smoke.integration.test.js dist/test/wire-mesh-transport.integration.test.js dist/test/wire-mesh-transport-approval.integration.test.js",
"test:visibility": "node --test dist/test/visibility.integration.test.js",
"test:delivery": "node dist/test/delivery-receipt.runner.js",
"test:federation": "node dist/test/federation.integration.test.js",
"test:all": "pnpm test && pnpm test:delivery && pnpm test:federation",
"test": "tsx --test --test-concurrency=1 'src/test/**/*.test.ts'",
"test:delivery": "tsx src/test/delivery-receipt.runner.ts",
"test:all": "pnpm test && pnpm test:delivery",
"test:frontend": "tsx --test src/bridges/user/web/frontend/test/**/*.unit.test.ts",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit"
Expand Down
19 changes: 4 additions & 15 deletions src/bridges/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

import {
MeshStore,
CommsTool,
createBridgeMesh,
buildAction,
ensureRegistered,
drainAndFormat,
MCP_TOOL_PARAMS,
} from "../../core/index.js";
import { TlsTransport } from "../../core/tls-transport.js";
import {
loadOrCreateIdentity,
type IdentitySlot,
} from "../../core/identity-store.js";
import type { IdentitySlot } from "../../core/identity-store.js";
import { tryStartWebServer } from "../user/web/server.js";
import { nanoid } from "../../core/nanoid.js";

Expand All @@ -32,15 +27,9 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}

export async function run(): Promise<void> {
// Persistent identity for this slot: a stable fingerprint means the agent
// ID survives restarts, so peers can keep targeting us. The stdio server
// has no graceful shutdown hook; a stale lock self-heals via the pid probe.
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us. The stdio server has no graceful shutdown hook; a stale lock self-heals via the pid probe.
const identitySlot: IdentitySlot = { harness: "mcp", cwd: process.cwd() };
const identity = loadOrCreateIdentity(identitySlot);
const store = new MeshStore();
store.peerId = identity.fingerprint;
store.setTransport(new TlsTransport(store.events, identity));
const tool = new CommsTool(store, store.discovery);
const { store, tool } = createBridgeMesh(identitySlot);
let agentId: string | undefined;

const mcp = new McpServer(
Expand Down
29 changes: 29 additions & 0 deletions src/core/bridge-mesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Shared bootstrap for a bridge's own mesh participation: load or create this bridge's persisted identity, construct a MeshStore wired to WireMeshTransport, and build the CommsTool that sits on top of it. Every bridge previously repeated this same four-line block against TlsTransport/identity.fingerprint directly; this factory is the single place that wiring lives now, so the substrate a bridge runs on is a one-line change here rather than six repeated ones.
*
* peerId is deviceIdToHex(identity.deviceId), not identity.fingerprint -- WireMeshTransport's own session bookkeeping is keyed by device-id, so MeshStore's own notion of "this peer's id" has to be the same value for the two to correlate. Every existing agent id changes the first time a bridge starts through this factory: there is no migration path, since the value is a hash of genuinely different bytes (device-id is SHA-256(raw public key); fingerprint is SHA-256(certificate DER)) -- a hard cutover, already established as correct when identity.ts first grew deviceId, not relitigated here.
*/

import { deviceIdToHex } from "@exadev/wire-mesh-core/domain/device-id";
import { MeshStore } from "./mesh-store.js";
import { CommsTool } from "./tool.js";
import { WireMeshTransport } from "./wire-mesh-transport.js";
import { loadOrCreateIdentity } from "./identity-store.js";
import type { IdentitySlot } from "./identity-store.js";

export interface BridgeMesh {
store: MeshStore;
tool: CommsTool;
}

export function createBridgeMesh(
slot: Readonly<IdentitySlot>,
coordinatorPort?: number,
): BridgeMesh {
const identity = loadOrCreateIdentity(slot);
const store = new MeshStore(coordinatorPort);
store.peerId = deviceIdToHex(Uint8Array.from(identity.deviceId));
store.setTransport(new WireMeshTransport(store.events, identity));
const tool = new CommsTool(store, store.discovery);
return { store, tool };
}
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type { CommsStore } from "./comms-store.js";
export { FileStore, CommsError } from "./store.js";
export { MeshStore } from "./mesh-store.js";
export { CommsTool } from "./tool.js";
export { createBridgeMesh } from "./bridge-mesh.js";
export type { BridgeMesh } from "./bridge-mesh.js";
export type { CommsContext, CommsResult } from "./tool.js";
export {
buildAction,
Expand Down
79 changes: 79 additions & 0 deletions src/test/bridge-mesh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Unit tests for createBridgeMesh -- the shared factory every bridge builds its own MeshStore/WireMeshTransport/CommsTool from, replacing the identical four-line block each of the six bridges used to repeat against TlsTransport directly.
*/

import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { test } from "node:test";
import { deviceIdToHex } from "@exadev/wire-mesh-core/domain/device-id";
import { createBridgeMesh } from "../core/bridge-mesh.js";
import {
loadOrCreateIdentity,
type IdentitySlot,
} from "../core/identity-store.js";
import { waitFor } from "./test-transport.js";

function tempSlot(harness: string): IdentitySlot {
const dir = fs.mkdtempSync(
path.join(tmpdir(), "agent-comms-bridge-mesh-test-"),
);
return { harness, cwd: "/tmp/project", dir };
}

void test("createBridgeMesh sets peerId to deviceIdToHex(identity.deviceId), not the certificate fingerprint", async () => {
const slot = tempSlot("test-harness");
const identity = loadOrCreateIdentity(slot);
const { store } = createBridgeMesh(slot);
try {
assert.strictEqual(
store.peerId,
deviceIdToHex(Uint8Array.from(identity.deviceId)),
);
assert.notStrictEqual(store.peerId, identity.fingerprint);
} finally {
await store.shutdown();
}
});

void test("createBridgeMesh wires a WireMeshTransport, not TlsTransport", async () => {
const slot = tempSlot("test-harness");
const { store } = createBridgeMesh(slot);
try {
// init() is the only way to prove the transport is actually usable end to end, which also confirms it's a WireMeshTransport by construction (createBridgeMesh only ever builds one).
await store.init();
assert.ok(store.connected);
} finally {
await store.shutdown();
}
});

void test("createBridgeMesh passes an explicit coordinatorPort through to MeshStore, forming one shared mesh", async () => {
const slotA = tempSlot("test-harness-a");
const slotB = tempSlot("test-harness-b");
const port = 20_900 + Math.floor(Math.random() * 100);
const a = createBridgeMesh(slotA, port);
const b = createBridgeMesh(slotB, port);
try {
await a.store.init();
await b.store.init();
assert.ok(a.store.connected);
assert.ok(b.store.connected);
await a.store.registerAgent({
name: "peer-a",
harness: "test-harness-a",
cwd: "/tmp/project",
pid: process.pid,
visibility: "visible",
tags: [],
});
await waitFor(
() => b.store.serialise().agents[a.store.peerId] !== undefined,
"b sees a's agent, proving both joined the same mesh on the shared coordinatorPort",
);
} finally {
await b.store.shutdown();
await a.store.shutdown();
}
});
25 changes: 12 additions & 13 deletions src/test/delivery-receipt.runner.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
#!/usr/bin/env node
/**
* Delivery receipt test runner — runs each test case in an isolated child
* process. Tests MUST NOT be run from within a process that has the
* agent-comms extension loaded (e.g. pi's agent session), because the
* parent's active TCP handles interfere with child process forking.
* Delivery receipt test runner — runs each test case in an isolated child process. Tests MUST NOT be run from within a process that has the agent-comms extension loaded (e.g. pi's agent session), because the parent's active TCP handles interfere with child process forking.
*
* Usage:
* pnpm test:delivery # from a clean shell (recommended)
* node dist/test/delivery-receipt.runner.js
* Usage: pnpm test:delivery # from a clean shell (recommended) tsx src/test/delivery-receipt.runner.ts
*/

import { execFileSync } from "node:child_process";
import * as path from "node:path";
import * as url from "node:url";

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const HELPER = path.join(__dirname, "delivery-receipt.helper.js");
const HELPER = path.join(__dirname, "delivery-receipt.helper.ts");

const tests = [
"push-room",
Expand All @@ -31,11 +26,15 @@ let failed = false;

for (const t of tests) {
try {
const stdout = execFileSync(process.execPath, [HELPER, t], {
timeout: 15_000,
stdio: ["pipe", "pipe", "pipe"],
encoding: "utf-8",
});
const stdout = execFileSync(
process.execPath,
["--import", "tsx", HELPER, t],
{
timeout: 15_000,
stdio: ["pipe", "pipe", "pipe"],
encoding: "utf-8",
},
);
if (stdout.trim()) console.log(stdout.trim());
console.log(`${t} ✓`);
} catch (e: unknown) {
Expand Down