/** * Runtime feature flags. Backed by a tiny single-row table so an admin * can flip a flag and have every Vibn pod pick it up within seconds (no * redeploy required). * * Currently used for: * - path_b_disabled : kill switch for the Path B AI dev-container * architecture. When true, shell.exec / fs.* / * devcontainer.* tools return 503 and the chat * system prompt falls back to Path A guidance. * * See AI_PATH_B_EXECUTION_PLAN.md ยง7 for the rollback story. */ import { query, queryOne } from '@/lib/db-postgres'; let tableReady = false; async function ensureFlagsTable(): Promise { if (tableReady) return; await query( `CREATE TABLE IF NOT EXISTS fs_feature_flags ( key TEXT PRIMARY KEY, value JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() );`, [], ); tableReady = true; } const TTL_MS = 10_000; const cache = new Map(); export async function getFlag(key: string, defaultValue: T): Promise { const cached = cache.get(key); if (cached && cached.expires > Date.now()) return cached.value as T; await ensureFlagsTable(); const row = await queryOne<{ value: T }>( `SELECT value FROM fs_feature_flags WHERE key = $1 LIMIT 1`, [key], ); const value = row?.value ?? defaultValue; cache.set(key, { value, expires: Date.now() + TTL_MS }); return value; } export async function setFlag(key: string, value: unknown): Promise { await ensureFlagsTable(); await query( `INSERT INTO fs_feature_flags (key, value, updated_at) VALUES ($1, $2::jsonb, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, [key, JSON.stringify(value)], ); cache.delete(key); } export async function isPathBDisabled(): Promise { return Boolean(await getFlag('path_b_disabled', false)); }