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.

173 lines • 7.16 kB
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