UNPKG

@essentialai/cogent-bridge

Version:

Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.

174 lines • 9.74 kB
import crypto from "node:crypto"; import { z } from "zod"; import { getConfig } from "../config.js"; import { successResult, errorResult, BridgeError, BridgeErrorCode } from "../errors.js"; import { saveCredentials, markJoinedInProcess, markCloudAttemptInProcess } from "../cloud/credential-store.js"; import { logger } from "../logger.js"; import { invalidChannelName, slackMapCommand } from "./channel-text.js"; /** * Register the cogent_create_session MCP tool. * * Creates a new channel on the relay and joins it. With an Org_ID it is a Team channel on * COGENT_TEAM_ENDPOINT; without one, a free channel on COGENT_ENDPOINT — the same routing rule * as cogent_join_session, line for line (COG-81: the Org_ID used to be dropped, so a "Team" * create landed a PUBLIC channel on the free relay and reported success). * After creation, persists credentials locally and reinitializes the cloud backend so the * runtime HttpClient and CloudWsClient use the new token/sessionId. */ export function registerCreateSessionTool(server) { server.registerTool("cogent_create_session", { title: "Create Cloud Channel", description: "Step 2: create a Cogent channel and join it. Call this ONLY after cogent_join_session " + "answered CHANNEL_NOT_FOUND, with the SAME channel name, secret and (Team channel) orgId. " + "Never choose a different name. Next call: cogent_register_peer.", inputSchema: { label: z .string() .optional() .describe("The channel name (3-32 characters: lowercase letters, digits, hyphens) — the same " + "name the join was given. Auto-generated if omitted."), secret: z .string() .min(8) .optional() .describe("Shared secret / password for the channel (min 8 chars). " + "Auto-generated if omitted."), orgId: z .string() .min(1) .optional() .describe("Org_ID — the 3rd credential for a Team (business) channel. " + "Provide it to create a Team channel; omit for a free channel."), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, }, async ({ label, secret, orgId }) => { try { // Record that a cloud create was ATTEMPTED (before any network call), so a // subsequent cogent_register_peer can tell a failed create from "no cloud // attempt at all" and refuse to silently fall back (T0.1, shipped 3.12.5). markCloudAttemptInProcess(); // Same normalisation as cogent_join_session: a stray space otherwise creates a channel // the join can never resolve. A blank Org_ID is treated as absent (free channel). // A value that was SUPPLIED but is blank is a mistake, not "auto-generate one": answering // it with a random name or password reported success for a channel nobody asked for. if (label !== undefined && label.trim() === "") { return errorResult(invalidChannelName(label) ?? new Error("label is empty")); } if (secret !== undefined && secret.trim() === "") { return errorResult(new BridgeError(BridgeErrorCode.INVALID_INPUT, "The channel password is empty", "Pass the password you were given, or omit `secret` to have one generated.")); } label = label?.trim() || undefined; secret = secret?.trim() || undefined; orgId = orgId?.trim() || undefined; const config = getConfig(); // Auto-route by Org_ID, exactly as the join does: the Org_ID is the discriminant. const endpoint = orgId ? config.COGENT_TEAM_ENDPOINT : config.COGENT_ENDPOINT; if (!endpoint) { return errorResult(new Error(orgId ? "COGENT_TEAM_ENDPOINT not configured. Set it to create Team (Org_ID) channels." : "COGENT_ENDPOINT not configured. Set it to use cloud mode.")); } if (label !== undefined) { const invalid = invalidChannelName(label); if (invalid) return errorResult(invalid); } // Generate secret if not provided (32 hex chars, well above min 8) const secretWasGenerated = secret === undefined; const sessionSecret = secret ?? crypto.randomBytes(16).toString("hex"); // POST to create session (unauthenticated endpoint -- no token yet). // The free request body is unchanged: no orgId key unless one was given. const createBody = orgId ? { secret: sessionSecret, label, orgId } : { secret: sessionSecret, label }; const resp = await fetch(`${endpoint}/api/sessions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(createBody), }); if (!resp.ok) { const body = await resp.text().catch(() => ""); // Two agents starting cold both get CHANNEL_NOT_FOUND and both create; the loser lands // here. The relay's own hint is "Choose a different label" — the one thing it must not do. if (resp.status === 400 && body.includes("already in use")) { return errorResult(new Error(`Channel '${label}' already exists (already in use). Call cogent_join_session with ` + `the same name, secret${orgId ? " and Org_ID" : ""}. Do not choose another name.`)); } return errorResult(new Error(`Failed to create session: HTTP ${resp.status}${body ? ` - ${body}` : ""}`)); } const data = (await resp.json()); // Persist credentials locally (include label for future resolution) await saveCredentials({ endpoint, sessionId: data.sessionId, token: data.token, label: data.label, savedAt: new Date().toISOString(), }); // Gates the persisted-credentials fallback in register-peer / startup for this process. markJoinedInProcess(); // Reinitialize cloud backend with new credentials. // Uses dynamic import to avoid compile-time dependency on startup.ts // (reinitCloudBackend is added in Plan 05). Cast to Record to avoid // TS2339 since the export doesn't exist yet. try { const startupModule = (await import("../startup.js")); if (typeof startupModule.reinitCloudBackend === "function") { await startupModule.reinitCloudBackend(endpoint, data.sessionId, data.token); logger.info("Cloud backend reinitialized with new session credentials"); } } catch (err) { // reinitCloudBackend may not exist yet (Plan 05 adds it). // Credentials are persisted, so a restart will pick them up. logger.debug(`Backend reinit not available: ${err}`); } const channelName = data.label ?? data.sessionId; return successResult({ success: true, created: true, sessionId: data.sessionId, label: data.label, token: data.token, createdAt: data.createdAt, slackMapCommand: slackMapCommand({ team: !!orgId, label: channelName, sessionId: data.sessionId }), // A secret the CALLER supplied is never echoed back: the caller already has it, and a // tool result can be quoted into a channel by an auto-reply. Only a secret this tool // generated is returned — nothing else knows it. ...(secretWasGenerated ? { secret: sessionSecret, config_snippet: { env_vars: `COGENT_ENDPOINT=${endpoint}\n` + `COGENT_SESSION_ID=${channelName}\n` + `COGENT_SECRET=${sessionSecret}\n` + `COGENT_PLATFORM=${config.COGENT_PLATFORM}`, mcp_config: { "cogent": { command: "npx", args: ["-y", "@essentialai/cogent-bridge"], env: { COGENT_ENDPOINT: endpoint, COGENT_SESSION_ID: channelName, COGENT_SECRET: sessionSecret, COGENT_PLATFORM: config.COGENT_PLATFORM, }, }, }, }, } : {}), note: `${orgId ? "Team channel" : "Channel"} "${channelName}" was created by this call and you are joined to it. ` + `Credentials saved locally. Cloud backend updated. Next: cogent_register_peer.`, }); } catch (err) { logger.error("create-session failed", { error: err }); return errorResult(err); } }); } //# sourceMappingURL=create-session.js.map