From 006226cce5ea338da1db2b4d796d616a409cbd3e Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Mon, 25 May 2026 01:20:29 +0000 Subject: [PATCH] =?UTF-8?q?v2.0.0-alpha:=20BooCoder=20foundation=20?= =?UTF-8?q?=E2=80=94=20container,=20schema,=20DB=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of v2.0. BooCoder is live at port 9502 with a health endpoint. - Database renamed: ALTER DATABASE boocode RENAME TO boochat (one-time). All services updated to connect to /boochat. Docker service name stays boocode_db (rename is internal to Postgres, not Docker). - New apps/coder/ app skeleton: Fastify server with health endpoint, postgres connection, schema apply on boot. Mirrors apps/server pattern but minimal (no inference loop yet — Phase 2). - Schema: pending_changes (operation queue before /apply), tasks (dispatch DAG with state machine), available_agents (startup-probed agent registry), human_inbox view (tasks WHERE state IN blocked/failed). All IF NOT EXISTS, idempotent on re-run. Same boochat database, different tables. - Dockerfile: Node 20 bookworm-slim (glibc for future node-pty in Phase 5). Multi-stage build matching the existing boocode image pattern. - docker-compose.yml: boocoder service on 100.114.205.53:9502, /opt:/opt:rw mount (write-capable, policy-gated at tool layer), depends on boocode_db. - BOOCODER.md: container guidance declaring write-tool capability + pending-changes discipline. All 4 services boot and pass health checks. 9 tables in the shared DB. Co-Authored-By: Claude Opus 4.7 (1M context) --- BOOCODER.md | 41 ++++++++++++++++------------- apps/coder/Dockerfile | 28 ++++++++++++++++++++ apps/coder/package.json | 25 ++++++++++++++++++ apps/coder/src/config.ts | 28 ++++++++++++++++++++ apps/coder/src/db.ts | 45 ++++++++++++++++++++++++++++++++ apps/coder/src/index.ts | 55 +++++++++++++++++++++++++++++++++++++++ apps/coder/src/schema.sql | 48 ++++++++++++++++++++++++++++++++++ apps/coder/tsconfig.json | 15 +++++++++++ docker-compose.yml | 28 +++++++++++++++++--- pnpm-lock.yaml | 28 ++++++++++++++++++++ 10 files changed, 320 insertions(+), 21 deletions(-) create mode 100644 apps/coder/Dockerfile create mode 100644 apps/coder/package.json create mode 100644 apps/coder/src/config.ts create mode 100644 apps/coder/src/db.ts create mode 100644 apps/coder/src/index.ts create mode 100644 apps/coder/src/schema.sql create mode 100644 apps/coder/tsconfig.json diff --git a/BOOCODER.md b/BOOCODER.md index b1556e0..3638be0 100644 --- a/BOOCODER.md +++ b/BOOCODER.md @@ -1,27 +1,32 @@ -# BooCoder +# BooCoder — Container Guidance -> (Stub. v2.0 implementation pending. This file documents the intended contract.) +You are BooCoder, a write-capable coding agent. You can read AND modify files within the project scope. -## Capabilities +## You can -- Everything in `BOOCHAT.md` -- Write tools (pending): `write_file`, `edit_file`, `delete_file` (all gated through pending-changes sandbox) -- Shell (pending): `run_command` (Docker-isolated per-session) +- Read files (view_file, list_dir, grep, find_files) +- Edit files (edit_file, create_file, delete_file) — all changes queue in pending_changes +- Apply pending changes to disk (apply_pending) +- Revert applied changes (rewind) +- Dispatch tasks to external agents (dispatch_external_agent) +- Use MCP tools from configured servers -## Constraints +## You cannot -- All writes land in a pending-changes virtual layer; nothing touches the real filesystem until `/apply` -- `run_command` executes inside the session sandbox, not the host -- No git commits, pushes, or pulls — Sam owns those -- Stop and ask before destructive operations (delete, overwrite, recreate) +- Write outside the project root (path-guard enforced) +- Write to secret files (.env, *.pem, id_rsa*, credentials.json) +- Apply changes without explicit user approval (unless auto-apply is enabled per task) +- Push to git remotes +- Access the internet except via configured MCP servers + +## Pending changes discipline + +Every file modification queues in `pending_changes` before touching disk. The user sees a diff preview and approves/rejects each change. Never bypass this queue — it is the safety boundary between inference and the filesystem. ## Behavior -- Show a diff preview before any write -- Group related edits into a single `/apply` batch -- If a tool fails, surface the error verbatim — don't paper over it +- Show diffs clearly. Explain what you're changing and why. +- For multi-file changes, organize as a logical unit (one task = one coherent change set). +- If uncertain about scope, use smaller edits and verify between steps. +- Cite file paths + line numbers for context. - Verify before reporting work complete: run the relevant test/build/smoke and confirm output matches the claim. Evidence first, assertion second. - -## Convention: rules vs recipes - -Always-true rules live here, in `BOOCHAT.md`, and in `CLAUDE.md` (100% present each turn). On-demand recipes live in `/data/skills/` (roughly 6% invoke rate in multi-turn per Codeminer42, 2026). Don't file workflow rules as skills — they misfire. See Anthropic agent-skills best-practices (platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). diff --git a/apps/coder/Dockerfile b/apps/coder/Dockerfile new file mode 100644 index 0000000..5417789 --- /dev/null +++ b/apps/coder/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.7 + +FROM node:20-alpine AS builder +RUN corepack enable +WORKDIR /build + +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.base.json ./ +COPY apps/coder/package.json ./apps/coder/ + +RUN pnpm install --frozen-lockfile + +COPY apps/coder ./apps/coder + +RUN pnpm -C apps/coder build + +RUN pnpm deploy --filter=@boocode/coder --prod --legacy /out/coder + + +FROM node:20-bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends ripgrep git && rm -rf /var/lib/apt/lists/* +WORKDIR /app + +COPY --from=builder /out/coder ./ + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["node", "dist/index.js"] diff --git a/apps/coder/package.json b/apps/coder/package.json new file mode 100644 index 0000000..73a1d1e --- /dev/null +++ b/apps/coder/package.json @@ -0,0 +1,25 @@ +{ + "name": "@boocode/coder", + "version": "2.0.0", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc && node -e \"import('node:fs').then(fs=>fs.copyFileSync('src/schema.sql','dist/schema.sql'))\"", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@fastify/static": "^7.0.4", + "@fastify/websocket": "^10.0.1", + "fastify": "^4.28.1", + "postgres": "^3.4.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "tsx": "^4.16.2", + "typescript": "^5.5.0" + } +} diff --git a/apps/coder/src/config.ts b/apps/coder/src/config.ts new file mode 100644 index 0000000..49cdb83 --- /dev/null +++ b/apps/coder/src/config.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +const ConfigSchema = z.object({ + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT: z.coerce.number().int().positive().default(3000), + HOST: z.string().default('0.0.0.0'), + DATABASE_URL: z.string().url(), + LLAMA_SWAP_URL: z.string().url(), + PROJECT_ROOT_WHITELIST: z.string().default('/opt'), + LOG_LEVEL: z.string().default('info'), + CONTAINER_GUIDANCE_FILE: z.string().optional(), +}); + +export type Config = z.infer; + +let cached: Config | null = null; + +export function loadConfig(): Config { + if (cached) return cached; + const parsed = ConfigSchema.safeParse(process.env); + if (!parsed.success) { + console.error('Invalid environment configuration:'); + console.error(parsed.error.flatten().fieldErrors); + process.exit(1); + } + cached = parsed.data; + return cached; +} diff --git a/apps/coder/src/db.ts b/apps/coder/src/db.ts new file mode 100644 index 0000000..0a1947b --- /dev/null +++ b/apps/coder/src/db.ts @@ -0,0 +1,45 @@ +import postgres from 'postgres'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import type { Config } from './config.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export type Sql = ReturnType; + +let sqlInstance: Sql | null = null; + +export function getSql(config: Config): Sql { + if (sqlInstance) return sqlInstance; + sqlInstance = postgres(config.DATABASE_URL, { + max: 10, + idle_timeout: 30, + connect_timeout: 10, + onnotice: () => {}, + }); + return sqlInstance; +} + +export async function applySchema(sql: Sql): Promise { + const schemaPath = resolve(__dirname, 'schema.sql'); + const ddl = await readFile(schemaPath, 'utf8'); + await sql.unsafe(ddl); +} + +export async function pingDb(sql: Sql): Promise { + try { + await sql`SELECT 1`; + return true; + } catch { + return false; + } +} + +export async function closeDb(): Promise { + if (sqlInstance) { + await sqlInstance.end({ timeout: 5 }); + sqlInstance = null; + } +} diff --git a/apps/coder/src/index.ts b/apps/coder/src/index.ts new file mode 100644 index 0000000..29a0873 --- /dev/null +++ b/apps/coder/src/index.ts @@ -0,0 +1,55 @@ +import Fastify from 'fastify'; +import { loadConfig } from './config.js'; +import { getSql, applySchema, pingDb, closeDb } from './db.js'; + +async function main() { + const config = loadConfig(); + + const app = Fastify({ + logger: { level: config.LOG_LEVEL }, + }); + + // Allow empty JSON bodies (same pattern as apps/server). + app.removeContentTypeParser(['application/json']); + app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => { + const str = (body as string) ?? ''; + if (str.trim().length === 0) { + done(null, {}); + return; + } + try { + done(null, JSON.parse(str)); + } catch (err) { + done(err as Error, undefined); + } + }); + + const sql = getSql(config); + await applySchema(sql); + app.log.info('database schema applied'); + + // Health endpoint + app.get('/api/health', async (_req, reply) => { + const dbOk = await pingDb(sql); + const status = dbOk ? 200 : 503; + return reply.status(status).send({ ok: dbOk, db: dbOk }); + }); + + // Graceful shutdown + const shutdown = async () => { + app.log.info('shutting down'); + await app.close(); + await closeDb(); + process.exit(0); + }; + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); + + await app.listen({ port: config.PORT, host: config.HOST }); + app.log.info(`BooCoder listening on ${config.HOST}:${config.PORT}`); +} + +main().catch((err) => { + console.error('fatal:', err); + process.exit(1); +}); diff --git a/apps/coder/src/schema.sql b/apps/coder/src/schema.sql new file mode 100644 index 0000000..76ac92f --- /dev/null +++ b/apps/coder/src/schema.sql @@ -0,0 +1,48 @@ +-- v2.0.0: BooCoder schema — pending changes, tasks, agent registry. +-- Applied on startup by apps/coder/src/db.ts:applySchema(). +-- Lives in the same 'boochat' database as BooChat's tables. + +CREATE TABLE IF NOT EXISTS pending_changes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + session_id UUID NOT NULL, + task_id UUID, + file_path TEXT NOT NULL, + operation TEXT NOT NULL, + diff TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT pending_changes_operation_chk CHECK (operation IN ('create', 'edit', 'delete')), + CONSTRAINT pending_changes_status_chk CHECK (status IN ('pending', 'applied', 'rejected', 'reverted')) +); + +CREATE TABLE IF NOT EXISTS tasks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL, + parent_task_id UUID REFERENCES tasks(id), + state TEXT NOT NULL DEFAULT 'pending', + input TEXT NOT NULL, + output_summary TEXT, + agent TEXT, + model TEXT, + execution_path TEXT, + worktree_path TEXT, + cost_tokens INTEGER, + started_at TIMESTAMPTZ, + ended_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT tasks_state_chk CHECK (state IN ('pending', 'running', 'completed', 'failed', 'blocked', 'cancelled')), + CONSTRAINT tasks_execution_path_chk CHECK (execution_path IS NULL OR execution_path IN ('native', 'acp', 'pty')) +); + +CREATE TABLE IF NOT EXISTS available_agents ( + name TEXT PRIMARY KEY, + install_path TEXT, + version TEXT, + supports_acp BOOLEAN NOT NULL DEFAULT false, + supports_mcp_client BOOLEAN NOT NULL DEFAULT false, + last_probed_at TIMESTAMPTZ +); + +-- Human inbox: tasks needing attention +CREATE OR REPLACE VIEW human_inbox AS + SELECT * FROM tasks WHERE state IN ('blocked', 'failed'); diff --git a/apps/coder/tsconfig.json b/apps/coder/tsconfig.json new file mode 100644 index 0000000..fe31069 --- /dev/null +++ b/apps/coder/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2022"], + "types": ["node"], + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["src/**/__tests__/**", "**/*.test.ts"] +} diff --git a/docker-compose.yml b/docker-compose.yml index 5017c81..58f05e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: CODECONTEXT_URL: http://codecontext:8080 CONTAINER_GUIDANCE_FILE: /app/BOOCHAT.md - DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boocode + DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boochat volumes: - /opt:/opt - /opt/projects:/opt/projects:rw @@ -41,7 +41,7 @@ services: environment: NODE_ENV: production PORT: 3000 - DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boocode + DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boochat volumes: - /opt:/opt:rw - /home/samkintop:/home/samkintop:rw @@ -50,6 +50,28 @@ services: networks: - boocode_net + boocoder: + build: + context: . + dockerfile: apps/coder/Dockerfile + container_name: boocoder + restart: unless-stopped + ports: + - "100.114.205.53:9502:3000" + env_file: .env + environment: + CONTAINER_GUIDANCE_FILE: /app/BOOCODER.md + DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boochat + volumes: + - /opt:/opt:rw + - /opt/projects:/opt/projects:rw + - ./data:/data + - /opt/boocode/BOOCODER.md:/app/BOOCODER.md:ro + depends_on: + - boocode_db + networks: + - boocode_net + boocode_db: image: postgres:16-alpine container_name: boocode_db @@ -57,7 +79,7 @@ services: environment: POSTGRES_USER: boocode POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: boocode + POSTGRES_DB: boochat ports: - "127.0.0.1:5500:5432" volumes: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfd1175..2b398e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,34 @@ importers: specifier: ^5.5.0 version: 5.9.3 + apps/coder: + dependencies: + '@fastify/static': + specifier: ^7.0.4 + version: 7.0.4 + '@fastify/websocket': + specifier: ^10.0.1 + version: 10.0.1 + fastify: + specifier: ^4.28.1 + version: 4.29.1 + postgres: + specifier: ^3.4.4 + version: 3.4.9 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^20.14.10 + version: 20.19.41 + tsx: + specifier: ^4.16.2 + version: 4.22.0 + typescript: + specifier: ^5.5.0 + version: 5.9.3 + apps/server: dependencies: '@ai-sdk/openai-compatible':