refactor: rename cli-v2 → cli, archive legacy cli, plus broker-side grants + auto-migrate
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

- apps/cli/ is now the canonical CLI (was apps/cli-v2/).
- apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag
  'cli-v0-legacy-final' before deletion; git history preserves it too.
- .github/workflows/release-cli.yml paths updated.
- pnpm-lock.yaml regenerated.

Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities):
- 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member.
- handleSend in broker fetches recipient grant maps once per send, drops
  messages silently when sender lacks the required capability.
- POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric.
- CLI grant/revoke/block now mirror to broker via syncToBroker.

Auto-migrate on broker startup:
- apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock
  before the HTTP server binds. Exits non-zero on failure so Coolify
  healthcheck fails closed.
- Dockerfile copies packages/db/migrations into /app/migrations.
- postgres 3.4.5 added as direct broker dep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-15 08:44:52 +01:00
parent c9ede3d469
commit ee12510ef1
374 changed files with 14706 additions and 11307 deletions

View File

@@ -0,0 +1,6 @@
export {
encryptDirect,
decryptDirect,
isDirectTarget,
} from "~/services/crypto/facade.js";
export type { Envelope } from "~/services/crypto/facade.js";

View File

@@ -0,0 +1,13 @@
export class BrokerConnectionError extends Error {
constructor(message: string, public readonly url: string) {
super(message);
this.name = "BrokerConnectionError";
}
}
export class HelloAckTimeout extends Error {
constructor() {
super("hello_ack timeout — broker did not respond");
this.name = "HelloAckTimeout";
}
}

View File

@@ -0,0 +1,8 @@
export { BrokerClient } from "./ws-client.js";
export type { Priority, ConnStatus, PeerInfo, InboundPush } from "./ws-client.js";
export { ensureClient, startClients, findClient, allClients, stopAll } from "./manager.js";
export { signHello } from "./hello-sig.js";
export { encryptDirect, decryptDirect, isDirectTarget } from "./envelope.js";
export type { Envelope } from "./envelope.js";
export { BrokerConnectionError, HelloAckTimeout } from "./errors.js";
export type { WsMessageType } from "./schemas.js";

View File

@@ -0,0 +1,17 @@
import { ensureSodium } from "~/services/crypto/facade.js";
export async function signHello(
meshId: string,
memberId: string,
pubkey: string,
secretKeyHex: string,
): Promise<{ timestamp: number; signature: string }> {
const s = await ensureSodium();
const timestamp = Date.now();
const canonical = `${meshId}|${memberId}|${pubkey}|${timestamp}`;
const sig = s.crypto_sign_detached(
s.from_string(canonical),
s.from_hex(secretKeyHex),
);
return { timestamp, signature: s.to_hex(sig) };
}

View File

@@ -0,0 +1,2 @@
export { BrokerClient } from "./ws-client.js";
export type { Priority, ConnStatus, PeerInfo, InboundPush } from "./ws-client.js";

View File

@@ -0,0 +1 @@
export * from "./facade.js";

View File

@@ -0,0 +1,47 @@
import { BrokerClient } from "./ws-client.js";
import type { Config, JoinedMesh } from "~/services/config/facade.js";
const clients = new Map<string, BrokerClient>();
let configDisplayName: string | undefined;
let configGroups: Config["groups"] = [];
export async function ensureClient(mesh: JoinedMesh): Promise<BrokerClient> {
const existing = clients.get(mesh.meshId);
if (existing) return existing;
const isDebug = process.env.CLAUDEMESH_DEBUG === "1" || process.env.CLAUDEMESH_DEBUG === "true";
const client = new BrokerClient(mesh, { debug: isDebug, displayName: configDisplayName });
clients.set(mesh.meshId, client);
try {
await client.connect();
for (const g of configGroups ?? []) {
try { await client.joinGroup(g.name, g.role); } catch {}
}
} catch (err) {
process.stderr.write(`[claudemesh] broker connect failed for ${mesh.slug}: ${err instanceof Error ? err.message : err} (will retry)\n`);
}
return client;
}
export async function startClients(config: Config): Promise<void> {
configDisplayName = config.displayName;
configGroups = config.groups ?? [];
await Promise.allSettled(config.meshes.map(ensureClient));
}
export function findClient(needle: string): BrokerClient | null {
const byId = clients.get(needle);
if (byId) return byId;
for (const c of clients.values()) {
if (c.meshSlug === needle) return c;
}
return null;
}
export function allClients(): BrokerClient[] {
return [...clients.values()];
}
export function stopAll(): void {
for (const c of clients.values()) c.close();
clients.clear();
}

View File

@@ -0,0 +1,28 @@
export type WsMessageType =
| "hello" | "hello_ack"
| "send" | "ack" | "push"
| "list_peers" | "peers_list"
| "set_status" | "set_summary" | "set_visible" | "set_profile" | "set_stats"
| "join_group" | "leave_group"
| "set_state" | "get_state" | "list_state" | "state_result" | "state_list_result" | "state_changed"
| "remember" | "memory_stored" | "recall" | "recall_result" | "forget"
| "schedule" | "schedule_ack" | "list_scheduled" | "scheduled_list" | "cancel_scheduled" | "cancel_scheduled_ack"
| "message_status" | "message_status_result"
| "upload_file" | "file_url" | "list_files" | "files_list" | "file_status" | "file_status_result" | "delete_file" | "grant_file_access"
| "vector_store" | "vector_stored" | "vector_search" | "vector_results" | "vector_delete" | "list_collections" | "collections_list"
| "graph_query" | "graph_result" | "graph_execute"
| "share_context" | "list_contexts" | "contexts_list" | "get_context" | "context_results"
| "create_task" | "task_created" | "list_tasks" | "tasks_list" | "claim_task" | "complete_task"
| "mesh_query" | "mesh_query_result" | "mesh_execute" | "mesh_schema" | "mesh_schema_result"
| "create_stream" | "stream_created" | "publish" | "subscribe" | "stream_data" | "list_streams" | "streams_list"
| "mcp_register" | "mcp_registered" | "mcp_list" | "mcp_list_result" | "mcp_call" | "mcp_call_result" | "mcp_call_forward" | "mcp_call_response" | "mcp_remove"
| "mcp_deploy" | "mcp_deploy_result" | "mcp_undeploy" | "mcp_update" | "mcp_logs" | "mcp_logs_result" | "mcp_schema" | "mcp_schema_result" | "mcp_catalog" | "mcp_catalog_result" | "mcp_scope" | "mcp_scope_result"
| "vault_set" | "vault_ack" | "vault_list" | "vault_list_result" | "vault_delete"
| "mesh_watch" | "watch_ack" | "mesh_unwatch" | "mesh_watches" | "watches_list" | "watch_triggered"
| "create_webhook" | "webhook_created" | "list_webhooks" | "webhooks_list" | "delete_webhook"
| "share_skill" | "skill_shared" | "get_skill" | "skill_result" | "list_skills" | "skills_list" | "remove_skill" | "skill_deploy" | "skill_deploy_result"
| "mesh_info" | "mesh_info_result" | "mesh_stats" | "mesh_stats_result" | "mesh_clock" | "mesh_clock_result"
| "mesh_set_clock" | "mesh_pause_clock" | "mesh_resume_clock"
| "ping" | "pong"
| "peer_file_request" | "peer_file_response" | "peer_dir_request" | "peer_dir_response"
| "error";

File diff suppressed because it is too large Load Diff