v2.0.0-alpha: BooCoder foundation — container, schema, DB rename

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:20:29 +00:00
parent 62d818af23
commit 006226cce5
10 changed files with 320 additions and 21 deletions

View File

@@ -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).

28
apps/coder/Dockerfile Normal file
View File

@@ -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"]

25
apps/coder/package.json Normal file
View File

@@ -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"
}
}

28
apps/coder/src/config.ts Normal file
View File

@@ -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<typeof ConfigSchema>;
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;
}

45
apps/coder/src/db.ts Normal file
View File

@@ -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<typeof postgres>;
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<void> {
const schemaPath = resolve(__dirname, 'schema.sql');
const ddl = await readFile(schemaPath, 'utf8');
await sql.unsafe(ddl);
}
export async function pingDb(sql: Sql): Promise<boolean> {
try {
await sql`SELECT 1`;
return true;
} catch {
return false;
}
}
export async function closeDb(): Promise<void> {
if (sqlInstance) {
await sqlInstance.end({ timeout: 5 });
sqlInstance = null;
}
}

55
apps/coder/src/index.ts Normal file
View File

@@ -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);
});

48
apps/coder/src/schema.sql Normal file
View File

@@ -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');

15
apps/coder/tsconfig.json Normal file
View File

@@ -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"]
}

View File

@@ -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:

28
pnpm-lock.yaml generated
View File

@@ -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':