v2.0.0-beta: write tools, pending-changes queue, inference loop, API routes
Phase 2 of v2.0. BooCoder is now a functional write-capable chatbot.
Write-path guard: resolveWritePath() uses resolve() (no realpath — files may
not exist for creates) + prefix-check + secret-file deny list (.env, *.pem,
id_rsa*, etc.). 23 unit tests cover traversal attacks.
Pending-changes service: queueEdit/Create/Delete → applyOne/All →
rejectOne/All → rewindOne. Edit diffs stored as JSON {old, new}. All writes
queue before touching disk; apply re-validates the path guard.
5 write tools: edit_file, create_file, delete_file, apply_pending, rewind.
Registered alongside 25 read-only tools from BooChat (30 total, alpha-sorted).
Write tools use a module-level inference context for sql+sessionId injection.
Inference loop via workspace dependency: apps/coder imports
createInferenceRunner, createBroker, ALL_TOOLS from @boocode/server (dist/).
apps/server gains declaration: true + exports map with typed subpath entries.
No code duplication — one inference engine shared by both apps.
API routes: POST /api/sessions/:id/messages (user msg → inference), POST stop,
GET/POST pending-changes CRUD (5 endpoints), WebSocket session streaming.
Dockerfile updated to build apps/server first (coder depends on its .d.ts).
Health endpoint reports tool count: {"ok":true,"db":true,"tools":30}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
apps/coder/src/routes/ws.ts
Normal file
51
apps/coder/src/routes/ws.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Sql } from '../db.js';
|
||||
import type { Broker } from '@boocode/server/broker';
|
||||
|
||||
export function registerWebSocket(
|
||||
app: FastifyInstance,
|
||||
sql: Sql,
|
||||
broker: Broker,
|
||||
): void {
|
||||
// Per-session streaming WebSocket. Clients connect here to receive live
|
||||
// inference frames (deltas, tool_calls, tool_results, message_complete).
|
||||
app.get<{ Params: { sessionId: string } }>(
|
||||
'/api/ws/sessions/:sessionId',
|
||||
{ websocket: true },
|
||||
async (socket, req) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
|
||||
// Validate session exists
|
||||
const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`;
|
||||
if (session.length === 0) {
|
||||
socket.send(JSON.stringify({ type: 'error', error: 'session not found' }));
|
||||
socket.close(1008, 'session not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send snapshot of existing messages so client can hydrate
|
||||
const messages = await sql<Record<string, unknown>[]>`
|
||||
SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq,
|
||||
tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata,
|
||||
summary, tail_start_id, compacted_at
|
||||
FROM messages_with_parts
|
||||
WHERE session_id = ${sessionId}
|
||||
ORDER BY created_at ASC, id ASC
|
||||
`;
|
||||
socket.send(JSON.stringify({ type: 'snapshot', messages }));
|
||||
|
||||
// Subscribe to broker for live frames
|
||||
const unsubscribe = broker.subscribe(sessionId, (frame) => {
|
||||
if (socket.readyState !== socket.OPEN) return;
|
||||
try {
|
||||
socket.send(JSON.stringify(frame));
|
||||
} catch (err) {
|
||||
app.log.warn({ err, sessionId }, 'ws send failed');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => unsubscribe());
|
||||
socket.on('error', () => unsubscribe());
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user