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.

204 lines • 12.4 kB
import { z } from "zod"; import os from "node:os"; import path from "node:path"; const defaultStatePath = path.join(os.homedir(), ".cogent"); /** The hosted FREE relay — the zero-config default endpoint (2026-08-06). */ export const DEFAULT_FREE_ENDPOINT = "https://cogent.tools"; export const configSchema = z.object({ COGENT_STATE_PATH: z.string().default(defaultStatePath), // Relay URL for free (password) channels. **Zero-config default = the hosted FREE // relay (cogent.tools)** — see the loadConfig() resolution below. A hand-rolled MCP // config that OMITTED this used to silently run LOCAL and never reach any cloud // channel (the marvin-coder confusion, 2026-08-06); defaulting it makes "cloud" the // out-of-the-box mode. Opt OUT to local/offline mode with COGENT_LOCAL. An explicit // value (a free self-host URL, or the Team URL for a custom Team agent) always wins. COGENT_ENDPOINT: z.string().optional(), // **Local mode opt-out.** Set truthy (1/true/yes/on) to force LOCAL file-based state // even when COGENT_ENDPOINT would otherwise default to the free relay. This is the // switch for offline use, air-gapped/self-contained agents, and (upcoming) local-LLM // operation where no cloud relay is wanted. When set, it wins over any COGENT_ENDPOINT. COGENT_LOCAL: z.string().optional(), // Team (business) relay. cogent_join_session auto-routes here when an Org_ID is // supplied (free joins use COGENT_ENDPOINT). Default = the hosted Team relay; a // self-hosted Team deployment overrides it. No runtime flipping — the presence of // an Org_ID on the join is the discriminant. COGENT_TEAM_ENDPOINT: z.string().default("https://app.cogent.tools"), COGENT_SESSION_ID: z.string().optional(), COGENT_SECRET: z.string().optional(), COGENT_POLL_INTERVAL_MS: z.coerce.number().int().min(1000).default(5000), COGENT_TIMEOUT_MS: z.coerce.number().int().min(1000).default(300_000), // Inline fast-path wait for a directed send's reply. The actual inline // wait is min(COGENT_FASTPATH_MS, COGENT_TIMEOUT_MS); COGENT_TIMEOUT_MS // stays the absolute ceiling. A reply within this window returns inline; // beyond it the send returns success+pending and the reply arrives async // via the existing inject-only path (260527-mkz). Default 120s: a woken // agent's real reply routinely needs >30s to generate, so the sender should // wait inline longer before falling back to pending (operator, 2026-07-24). COGENT_FASTPATH_MS: z.coerce.number().int().min(1000).default(120_000), COGENT_CHAR_LIMIT: z.coerce.number().int().min(0).default(0), COGENT_LOG_LEVEL: z .enum(["debug", "info", "warn", "error"]) .default("info"), COGENT_CLAUDE_PATH: z.string().default("claude"), COGENT_STALE_TIMEOUT_MS: z.coerce.number().int().min(0).default(1_800_000), COGENT_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().min(0).default(60_000), COGENT_WS_STALE_MS: z.coerce.number().int().positive().default(90_000), COGENT_PLATFORM: z .enum(["cc", "codex", "slack", "gchat", "web"]) .default("cc"), COGENT_CODEX_PATH: z.string().default("codex"), COGENT_CODEX_SANDBOX: z .enum(["full-auto", "read-only", "workspace-write", "danger-full-access", "bypass"]) .default("full-auto"), // Codex real-time-wake rail selector. codex >=0.145 has a SINGLE-WRITER // thread-store: a codex agent running in an interactive TUI holds an exclusive // writer on its own thread, so the current rail (`exec-resume` = // `codex exec resume <same id>`) fast-fails `-32600 already has an active // writer` and the peer never gets a real-time reply. The `app-server` rail // injects the peer's message as a turn THROUGH codex's app-server daemon (the // daemon is the single writer; the human's `codex --remote` TUI and the bridge // are both just clients → no conflict). DEFAULT stays `exec-resume` so this is // purely additive/opt-in: nothing changes for any agent until a codex user // launches via the daemon (see bin/cogent-codex) and sets this to `app-server`. // Only affects COGENT_PLATFORM=codex; claude/gemini/mail rails never read it. COGENT_CODEX_WAKE: z .enum(["auto", "exec-resume", "app-server"]) .default("auto"), // Auto-recovery for a blocked Codex sandbox. On a host where the OS sandbox // can't initialize (unprivileged user namespaces blocked — Ubuntu 24.04's // apparmor_restrict_unprivileged_userns, containers, hardened kernels), // `codex exec resume` fails every file/test command and the agent can only // report "not finished". DEFAULT ON (unset = on): the startup sandbox probe // (codex-preflight) auto-switches the effective sandbox to `bypass` so tasks // complete — but ONLY on a PROVABLY-blocked host (probe status "blocked", // never "ok"/"unknown"). On such a host codex cannot run sandboxed either way, // so this restores function rather than weakening a working sandbox, and it is // logged loudly (never silent). This is a product-wide fix so every Codex agent // on a hardened host self-heals with no per-box config. Opt OUT for strict // environments by setting it to "0"/"false"/"no"/"off" (Cogent then WARNS + // surfaces the failure but leaves the sandbox strict). Because this is a // full-access feature, parsing FAILS SAFE: unset = ON, only explicit // affirmatives ("true"/"1"/"yes"/"on"/"enable"/"enabled") keep it ON, and ANY // other SET value — empty string, "disable", a typo — resolves to OFF rather // than silently leaving the bypass enabled. COGENT_CODEX_SANDBOX_FALLBACK: z .string() .optional() .transform((v) => v === undefined ? true : ["true", "1", "yes", "on", "enable", "enabled"].includes(v.trim().toLowerCase())), // Wake-trace resolver: when >1 candidate session share a cwd and none is the // pinned session, the newest must lead the runner-up by at least this many ms // to be trusted; otherwise the wake is refused (never guess silently). 60s bias // toward safe refusal over a wrong-session guess; env-tunable. COGENT_RESOLVER_AMBIGUOUS_MARGIN_MS: z.coerce .number() .int() .min(0) .default(60_000), // Slice 4 (Story 8.5): anti-noise A2A default. A BROADCAST (toPeerId "*") must // NOT trigger an automatic subprocess wake, so addressing the whole channel // never starts an automatic multi-agent debate ("no auto debate unless the // human asks"). Directed messages still auto-relay. Opt a peer INTO broadcast // auto-relay by setting this to "true"/"1". Default (unset) = manual broadcast. COGENT_BROADCAST_AUTORELAY: z .string() .optional() .transform((v) => v === "true" || v === "1"), // When an agent is auto-woken to answer an incoming message, MIRROR the incoming // scope in the reply: a message addressed to the whole channel (toPeerId "*") gets // a BROADCAST reply so every peer sees the answer; a directed / P2P message still // replies to the sender only. Default ON (the intuitive group-chat semantics — // "broadcast question → broadcast answer"). Opt out with "false"/"0" to restore the // legacy always-reply-to-sender behavior. COGENT_REPLY_BROADCAST: z .string() .optional() .transform((v) => v !== "false" && v !== "0"), // Full-group-chat awareness. When ON, an agent-origin BROADCAST from a PEER // (another agent addressing the whole channel, toPeerId "*") is INJECTED into the // local session so this agent SEES what its peers said — but NO reply is relayed // (inject-only, response discarded) and the notice forbids calling any cogent_* // tool, so it can never start a broadcast storm. Humans still drive real replies. // Default ON (3.10.1) — the group-chat behavior must reach every agent with ZERO // config, incl. Team agents on a custom (non-plugin) config; opt out with // "false"/"0". Only fires for multi-agent channels (own broadcast is skipped; // single-agent channels have no peer to inject). Distinct from // COGENT_BROADCAST_AUTORELAY, which makes agents fully AUTO-REPLY (storm-prone; not this). COGENT_BROADCAST_AWARENESS: z .string() .optional() .transform((v) => v !== "false" && v !== "0"), // Opt IN to the legacy "manual broadcast" behavior: agents do NOT auto-answer a // fresh channel question (toPeerId "*") — a human drives the reply. Default OFF: // the operator invariant is that an agent ALWAYS wakes + answers a fresh channel // message. Always-answering is storm-safe because an agent's own auto-reply is // tagged isRelayEcho and is never re-answered by peers (see auto-relay // handleIncoming's echo guard). COGENT_BROADCAST_AUTORELAY still force-answers // even in manual mode (back-compat). COGENT_BROADCAST_MANUAL: z .string() .optional() .transform((v) => v === "true" || v === "1"), // C (check-on-stop): after each turn, a plugin Stop hook silently polls for // directed / human-broadcast messages that arrived while this agent was BUSY // (so B's real-time wake was missed) and hands any unanswered ones back to the // agent to reply. Default ON. Opt out with "false"/"0". Read by the standalone // hook (plugin/hooks/check-on-stop.mjs) from the process env; mirrored in the // schema so the value is documented + testable in one place. See // docs/superpowers/specs/2026-07-29-cogent-wake-reliability-design.md. COGENT_CHECK_ON_STOP: z .string() .optional() .transform((v) => v !== "false" && v !== "0"), // Which messages C acts on. CSV of tokens: "directed" (toPeerId === me) and/or // "human-broadcast" (toPeerId === "*" from a human origin: slack/gchat/web). // Default both. Agent-origin broadcasts are never auto-surfaced by C (storm-safe; // COGENT_BROADCAST_AWARENESS already handles seeing peer broadcasts). COGENT_CHECK_ON_STOP_SCOPE: z.string().default("directed,human-broadcast"), // Cogent Mail F4 — the mailbox domains an agent may configure + send to. CSV; default is the // single Cogent mail domain. Cogent mail is ORGANISATION-ONLY (compliance: an accountable org // owns every mailbox), so BYO external / corporate-domain mailboxes are BLOCKED for now — this // allowlist is the configurable seam for adding per-org corporate domains in a future release. // Baked default here (a LIBRARY default, not plugin env) so Team agents get it too. COGENT_MAIL_ALLOWED_DOMAINS: z.string().default("mail.cogent.tools"), }); let _config = null; /** Truthy parse for the COGENT_LOCAL opt-out (fail-safe: only explicit affirmatives count). */ function isLocalOptOut(value) { if (value === undefined) return false; return ["1", "true", "yes", "on", "enable", "enabled"].includes(value.trim().toLowerCase()); } export function loadConfig(env = process.env) { const result = configSchema.safeParse(env); if (!result.success) { const issues = result.error.issues .map((i) => ` ${i.path.join(".")}: ${i.message}`) .join("\n"); throw new Error(`Invalid configuration:\n${issues}`); } // Resolve the EFFECTIVE free endpoint (the Local/Cloud mode decision): // COGENT_LOCAL truthy → LOCAL (endpoint cleared, wins over everything) // explicit COGENT_ENDPOINT → that URL (free self-host, or a custom Team agent's URL) // neither → the hosted FREE relay (zero-config cloud default) // COGENT_TEAM_ENDPOINT is untouched — an Org_ID join still routes there (join-session.ts). const data = { ...result.data }; if (isLocalOptOut(data.COGENT_LOCAL)) { data.COGENT_ENDPOINT = undefined; } else if (!data.COGENT_ENDPOINT) { data.COGENT_ENDPOINT = DEFAULT_FREE_ENDPOINT; } _config = Object.freeze(data); return _config; } export function getConfig() { if (!_config) throw new Error("Config not loaded. Call loadConfig() first."); return _config; } export function resetConfig() { _config = null; } //# sourceMappingURL=config.js.map