refactor: rename cli-v2 → cli, archive legacy cli, plus broker-side grants + auto-migrate
- 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:
6
apps/cli/src/services/broker/envelope.ts
Normal file
6
apps/cli/src/services/broker/envelope.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
encryptDirect,
|
||||
decryptDirect,
|
||||
isDirectTarget,
|
||||
} from "~/services/crypto/facade.js";
|
||||
export type { Envelope } from "~/services/crypto/facade.js";
|
||||
13
apps/cli/src/services/broker/errors.ts
Normal file
13
apps/cli/src/services/broker/errors.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
8
apps/cli/src/services/broker/facade.ts
Normal file
8
apps/cli/src/services/broker/facade.ts
Normal 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";
|
||||
17
apps/cli/src/services/broker/hello-sig.ts
Normal file
17
apps/cli/src/services/broker/hello-sig.ts
Normal 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) };
|
||||
}
|
||||
2
apps/cli/src/services/broker/implementation.ts
Normal file
2
apps/cli/src/services/broker/implementation.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { BrokerClient } from "./ws-client.js";
|
||||
export type { Priority, ConnStatus, PeerInfo, InboundPush } from "./ws-client.js";
|
||||
1
apps/cli/src/services/broker/index.ts
Normal file
1
apps/cli/src/services/broker/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./facade.js";
|
||||
47
apps/cli/src/services/broker/manager.ts
Normal file
47
apps/cli/src/services/broker/manager.ts
Normal 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();
|
||||
}
|
||||
28
apps/cli/src/services/broker/schemas.ts
Normal file
28
apps/cli/src/services/broker/schemas.ts
Normal 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";
|
||||
2221
apps/cli/src/services/broker/ws-client.ts
Normal file
2221
apps/cli/src/services/broker/ws-client.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user