@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
JavaScript
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