Kicks off Path B (AI_PATH_B_EXECUTION_PLAN.md): each Vibn project gets
its own vibn-dev Coolify service that the AI drives directly via shell
and filesystem tools. Sub-second iteration vs the 5-min Gitea redeploy
loop.
What's in this commit (week 1, slice 1):
- vibn-dev Dockerfile: small Ubuntu base (~500 MB target). git, ripgrep,
python3, mise. Language toolchains lazy-install on first use.
- lib/dev-container.ts: ensureDevContainer / suspend / resume /
execInDevContainer. Backed by a new fs_project_dev_containers table.
- lib/feature-flags.ts + /api/admin/path-b/{disable,enable}: kill switch.
Bearer NEXTAUTH_SECRET flips path_b_disabled, propagates in ~10s.
- New MCP tools wired into /api/mcp: devcontainer.{ensure,status,suspend},
shell.exec, fs.{read,write,edit,list,delete,glob,grep}. All enforce
workspace isolation via fs_projects ownership check.
- vibn-tools.ts: 11 new Gemini tool defs (smoke test passes, 63 total).
- chat system prompt: shell-first guidance; gitea_file_* marked
deprecated for iterative work (still available, removed week 3).
Safety nets baked in:
- pathBGuard() returns 503 from every Path B tool when the kill switch
flips
- fs.* paths locked to /workspace
- ensureResourceInWorkspaceProjects via fs_project_dev_containers PK
- per-project resource limits (1 vCPU, 1 GiB RAM) on the compose spec
Still pending (queued):
- dev_server.* (preview URLs through Traefik)
- ship tool (push to Gitea + trigger prod deploy)
- auto-push autosave to vibn-autosave/main every 5 min
- idle-suspend cron after 30 min inactivity
- HMR-through-Traefik spike
- eval harness
Made-with: Cursor
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<string, { value: any; expires: number }>();
|
|
|
|
export async function getFlag<T = unknown>(key: string, defaultValue: T): Promise<T> {
|
|
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<void> {
|
|
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<boolean> {
|
|
return Boolean(await getFlag<boolean>('path_b_disabled', false));
|
|
}
|