@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.
209 lines • 12.9 kB
JavaScript
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"),
// COG-86: what a Claude wake may do. `read` (default) = Read, Glob and Grep only: the wake answers but cannot edit,
// run commands or commit. A wake resumes the SAME session a human may have open in a terminal, and nothing can
// tell reliably that the terminal is there; on 2026-10-15 a full-tool wake edited a live session's files, then
// committed and pushed. `full` = every tool (the behaviour before 3.25.9), for an agent no human drives.
COGENT_WAKE_TOOLS: z.enum(["read", "full"]).default("read"),
// 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