@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.
173 lines • 7.16 kB
JavaScript
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
/**
* Legacy global credential file used by cogent-bridge < 3.1.8.
* A single file shared across every CC session on the host -- which is
* exactly the cross-cwd "credential bleed" bug 3.1.8 fixes. Kept here
* as a constant so startup can detect-and-warn on upgrade.
*/
export const LEGACY_GLOBAL_CREDENTIAL_PATH = path.join(os.homedir(), ".cogent-credentials.json");
/**
* Per-cwd default location for credentials.
*
* ~/.cogent/credentials/<sha256(absoluteCwd)[..16]>.json
*
* Each working directory gets its own credential file, so a /cogent:register
* in project A cannot inherit the cloud session that project B last joined.
* The 16-char prefix of the SHA is enough to make collisions astronomically
* unlikely while keeping filenames short.
*/
function defaultCredentialPath(cwd = process.cwd()) {
const absoluteCwd = path.resolve(cwd);
const hash = crypto
.createHash("sha256")
.update(absoluteCwd)
.digest("hex")
.slice(0, 16);
return path.join(os.homedir(), ".cogent", "credentials", `${hash}.json`);
}
/**
* Resolve the credential path with the override hierarchy:
* 1. explicit `credentialPath` arg (tests, advanced users)
* 2. process.env.COGENT_CREDENTIALS_FILE (operators with custom paths)
* 3. defaultCredentialPath(process.cwd()) (per-cwd, the new default)
*/
export function resolveCredentialPath(credentialPath) {
if (credentialPath)
return credentialPath;
const envOverride = process.env.COGENT_CREDENTIALS_FILE;
if (envOverride)
return envOverride;
return defaultCredentialPath();
}
// ---------------------------------------------------------------------------
// Joined-in-process flag
// ---------------------------------------------------------------------------
//
// register-peer falls back to persisted credentials only if cogent_join_session
// (or cogent_create_session) has been called in this MCP process. This guards
// against silently using stale or another-cwd's creds. Override with
// COGENT_TRUST_PERSISTED_CREDENTIALS=1 if you really want the old behavior.
let joinedInProcessFlag = false;
export function markJoinedInProcess() {
joinedInProcessFlag = true;
}
export function hasJoinedInProcess() {
return joinedInProcessFlag;
}
/**
* Test-only reset hook. Production code should never call this.
*/
export function __resetJoinedInProcessForTests() {
joinedInProcessFlag = false;
}
// ---------------------------------------------------------------------------
// Cloud-attempt flag (distinct from joined-in-process)
// ---------------------------------------------------------------------------
//
// markJoinedInProcess() flips only on a SUCCESSFUL join/create. This second
// flag flips as soon as cogent_join_session / cogent_create_session is ENTERED,
// before the network call — so register-peer can tell the difference between
// "no cloud attempt was ever made" (legit local mode / env-configured session)
// and "a cloud join/create was tried and did NOT succeed". In the latter case
// register-peer must fail loud instead of silently falling back to the local
// FileBackend or auto-creating a throwaway free channel (T0.1, shipped 3.12.5 — a failed
// Team join otherwise registers success:true on the WRONG channel, invisible to
// every peer).
//
// A new attempt also INVALIDATES any earlier success, so "joined" always means "the LATEST
// join/create succeeded" (COG-33). Otherwise a free join followed by a failed Team join left
// joined=true, the guard stayed silent, and register landed on the still-active FREE backend.
let cloudAttemptInProcessFlag = false;
export function markCloudAttemptInProcess() {
cloudAttemptInProcessFlag = true;
joinedInProcessFlag = false;
}
export function hasCloudAttemptInProcess() {
return cloudAttemptInProcessFlag;
}
/**
* Test-only reset hook. Production code should never call this.
*/
export function __resetCloudAttemptInProcessForTests() {
cloudAttemptInProcessFlag = false;
}
// ---------------------------------------------------------------------------
// Load / save / clear
// ---------------------------------------------------------------------------
/**
* Load persisted credentials from disk.
* Returns null on any error (file not found, JSON parse failure, etc.).
*/
export async function loadCredentials(credentialPath) {
const filePath = resolveCredentialPath(credentialPath);
try {
const raw = await fs.readFile(filePath, "utf-8");
return JSON.parse(raw);
}
catch {
return null;
}
}
/**
* Save credentials to disk as pretty-printed JSON.
* Creates the parent directory if it does not exist.
*/
export async function saveCredentials(creds, credentialPath) {
const filePath = resolveCredentialPath(credentialPath);
// Bearer token is a secret: keep the directory owner-only and the file
// 0600 so another user on a shared host cannot read it. mkdir/writeFile
// `mode` only applies on creation, so chmod afterward to also tighten a
// pre-existing (possibly world-readable) file written by an older version.
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
await fs.writeFile(filePath, JSON.stringify(creds, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
await fs.chmod(filePath, 0o600);
}
/**
* Persist a peerSecret the relay just issued for `peerId` in `sessionId`.
*
* 🔴 The relay returns a secret only when it MINTS one (the peer was created, re-created after an
* eviction, or adopted) and never again. Every register path must keep it: the self-heal
* re-registration and the startup presence restore used to discard it, so the next re-join (a new
* token) left the agent holding a dead secret and PEER_NOT_YOURS forever (klaire-frontend,
* 2026-09-30). Credentials for another channel are left alone.
*/
export async function rememberIssuedPeerSecret(sessionId, peerId, peerSecret) {
const creds = await loadCredentials();
if (!creds || creds.sessionId !== sessionId)
return;
if (creds.peerId === peerId && creds.peerSecret === peerSecret)
return;
await saveCredentials({ ...creds, peerId, peerSecret, savedAt: new Date().toISOString() });
}
/**
* Delete the credential file from disk.
* Silently ignores ENOENT (file already absent).
*/
export async function clearCredentials(credentialPath) {
const filePath = resolveCredentialPath(credentialPath);
try {
await fs.unlink(filePath);
}
catch (err) {
if (err.code !== "ENOENT") {
throw err;
}
}
}
/**
* Detect whether the legacy global credential file exists. Used by startup
* to warn the operator about the path migration on first run after upgrade.
*/
export async function legacyGlobalCredentialsExist() {
try {
await fs.access(LEGACY_GLOBAL_CREDENTIAL_PATH);
return true;
}
catch {
return false;
}
}
//# sourceMappingURL=credential-store.js.map