From eeb87018d797a70cd6bd50d1d417230bf4dcce8d Mon Sep 17 00:00:00 2001 From: Julia McGhee Date: Sat, 21 Mar 2026 21:15:15 +0000 Subject: [PATCH] Add event-driven tasks via Gitea webhooks Webhook endpoint at /api/webhooks/gitea receives Gitea status events, matches them against configurable event triggers with conditions (event type, repo glob, state, context), renders task templates with {{variable}} substitution, and creates harness tasks automatically. Includes circuit breaker: after N consecutive task failures from the same trigger (default 3), the trigger auto-disables. Re-enable manually via PATCH /api/event-triggers/:id. New tables: harness_event_triggers (rules + circuit breaker state), harness_event_log (audit trail + dedup via X-Gitea-Delivery). --- apps/harness/package.json | 8 +- apps/harness/scripts/build-mcp.mjs | 19 + .../src/app/api/event-triggers/[id]/route.ts | 64 ++ .../src/app/api/event-triggers/route.ts | 53 ++ .../harness/src/app/api/orchestrator/route.ts | 8 +- .../src/app/api/tasks/[id]/start/route.ts | 2 +- .../src/app/api/tasks/[id]/stop/route.ts | 9 +- .../src/app/api/webhooks/gitea/route.ts | 158 ++++ apps/harness/src/lib/event-matching.ts | 34 + apps/harness/src/lib/event-store.ts | 123 +++ apps/harness/src/lib/orchestrator.ts | 97 ++- apps/harness/src/lib/store.ts | 71 +- apps/harness/src/lib/template.ts | 95 +++ apps/harness/src/mcp-server.ts | 383 +++++++++ packages/db/drizzle/0001_stale_pride.sql | 43 + packages/db/drizzle/meta/0001_snapshot.json | 775 ++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema.ts | 68 ++ pnpm-lock.yaml | 386 +++++++++ 19 files changed, 2368 insertions(+), 35 deletions(-) create mode 100644 apps/harness/scripts/build-mcp.mjs create mode 100644 apps/harness/src/app/api/event-triggers/[id]/route.ts create mode 100644 apps/harness/src/app/api/event-triggers/route.ts create mode 100644 apps/harness/src/app/api/webhooks/gitea/route.ts create mode 100644 apps/harness/src/lib/event-matching.ts create mode 100644 apps/harness/src/lib/event-store.ts create mode 100644 apps/harness/src/lib/template.ts create mode 100644 apps/harness/src/mcp-server.ts create mode 100644 packages/db/drizzle/0001_stale_pride.sql create mode 100644 packages/db/drizzle/meta/0001_snapshot.json diff --git a/apps/harness/package.json b/apps/harness/package.json index 7c557f8..a184dd9 100644 --- a/apps/harness/package.json +++ b/apps/harness/package.json @@ -4,13 +4,14 @@ "private": true, "scripts": { "dev": "next dev --port 3100", - "build": "next build", + "build": "node scripts/build-mcp.mjs && next build", "start": "next start", "lint": "next lint", "test": "echo \"no tests yet\"" }, "dependencies": { "@homelab/db": "workspace:^", + "@modelcontextprotocol/sdk": "^1.27.1", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "drizzle-orm": "^0.36.0", @@ -19,14 +20,17 @@ "postgres": "^3.4.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "tsx": "^4.19.0", "ws": "^8.20.0", - "yaml": "^2.7.0" + "yaml": "^2.7.0", + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/ws": "^8.18.1", + "esbuild": "^0.27.4", "typescript": "^5.7.0" } } diff --git a/apps/harness/scripts/build-mcp.mjs b/apps/harness/scripts/build-mcp.mjs new file mode 100644 index 0000000..3ae59ff --- /dev/null +++ b/apps/harness/scripts/build-mcp.mjs @@ -0,0 +1,19 @@ +import { build } from "esbuild"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +await build({ + entryPoints: [resolve(__dirname, "../src/mcp-server.ts")], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + outfile: resolve(__dirname, "../dist/mcp-server.mjs"), + external: [], + banner: { js: "#!/usr/bin/env node" }, + // Inline all deps — the output is a self-contained script +}); + +console.log("Built dist/mcp-server.mjs"); diff --git a/apps/harness/src/app/api/event-triggers/[id]/route.ts b/apps/harness/src/app/api/event-triggers/[id]/route.ts new file mode 100644 index 0000000..7b14f3f --- /dev/null +++ b/apps/harness/src/app/api/event-triggers/[id]/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getEventTrigger, + updateEventTrigger, + deleteEventTrigger, + getEventLogByTrigger, +} from "@/lib/event-store"; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const trigger = await getEventTrigger(id); + if (!trigger) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + const recentLogs = await getEventLogByTrigger(id, 20); + return NextResponse.json({ ...trigger, recentLogs }); +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const body = await request.json(); + + // Allow re-enabling a circuit-broken trigger + const updates: Record = {}; + if (body.name !== undefined) updates.name = body.name; + if (body.enabled !== undefined) updates.enabled = body.enabled; + if (body.eventType !== undefined) updates.eventType = body.eventType; + if (body.repoFilter !== undefined) updates.repoFilter = body.repoFilter; + if (body.stateFilter !== undefined) updates.stateFilter = body.stateFilter; + if (body.contextFilter !== undefined) updates.contextFilter = body.contextFilter; + if (body.taskTemplate !== undefined) updates.taskTemplate = body.taskTemplate; + if (body.maxConsecutiveFailures !== undefined) updates.maxConsecutiveFailures = body.maxConsecutiveFailures; + if (body.webhookSecret !== undefined) updates.webhookSecret = body.webhookSecret; + + // Re-enable: reset circuit breaker + if (body.enabled === true) { + updates.consecutiveFailures = 0; + updates.disabledReason = null; + } + + const updated = await updateEventTrigger(id, updates); + if (!updated) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + return NextResponse.json(updated); +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const deleted = await deleteEventTrigger(id); + if (!deleted) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + return NextResponse.json({ ok: true }); +} diff --git a/apps/harness/src/app/api/event-triggers/route.ts b/apps/harness/src/app/api/event-triggers/route.ts new file mode 100644 index 0000000..8a949d4 --- /dev/null +++ b/apps/harness/src/app/api/event-triggers/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getAllEventTriggers, + createEventTrigger, +} from "@/lib/event-store"; + +export async function GET() { + return NextResponse.json(await getAllEventTriggers()); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + + if (!body.name || !body.eventType || !body.taskTemplate) { + return NextResponse.json( + { error: "name, eventType, and taskTemplate are required" }, + { status: 400 }, + ); + } + + if (!body.taskTemplate.agentId || !body.taskTemplate.goal) { + return NextResponse.json( + { error: "taskTemplate must include agentId and goal" }, + { status: 400 }, + ); + } + + const trigger = await createEventTrigger({ + id: body.id || `evt-${Date.now()}`, + name: body.name, + enabled: body.enabled ?? true, + eventType: body.eventType, + repoFilter: body.repoFilter || null, + stateFilter: body.stateFilter || null, + contextFilter: body.contextFilter || null, + taskTemplate: { + slug: body.taskTemplate.slug || "event-{{sha_short}}", + goal: body.taskTemplate.goal, + project: body.taskTemplate.project || "{{repo}}", + gitProvider: body.taskTemplate.gitProvider, + gitBaseUrl: body.taskTemplate.gitBaseUrl, + agentId: body.taskTemplate.agentId, + maxIterations: body.taskTemplate.maxIterations || 6, + criteria: body.taskTemplate.criteria || [], + constraints: body.taskTemplate.constraints || [], + knowledgeRefs: body.taskTemplate.knowledgeRefs || [], + }, + maxConsecutiveFailures: body.maxConsecutiveFailures ?? 3, + webhookSecret: body.webhookSecret || null, + }); + + return NextResponse.json(trigger, { status: 201 }); +} diff --git a/apps/harness/src/app/api/orchestrator/route.ts b/apps/harness/src/app/api/orchestrator/route.ts index a4779b4..0b27770 100644 --- a/apps/harness/src/app/api/orchestrator/route.ts +++ b/apps/harness/src/app/api/orchestrator/route.ts @@ -8,8 +8,8 @@ import { export async function GET() { return NextResponse.json({ - running: isRunning(), - currentTaskId: currentRunningTaskId(), + running: await isRunning(), + currentTaskId: await currentRunningTaskId(), }); } @@ -18,12 +18,12 @@ export async function POST(request: NextRequest) { const action = body.action as string; if (action === "start") { - startOrchestrator(); + await startOrchestrator(); return NextResponse.json({ ok: true, running: true }); } if (action === "stop") { - stopOrchestrator(); + await stopOrchestrator(); return NextResponse.json({ ok: true, running: false }); } diff --git a/apps/harness/src/app/api/tasks/[id]/start/route.ts b/apps/harness/src/app/api/tasks/[id]/start/route.ts index e43bcf5..1bbed98 100644 --- a/apps/harness/src/app/api/tasks/[id]/start/route.ts +++ b/apps/harness/src/app/api/tasks/[id]/start/route.ts @@ -20,7 +20,7 @@ export async function POST( ); } - startOrchestrator(); + await startOrchestrator(); return NextResponse.json({ ok: true, message: "Orchestrator started, task will be picked up" }); } diff --git a/apps/harness/src/app/api/tasks/[id]/stop/route.ts b/apps/harness/src/app/api/tasks/[id]/stop/route.ts index b217084..3c0c881 100644 --- a/apps/harness/src/app/api/tasks/[id]/stop/route.ts +++ b/apps/harness/src/app/api/tasks/[id]/stop/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { getTask } from "@/lib/store"; -import { cancelTask } from "@/lib/orchestrator"; +import { getTask, requestTaskCancel } from "@/lib/store"; export async function POST( _request: NextRequest, @@ -20,11 +19,11 @@ export async function POST( ); } - const cancelled = cancelTask(id); + const cancelled = await requestTaskCancel(id); if (!cancelled) { return NextResponse.json( - { error: "Task is not the currently executing task" }, - { status: 400 }, + { error: "Failed to request cancellation" }, + { status: 500 }, ); } diff --git a/apps/harness/src/app/api/webhooks/gitea/route.ts b/apps/harness/src/app/api/webhooks/gitea/route.ts new file mode 100644 index 0000000..1016d53 --- /dev/null +++ b/apps/harness/src/app/api/webhooks/gitea/route.ts @@ -0,0 +1,158 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createHmac, timingSafeEqual } from "node:crypto"; +import { createTask } from "@/lib/store"; +import { extractVariables, renderTaskTemplate } from "@/lib/template"; +import { findMatchingTriggers } from "@/lib/event-matching"; +import { + createEventLogEntry, + getEventLogByDeliveryId, + getEventTrigger, +} from "@/lib/event-store"; +import { startOrchestrator } from "@/lib/orchestrator"; +import type { Task } from "@/lib/types"; + +function verifySignature( + rawBody: string, + signature: string, + secret: string, +): boolean { + const computed = createHmac("sha256", secret).update(rawBody).digest("hex"); + try { + return timingSafeEqual( + Buffer.from(computed, "hex"), + Buffer.from(signature, "hex"), + ); + } catch { + return false; + } +} + +export async function POST(request: NextRequest) { + const deliveryId = request.headers.get("x-gitea-delivery") || ""; + const eventType = request.headers.get("x-gitea-event") || ""; + const signature = request.headers.get("x-gitea-signature") || ""; + + const rawBody = await request.text(); + let payload: Record; + try { + payload = JSON.parse(rawBody); + } catch { + return NextResponse.json({ error: "invalid JSON" }, { status: 400 }); + } + + // HMAC validation + const globalSecret = process.env.GITEA_WEBHOOK_SECRET; + if (globalSecret && signature) { + if (!verifySignature(rawBody, signature, globalSecret)) { + return NextResponse.json({ error: "invalid signature" }, { status: 401 }); + } + } + + // Dedup + if (deliveryId) { + const existing = await getEventLogByDeliveryId(deliveryId); + if (existing) { + return NextResponse.json({ status: "duplicate", deliveryId }); + } + } + + // Parse event + const event = extractVariables(eventType, payload); + + // Find matching triggers + const triggers = await findMatchingTriggers(event); + + if (triggers.length === 0) { + return NextResponse.json({ + status: "no_match", + eventType, + repo: event.repo, + state: event.state, + }); + } + + const results: { triggerId: string; taskId: string | null; status: string }[] = []; + + for (const trigger of triggers) { + const logDeliveryId = triggers.length > 1 + ? `${deliveryId || Date.now()}-${trigger.id}` + : deliveryId || String(Date.now()); + + try { + // Check per-trigger secret if set + if (trigger.webhookSecret && signature) { + if (!verifySignature(rawBody, signature, trigger.webhookSecret)) { + await createEventLogEntry({ + triggerId: trigger.id, + deliveryId: logDeliveryId, + eventType, + repo: event.repo, + commitSha: event.sha, + branch: event.branch, + status: "skipped", + skipReason: "Per-trigger signature validation failed", + payload, + }); + results.push({ triggerId: trigger.id, taskId: null, status: "skipped" }); + continue; + } + } + + // Render task spec from template + const spec = renderTaskTemplate(trigger.taskTemplate, event); + const taskId = `task-${Date.now()}-${trigger.id.slice(-4)}`; + + const task: Task = { + id: taskId, + slug: spec.slug, + goal: spec.goal, + project: spec.project, + status: "pending", + iteration: 0, + maxIterations: spec.maxIterations, + startedAt: null, + evals: {}, + iterations: [], + spec, + }; + + await createTask(task); + + await createEventLogEntry({ + triggerId: trigger.id, + deliveryId: logDeliveryId, + eventType, + repo: event.repo, + commitSha: event.sha, + branch: event.branch, + status: "task_created", + taskId, + payload, + }); + + results.push({ triggerId: trigger.id, taskId, status: "task_created" }); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + await createEventLogEntry({ + triggerId: trigger.id, + deliveryId: logDeliveryId, + eventType, + repo: event.repo, + commitSha: event.sha, + branch: event.branch, + status: "error", + error: errorMsg, + payload, + }).catch(() => {}); + + results.push({ triggerId: trigger.id, taskId: null, status: "error" }); + } + } + + // Ensure orchestrator is running to pick up new tasks + if (results.some(r => r.status === "task_created")) { + startOrchestrator(); + } + + return NextResponse.json({ status: "processed", results }); +} diff --git a/apps/harness/src/lib/event-matching.ts b/apps/harness/src/lib/event-matching.ts new file mode 100644 index 0000000..4bcb841 --- /dev/null +++ b/apps/harness/src/lib/event-matching.ts @@ -0,0 +1,34 @@ +// Match webhook events against event trigger conditions. + +import { getEnabledEventTriggers, type EventTrigger } from "./event-store"; +import { type ParsedEvent } from "./template"; + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function globMatch(pattern: string, value: string): boolean { + const regex = new RegExp( + "^" + pattern.split("*").map(escapeRegex).join(".*") + "$", + ); + return regex.test(value); +} + +export function matchesTrigger(trigger: EventTrigger, event: ParsedEvent): boolean { + if (!trigger.enabled) return false; + + if (trigger.eventType !== "*" && trigger.eventType !== event.eventType) return false; + + if (trigger.repoFilter && !globMatch(trigger.repoFilter, event.repo)) return false; + + if (trigger.stateFilter && trigger.stateFilter !== event.state) return false; + + if (trigger.contextFilter && !event.context.includes(trigger.contextFilter)) return false; + + return true; +} + +export async function findMatchingTriggers(event: ParsedEvent): Promise { + const triggers = await getEnabledEventTriggers(); + return triggers.filter(t => matchesTrigger(t, event)); +} diff --git a/apps/harness/src/lib/event-store.ts b/apps/harness/src/lib/event-store.ts new file mode 100644 index 0000000..e26d881 --- /dev/null +++ b/apps/harness/src/lib/event-store.ts @@ -0,0 +1,123 @@ +import { eq, desc } from "drizzle-orm"; +import { db } from "./db"; +import { eventTriggers, eventLog } from "@homelab/db"; + +// ─── TYPES ────────────────────────────────────────────────── + +export type EventTrigger = typeof eventTriggers.$inferSelect; +export type EventTriggerInsert = typeof eventTriggers.$inferInsert; +export type EventLogEntry = typeof eventLog.$inferSelect; + +// ─── EVENT TRIGGERS ───────────────────────────────────────── + +export async function getAllEventTriggers(): Promise { + return db.select().from(eventTriggers); +} + +export async function getEnabledEventTriggers(): Promise { + return db.select().from(eventTriggers).where(eq(eventTriggers.enabled, true)); +} + +export async function getEventTrigger(id: string): Promise { + const [row] = await db.select().from(eventTriggers).where(eq(eventTriggers.id, id)); + return row; +} + +export async function createEventTrigger(trigger: EventTriggerInsert): Promise { + const [row] = await db.insert(eventTriggers).values(trigger).returning(); + return row; +} + +export async function updateEventTrigger( + id: string, + updates: Partial, +): Promise { + const [row] = await db + .update(eventTriggers) + .set({ ...updates, updatedAt: new Date() }) + .where(eq(eventTriggers.id, id)) + .returning(); + return row; +} + +export async function deleteEventTrigger(id: string): Promise { + await db.delete(eventLog).where(eq(eventLog.triggerId, id)); + const result = await db.delete(eventTriggers).where(eq(eventTriggers.id, id)); + return (result as unknown as { rowCount: number }).rowCount > 0; +} + +// ─── EVENT LOG ────────────────────────────────────────────── + +export async function createEventLogEntry( + entry: typeof eventLog.$inferInsert, +): Promise { + const [row] = await db.insert(eventLog).values(entry).returning(); + return row; +} + +export async function getEventLogByDeliveryId( + deliveryId: string, +): Promise { + const [row] = await db + .select() + .from(eventLog) + .where(eq(eventLog.deliveryId, deliveryId)); + return row; +} + +export async function getEventLogByTaskId( + taskId: string, +): Promise { + const [row] = await db + .select() + .from(eventLog) + .where(eq(eventLog.taskId, taskId)); + return row; +} + +export async function getEventLogByTrigger( + triggerId: string, + limit = 50, + offset = 0, +): Promise { + return db + .select() + .from(eventLog) + .where(eq(eventLog.triggerId, triggerId)) + .orderBy(desc(eventLog.createdAt)) + .limit(limit) + .offset(offset); +} + +// ─── CIRCUIT BREAKER ──────────────────────────────────────── + +export async function recordTaskOutcome( + triggerId: string, + passed: boolean, +): Promise { + const trigger = await getEventTrigger(triggerId); + if (!trigger) return; + + if (passed) { + await db + .update(eventTriggers) + .set({ consecutiveFailures: 0, updatedAt: new Date() }) + .where(eq(eventTriggers.id, triggerId)); + return; + } + + const newCount = trigger.consecutiveFailures + 1; + const shouldDisable = newCount >= trigger.maxConsecutiveFailures; + + await db + .update(eventTriggers) + .set({ + consecutiveFailures: newCount, + enabled: shouldDisable ? false : trigger.enabled, + disabledReason: shouldDisable + ? `Circuit breaker: ${newCount} consecutive failures` + : trigger.disabledReason, + updatedAt: new Date(), + }) + .where(eq(eventTriggers.id, triggerId)); +} diff --git a/apps/harness/src/lib/orchestrator.ts b/apps/harness/src/lib/orchestrator.ts index 988bb99..eaea8d2 100644 --- a/apps/harness/src/lib/orchestrator.ts +++ b/apps/harness/src/lib/orchestrator.ts @@ -5,9 +5,15 @@ import { updateIteration, getFirstPendingTask, getRunningTasks, + getOrchestratorState, + setOrchestratorRunning, + setOrchestratorCurrentTask, + updateOrchestratorHeartbeat, + isTaskCancelRequested, } from "./store"; import { recordUsage } from "./model-store"; import { getAgentConfig } from "./agents"; +import { getEventLogByTaskId, recordTaskOutcome } from "./event-store"; import { getRawCredentialsByProvider } from "./credentials"; import { ensureBareClone, @@ -25,47 +31,48 @@ import { evaluate } from "./evaluator"; import { Task, Iteration } from "./types"; const POLL_INTERVAL_MS = 2000; +const CANCEL_CHECK_INTERVAL_MS = 3000; +// Local process state — only the AbortController and poll timer live in memory. +// Everything else (running, currentTaskId) is in Postgres. let pollTimer: ReturnType | null = null; -let running = false; -let currentTaskId: string | null = null; let currentAbort: AbortController | null = null; -export function isRunning(): boolean { - return running; +// ─── Public API (all DB-backed) ───────────────────────────── + +export async function isRunning(): Promise { + const state = await getOrchestratorState(); + return state.running; } -export function currentRunningTaskId(): string | null { - return currentTaskId; +export async function currentRunningTaskId(): Promise { + const state = await getOrchestratorState(); + return state.currentTaskId; } -export function startOrchestrator(): void { - if (running) return; - running = true; +export async function startOrchestrator(): Promise { + const state = await getOrchestratorState(); + if (state.running && pollTimer) return; - recoverCrashedTasks(); + await setOrchestratorRunning(true); + await recoverCrashedTasks(); pollTimer = setInterval(() => { - if (currentTaskId) return; poll(); }, POLL_INTERVAL_MS); poll(); } -export function stopOrchestrator(): void { - running = false; +export async function stopOrchestrator(): Promise { + await setOrchestratorRunning(false); if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } -export function cancelTask(taskId: string): boolean { - if (currentTaskId !== taskId) return false; - currentAbort?.abort(); - return true; -} +// ─── Internals ────────────────────────────────────────────── async function recoverCrashedTasks(): Promise { const runningTasks = await getRunningTasks(); @@ -84,15 +91,17 @@ async function recoverCrashedTasks(): Promise { completedAt: Date.now(), }); } + await setOrchestratorCurrentTask(null); } async function poll(): Promise { - if (!running || currentTaskId) return; + const state = await getOrchestratorState(); + if (!state.running || state.currentTaskId) return; const task = await getFirstPendingTask(); if (!task) return; - currentTaskId = task.id; + await setOrchestratorCurrentTask(task.id); currentAbort = new AbortController(); try { @@ -104,11 +113,43 @@ async function poll(): Promise { completedAt: Date.now(), }); } finally { - currentTaskId = null; + // Circuit breaker: update event trigger if this task was event-driven + try { + const finalTask = await getTask(task.id); + if (finalTask && (finalTask.status === "completed" || finalTask.status === "failed")) { + const logEntry = await getEventLogByTaskId(task.id); + if (logEntry) { + await recordTaskOutcome(logEntry.triggerId, finalTask.status === "completed"); + } + } + } catch (err) { + console.error("[orchestrator] Circuit breaker update failed:", err); + } + + await setOrchestratorCurrentTask(null); currentAbort = null; } } +/** + * Periodically checks the cancel_requested flag in Postgres and triggers + * the local AbortController if set. Returns a cleanup function. + */ +function startCancelWatcher(taskId: string, abort: AbortController): () => void { + const timer = setInterval(async () => { + try { + if (await isTaskCancelRequested(taskId)) { + abort.abort(); + clearInterval(timer); + } + } catch { + // DB read failure — don't crash the watcher, just retry next tick + } + }, CANCEL_CHECK_INTERVAL_MS); + + return () => clearInterval(timer); +} + async function runTask(task: Task): Promise { const agentConfig = await getAgentConfig(task.spec.agentId); if (!agentConfig) { @@ -138,11 +179,15 @@ async function runTask(task: Task): Promise { startedAt: Date.now(), }); + // Start watching for cancel_requested in DB + const stopCancelWatcher = startCancelWatcher(task.id, currentAbort!); + let bareClone: string; try { bareClone = await ensureBareClone(repoUrl, task.slug); } catch (err) { console.error(`[orchestrator] Failed to clone repo for task ${task.id}:`, err); + stopCancelWatcher(); await updateTask(task.id, { status: "failed", completedAt: Date.now(), @@ -159,11 +204,17 @@ async function runTask(task: Task): Promise { status: "failed", completedAt: Date.now(), }); + stopCancelWatcher(); return; } + await updateOrchestratorHeartbeat(); + const result = await runIteration(task, n, bareClone, branchName); - if (!result) return; + if (!result) { + stopCancelWatcher(); + return; + } if (result.allPassed) { converged = true; @@ -171,6 +222,8 @@ async function runTask(task: Task): Promise { } } + stopCancelWatcher(); + if (converged) { try { const finalTask = await getTask(task.id); diff --git a/apps/harness/src/lib/store.ts b/apps/harness/src/lib/store.ts index e47c3d8..81dc0ce 100644 --- a/apps/harness/src/lib/store.ts +++ b/apps/harness/src/lib/store.ts @@ -1,8 +1,77 @@ import { eq, and } from "drizzle-orm"; import { db } from "./db"; -import { tasks as tasksTable, iterations as iterationsTable } from "@homelab/db"; +import { + tasks as tasksTable, + iterations as iterationsTable, + orchestratorState as orchTable, +} from "@homelab/db"; import { Task, Iteration } from "./types"; +// ─── ORCHESTRATOR STATE ───────────────────────────────────── + +export interface OrchestratorState { + running: boolean; + currentTaskId: string | null; + heartbeat: number | null; +} + +async function ensureOrchestratorRow(): Promise { + await db + .insert(orchTable) + .values({ id: "singleton", running: false }) + .onConflictDoNothing(); +} + +export async function getOrchestratorState(): Promise { + await ensureOrchestratorRow(); + const [row] = await db.select().from(orchTable).where(eq(orchTable.id, "singleton")); + return { + running: row.running, + currentTaskId: row.currentTaskId ?? null, + heartbeat: row.heartbeat ?? null, + }; +} + +export async function setOrchestratorRunning(running: boolean): Promise { + await ensureOrchestratorRow(); + await db + .update(orchTable) + .set({ running, updatedAt: new Date() }) + .where(eq(orchTable.id, "singleton")); +} + +export async function setOrchestratorCurrentTask(taskId: string | null): Promise { + await db + .update(orchTable) + .set({ currentTaskId: taskId, heartbeat: Date.now(), updatedAt: new Date() }) + .where(eq(orchTable.id, "singleton")); +} + +export async function updateOrchestratorHeartbeat(): Promise { + await db + .update(orchTable) + .set({ heartbeat: Date.now(), updatedAt: new Date() }) + .where(eq(orchTable.id, "singleton")); +} + +// ─── TASK CANCEL FLAG ─────────────────────────────────────── + +export async function requestTaskCancel(taskId: string): Promise { + const result = await db + .update(tasksTable) + .set({ cancelRequested: true, updatedAt: new Date() }) + .where(and(eq(tasksTable.id, taskId), eq(tasksTable.status, "running"))); + return (result as unknown as { rowCount: number }).rowCount > 0; +} + +export async function isTaskCancelRequested(taskId: string): Promise { + const [row] = await db + .select({ cancelRequested: tasksTable.cancelRequested }) + .from(tasksTable) + .where(eq(tasksTable.id, taskId)); + return row?.cancelRequested ?? false; +} + function rowToTask( row: typeof tasksTable.$inferSelect, iters: (typeof iterationsTable.$inferSelect)[], diff --git a/apps/harness/src/lib/template.ts b/apps/harness/src/lib/template.ts new file mode 100644 index 0000000..4571735 --- /dev/null +++ b/apps/harness/src/lib/template.ts @@ -0,0 +1,95 @@ +// Template variable extraction and substitution for event-driven tasks. + +export interface ParsedEvent { + eventType: string; + repo: string; + repoName: string; + owner: string; + sha: string; + shaShort: string; + branch: string; + state: string; + context: string; + targetUrl: string; + commitMessage: string; + timestamp: string; +} + +export function extractVariables( + eventType: string, + payload: Record, +): ParsedEvent { + const repository = payload.repository as Record | undefined; + const owner = repository?.owner as Record | undefined; + const commit = payload.commit as Record | undefined; + const branches = payload.branches as { name: string }[] | undefined; + const sha = String(payload.sha || ""); + + return { + eventType, + repo: String(repository?.full_name || ""), + repoName: String(repository?.name || ""), + owner: String(owner?.login || ""), + sha, + shaShort: sha.slice(0, 7), + branch: branches?.[0]?.name || "", + state: String(payload.state || ""), + context: String(payload.context || ""), + targetUrl: String(payload.target_url || ""), + commitMessage: String(commit?.message || payload.description || ""), + timestamp: new Date().toISOString(), + }; +} + +export function renderTemplate( + template: string, + vars: ParsedEvent, +): string { + const map: Record = { + repo: vars.repo, + repo_name: vars.repoName, + owner: vars.owner, + sha: vars.sha, + sha_short: vars.shaShort, + branch: vars.branch, + state: vars.state, + context: vars.context, + target_url: vars.targetUrl, + commit_message: vars.commitMessage, + timestamp: vars.timestamp, + }; + + return template.replace(/\{\{(\w+)\}\}/g, (match, key) => map[key] ?? match); +} + +export function renderTaskTemplate( + template: { + slug: string; + goal: string; + project: string; + gitProvider?: string; + gitBaseUrl?: string; + agentId: string; + maxIterations: number; + criteria: { label: string; target: string }[]; + constraints: string[]; + knowledgeRefs: string[]; + }, + vars: ParsedEvent, +) { + return { + slug: renderTemplate(template.slug, vars), + goal: renderTemplate(template.goal, vars), + project: renderTemplate(template.project, vars), + gitProvider: template.gitProvider as "github" | "gitlab" | "gitea" | undefined, + gitBaseUrl: template.gitBaseUrl, + agentId: template.agentId, + maxIterations: template.maxIterations, + criteria: template.criteria.map(c => ({ + label: renderTemplate(c.label, vars), + target: renderTemplate(c.target, vars), + })), + constraints: template.constraints.map(c => renderTemplate(c, vars)), + knowledgeRefs: template.knowledgeRefs.map(k => renderTemplate(k, vars)), + }; +} diff --git a/apps/harness/src/mcp-server.ts b/apps/harness/src/mcp-server.ts new file mode 100644 index 0000000..5c861be --- /dev/null +++ b/apps/harness/src/mcp-server.ts @@ -0,0 +1,383 @@ +#!/usr/bin/env node + +/** + * Harness MCP Server + * + * Stdio MCP server that exposes harness knowledge management and task + * orchestration tools to agents spawned by the harness orchestrator. + * + * Environment variables: + * DATABASE_URL — Postgres connection string (required) + * HARNESS_KNOWLEDGE_DIR — Path to knowledge documents directory + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { eq, and } from "drizzle-orm"; +import postgres from "postgres"; +import { + tasks as tasksTable, + iterations as iterationsTable, + agentConfigs as agentTable, + curatedModels as modelsTable, + orchestratorState as orchTable, +} from "@homelab/db"; +import { readFile, writeFile, readdir, mkdir, stat } from "node:fs/promises"; +import path from "node:path"; + +// ── Database ──────────────────────────────────────────────────── + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + console.error("DATABASE_URL is required"); + process.exit(1); +} + +const client = postgres(connectionString); +const db = drizzle(client); + +// ── Knowledge dir ─────────────────────────────────────────────── + +const KNOWLEDGE_DIR = process.env.HARNESS_KNOWLEDGE_DIR || ""; + +// ── Helpers ───────────────────────────────────────────────────── + +function taskSummary(row: typeof tasksTable.$inferSelect) { + return { + id: row.id, + slug: row.slug, + goal: row.goal, + status: row.status, + project: row.project, + iteration: row.iteration, + maxIterations: row.maxIterations, + startedAt: row.startedAt, + completedAt: row.completedAt, + }; +} + +// ── Server ────────────────────────────────────────────────────── + +const server = new McpServer({ + name: "harness", + version: "1.0.0", +}); + +// ── Knowledge Tools ───────────────────────────────────────────── + +server.tool( + "knowledge_list", + "List all knowledge documents available in the harness knowledge base", + async () => { + if (!KNOWLEDGE_DIR) { + return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }] }; + } + try { + const entries = await readdir(KNOWLEDGE_DIR, { withFileTypes: true }); + const files = entries + .filter((e) => e.isFile()) + .map((e) => e.name); + return { + content: [{ type: "text", text: files.length > 0 ? files.join("\n") : "(empty)" }], + }; + } catch (err) { + return { content: [{ type: "text", text: `Error listing knowledge dir: ${err}` }], isError: true }; + } + }, +); + +server.tool( + "knowledge_read", + "Read a specific knowledge document by filename", + { filename: z.string().describe("Filename of the knowledge document to read") }, + async ({ filename }) => { + if (!KNOWLEDGE_DIR) { + return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true }; + } + const filePath = path.resolve(KNOWLEDGE_DIR, filename); + // Prevent path traversal + if (!filePath.startsWith(path.resolve(KNOWLEDGE_DIR))) { + return { content: [{ type: "text", text: "Invalid path" }], isError: true }; + } + try { + const content = await readFile(filePath, "utf-8"); + return { content: [{ type: "text", text: content }] }; + } catch (err) { + return { content: [{ type: "text", text: `Error reading ${filename}: ${err}` }], isError: true }; + } + }, +); + +server.tool( + "knowledge_write", + "Create or update a knowledge document in the harness knowledge base", + { + filename: z.string().describe("Filename for the knowledge document (e.g. 'findings.md')"), + content: z.string().describe("Content to write to the knowledge document"), + }, + async ({ filename, content }) => { + if (!KNOWLEDGE_DIR) { + return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true }; + } + const filePath = path.resolve(KNOWLEDGE_DIR, filename); + if (!filePath.startsWith(path.resolve(KNOWLEDGE_DIR))) { + return { content: [{ type: "text", text: "Invalid path" }], isError: true }; + } + try { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content, "utf-8"); + return { content: [{ type: "text", text: `Wrote ${filename} (${content.length} bytes)` }] }; + } catch (err) { + return { content: [{ type: "text", text: `Error writing ${filename}: ${err}` }], isError: true }; + } + }, +); + +server.tool( + "knowledge_search", + "Search across all knowledge documents for a text pattern (case-insensitive substring match)", + { query: z.string().describe("Text to search for across all knowledge documents") }, + async ({ query }) => { + if (!KNOWLEDGE_DIR) { + return { content: [{ type: "text", text: "HARNESS_KNOWLEDGE_DIR not configured" }], isError: true }; + } + try { + const entries = await readdir(KNOWLEDGE_DIR, { withFileTypes: true }); + const files = entries.filter((e) => e.isFile()); + const results: string[] = []; + const lowerQuery = query.toLowerCase(); + + for (const file of files) { + const filePath = path.join(KNOWLEDGE_DIR, file.name); + const content = await readFile(filePath, "utf-8"); + const lines = content.split("\n"); + const matches = lines + .map((line, i) => ({ line, lineNum: i + 1 })) + .filter(({ line }) => line.toLowerCase().includes(lowerQuery)); + + if (matches.length > 0) { + results.push( + `## ${file.name}\n` + + matches + .slice(0, 10) + .map(({ line, lineNum }) => ` L${lineNum}: ${line.trim()}`) + .join("\n") + + (matches.length > 10 ? `\n ... and ${matches.length - 10} more matches` : ""), + ); + } + } + + return { + content: [{ + type: "text", + text: results.length > 0 ? results.join("\n\n") : `No matches for "${query}"`, + }], + }; + } catch (err) { + return { content: [{ type: "text", text: `Error searching: ${err}` }], isError: true }; + } + }, +); + +// ── Task Tools ────────────────────────────────────────────────── + +server.tool( + "task_list", + "List all harness tasks with their current status and evaluation results", + async () => { + const rows = await db.select().from(tasksTable); + const tasks = rows.map(taskSummary); + return { + content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }], + }; + }, +); + +server.tool( + "task_get", + "Get full details for a harness task including iteration history and evaluations", + { taskId: z.string().describe("Task ID to look up") }, + async ({ taskId }) => { + const [taskRow] = await db.select().from(tasksTable).where(eq(tasksTable.id, taskId)); + if (!taskRow) { + return { content: [{ type: "text", text: `Task ${taskId} not found` }], isError: true }; + } + const iters = await db + .select() + .from(iterationsTable) + .where(eq(iterationsTable.taskId, taskId)); + + const result = { + ...taskSummary(taskRow), + spec: taskRow.spec, + evals: taskRow.evals, + pr: taskRow.pr, + iterations: iters + .sort((a, b) => a.n - b.n) + .map((i) => ({ + n: i.n, + status: i.status, + diagnosis: i.diagnosis, + evals: i.evals, + diffStats: i.diffStats, + agentOutput: i.agentOutput ? i.agentOutput.slice(-4000) : null, + startedAt: i.startedAt, + completedAt: i.completedAt, + })), + }; + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + }, +); + +server.tool( + "task_create", + "Create a new harness task. The task will be created in 'pending' status and can be started with task_start.", + { + slug: z.string().describe("Unique short identifier for the task (e.g. 'fix-auth-bug')"), + goal: z.string().describe("High-level description of what the task should accomplish"), + project: z.string().describe("Repository in 'owner/repo' format"), + agentId: z.string().describe("ID of the agent configuration to use"), + maxIterations: z.number().optional().describe("Maximum iterations before giving up (default: 6)"), + criteria: z + .array( + z.object({ + label: z.string().describe("Criterion name"), + target: z.string().describe("Evaluation target DSL (e.g. 'exitCode:0', 'filesChanged:>0')"), + }), + ) + .optional() + .describe("Success criteria for evaluation"), + constraints: z.array(z.string()).optional().describe("Implementation constraints"), + knowledgeRefs: z.array(z.string()).optional().describe("Knowledge document filenames to include in prompt"), + gitProvider: z.enum(["github", "gitlab", "gitea"]).optional().describe("Git provider (default: github)"), + gitBaseUrl: z.string().optional().describe("Base URL for the git provider API"), + }, + async (args) => { + const spec = { + slug: args.slug, + goal: args.goal, + project: args.project, + agentId: args.agentId, + maxIterations: args.maxIterations ?? 6, + criteria: args.criteria ?? [], + constraints: args.constraints ?? [], + knowledgeRefs: args.knowledgeRefs ?? [], + gitProvider: args.gitProvider, + gitBaseUrl: args.gitBaseUrl, + }; + + const taskId = `task-${Date.now()}`; + await db.insert(tasksTable).values({ + id: taskId, + slug: spec.slug, + goal: spec.goal, + project: spec.project, + status: "pending", + iteration: 0, + maxIterations: spec.maxIterations, + startedAt: null, + evals: {}, + spec, + }); + + return { + content: [{ type: "text", text: JSON.stringify({ id: taskId, status: "pending", slug: spec.slug }) }], + }; + }, +); + +server.tool( + "task_start", + "Ensure the orchestrator is running so it will pick up pending tasks. Sets orchestrator state to running in the database.", + async () => { + // Ensure the singleton row exists + await db + .insert(orchTable) + .values({ id: "singleton", running: false }) + .onConflictDoNothing(); + // Set running + await db + .update(orchTable) + .set({ running: true, updatedAt: new Date() }) + .where(eq(orchTable.id, "singleton")); + + return { + content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Orchestrator set to running — pending tasks will be picked up" }) }], + }; + }, +); + +server.tool( + "task_stop", + "Request cancellation of a running harness task. Sets cancel_requested flag which the orchestrator polls.", + { taskId: z.string().describe("ID of the running task to cancel") }, + async ({ taskId }) => { + const result = await db + .update(tasksTable) + .set({ cancelRequested: true, updatedAt: new Date() }) + .where(and(eq(tasksTable.id, taskId), eq(tasksTable.status, "running"))); + + const rowCount = (result as unknown as { rowCount: number }).rowCount; + if (rowCount === 0) { + return { content: [{ type: "text", text: `Task ${taskId} is not running or not found` }], isError: true }; + } + return { + content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Cancellation requested" }) }], + }; + }, +); + +// ── Agent & Model Tools ───────────────────────────────────────── + +server.tool( + "agent_list", + "List all configured agent runtimes (agent configs with runtime, model, and provider)", + async () => { + const rows = await db.select().from(agentTable); + const agents = rows.map((r) => ({ + id: r.id, + name: r.name, + runtime: r.runtime, + modelId: r.modelId, + provider: r.provider, + })); + return { + content: [{ type: "text", text: JSON.stringify(agents, null, 2) }], + }; + }, +); + +server.tool( + "model_list", + "List available AI models with pricing information", + async () => { + const rows = await db.select().from(modelsTable).where(eq(modelsTable.enabled, true)); + const models = rows.map((r) => ({ + id: r.id, + name: r.name, + provider: r.provider, + contextWindow: r.contextWindow, + costPer1kInput: r.costPer1kInput, + costPer1kOutput: r.costPer1kOutput, + })); + return { + content: [{ type: "text", text: JSON.stringify(models, null, 2) }], + }; + }, +); + +// ── Start ─────────────────────────────────────────────────────── + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error("Harness MCP server failed:", err); + process.exit(1); +}); diff --git a/packages/db/drizzle/0001_stale_pride.sql b/packages/db/drizzle/0001_stale_pride.sql new file mode 100644 index 0000000..7e2e39e --- /dev/null +++ b/packages/db/drizzle/0001_stale_pride.sql @@ -0,0 +1,43 @@ +CREATE TABLE IF NOT EXISTS "harness_event_log" ( + "id" serial PRIMARY KEY NOT NULL, + "trigger_id" text NOT NULL, + "delivery_id" text NOT NULL, + "event_type" text NOT NULL, + "repo" text NOT NULL, + "commit_sha" text, + "branch" text, + "status" text DEFAULT 'received' NOT NULL, + "task_id" text, + "skip_reason" text, + "error" text, + "payload" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "harness_event_log_delivery_id_unique" UNIQUE("delivery_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "harness_event_triggers" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "event_type" text NOT NULL, + "repo_filter" text, + "state_filter" text, + "context_filter" text, + "task_template" jsonb NOT NULL, + "consecutive_failures" integer DEFAULT 0 NOT NULL, + "max_consecutive_failures" integer DEFAULT 3 NOT NULL, + "disabled_reason" text, + "webhook_secret" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "harness_orchestrator" ( + "id" text PRIMARY KEY DEFAULT 'singleton' NOT NULL, + "running" boolean DEFAULT false NOT NULL, + "current_task_id" text, + "heartbeat" bigint, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "harness_tasks" ADD COLUMN "cancel_requested" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0001_snapshot.json b/packages/db/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4583d06 --- /dev/null +++ b/packages/db/drizzle/meta/0001_snapshot.json @@ -0,0 +1,775 @@ +{ + "id": "e84e33a6-8185-4839-ad18-37149f19eb32", + "prevId": "629b632c-cf1f-46ff-bb2f-e47d2343ddbd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.harness_agent_configs": { + "name": "harness_agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_tokens": { + "name": "max_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_credentials": { + "name": "harness_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_curated_models": { + "name": "harness_curated_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cost_per_1k_input": { + "name": "cost_per_1k_input", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost_per_1k_output": { + "name": "cost_per_1k_output", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_event_log": { + "name": "harness_event_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "harness_event_log_delivery_id_unique": { + "name": "harness_event_log_delivery_id_unique", + "nullsNotDistinct": false, + "columns": [ + "delivery_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_event_triggers": { + "name": "harness_event_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_filter": { + "name": "repo_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_filter": { + "name": "state_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_filter": { + "name": "context_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_template": { + "name": "task_template", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_consecutive_failures": { + "name": "max_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_iterations": { + "name": "harness_iterations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n": { + "name": "n", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "diagnosis": { + "name": "diagnosis", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_output": { + "name": "agent_output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evals": { + "name": "evals", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "diff_stats": { + "name": "diff_stats", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_model_usage": { + "name": "harness_model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_orchestrator": { + "name": "harness_orchestrator", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'singleton'" + }, + "running": { + "name": "running", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "current_task_id": { + "name": "current_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heartbeat": { + "name": "heartbeat", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.harness_tasks": { + "name": "harness_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "iteration": { + "name": "iteration", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_iterations": { + "name": "max_iterations", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 6 + }, + "started_at": { + "name": "started_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'—'" + }, + "evals": { + "name": "evals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "pr": { + "name": "pr", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cancel_requested": { + "name": "cancel_requested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index a8f57c5..fef4a6d 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1774124029586, "tag": "0000_sparkling_gressill", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1774127485727, + "tag": "0001_stale_pride", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 18896fd..5cb4c26 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -75,6 +75,16 @@ export const agentConfigs = pgTable("harness_agent_configs", { updatedAt: timestamp("updated_at").defaultNow().notNull(), }); +// ─── HARNESS: ORCHESTRATOR STATE ────────────────────────── + +export const orchestratorState = pgTable("harness_orchestrator", { + id: text("id").primaryKey().default("singleton"), + running: boolean("running").default(false).notNull(), + currentTaskId: text("current_task_id"), + heartbeat: bigint("heartbeat", { mode: "number" }), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + // ─── HARNESS: TASKS ──────────────────────────────────────── export const tasks = pgTable("harness_tasks", { @@ -89,6 +99,7 @@ export const tasks = pgTable("harness_tasks", { project: text("project").notNull().default("—"), evals: jsonb("evals").$type>().default({}).notNull(), pr: jsonb("pr").$type<{ number: number; title: string; status: string }>(), + cancelRequested: boolean("cancel_requested").default(false).notNull(), spec: jsonb("spec").$type<{ slug: string; goal: string; @@ -117,3 +128,60 @@ export const iterations = pgTable("harness_iterations", { startedAt: bigint("started_at", { mode: "number" }), completedAt: bigint("completed_at", { mode: "number" }), }); + +// ─── HARNESS: EVENT TRIGGERS ─────────────────────────────── + +export const eventTriggers = pgTable("harness_event_triggers", { + id: text("id").primaryKey(), + name: text("name").notNull(), + enabled: boolean("enabled").default(true).notNull(), + + // Matching conditions (all AND'd, null = any) + eventType: text("event_type").notNull(), // "status" | "*" + repoFilter: text("repo_filter"), // glob: "lazorgurl/*" or exact "lazorgurl/homelab" + stateFilter: text("state_filter"), // "failure" | "error" | null + contextFilter: text("context_filter"), // substring match on CI context, null = any + + // Task template with {{variable}} placeholders + taskTemplate: jsonb("task_template").$type<{ + slug: string; + goal: string; + project: string; + gitProvider?: string; + gitBaseUrl?: string; + agentId: string; + maxIterations: number; + criteria: { label: string; target: string }[]; + constraints: string[]; + knowledgeRefs: string[]; + }>().notNull(), + + // Circuit breaker + consecutiveFailures: integer("consecutive_failures").default(0).notNull(), + maxConsecutiveFailures: integer("max_consecutive_failures").default(3).notNull(), + disabledReason: text("disabled_reason"), + + // Webhook auth (per-trigger override, falls back to GITEA_WEBHOOK_SECRET env) + webhookSecret: text("webhook_secret"), + + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// ─── HARNESS: EVENT LOG ──────────────────────────────────── + +export const eventLog = pgTable("harness_event_log", { + id: serial("id").primaryKey(), + triggerId: text("trigger_id").notNull(), + deliveryId: text("delivery_id").notNull().unique(), // X-Gitea-Delivery header (dedup) + eventType: text("event_type").notNull(), + repo: text("repo").notNull(), + commitSha: text("commit_sha"), + branch: text("branch"), + status: text("status").notNull().default("received"), // received | task_created | skipped | error + taskId: text("task_id"), + skipReason: text("skip_reason"), + error: text("error"), + payload: jsonb("payload").$type>().notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3105d73..fd4a6c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@homelab/db': specifier: workspace:^ version: link:../../packages/db + '@modelcontextprotocol/sdk': + specifier: ^1.27.1 + version: 1.27.1(zod@4.3.6) '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -99,12 +102,18 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + tsx: + specifier: ^4.19.0 + version: 4.21.0 ws: specifier: ^8.20.0 version: 8.20.0 yaml: specifier: ^2.7.0 version: 2.8.2 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/node': specifier: ^22.10.0 @@ -118,6 +127,9 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 + esbuild: + specifier: ^0.27.4 + version: 0.27.4 typescript: specifier: ^5.7.0 version: 5.9.3 @@ -698,6 +710,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -870,6 +888,16 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1937,6 +1965,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -1956,9 +1988,20 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2049,6 +2092,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2139,6 +2186,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -2146,6 +2197,10 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -2542,10 +2597,28 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2562,6 +2635,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2590,6 +2666,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2623,6 +2703,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2723,6 +2807,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} + engines: {node: '>=16.9.0'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2735,6 +2823,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2762,6 +2854,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2837,6 +2933,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2887,6 +2986,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2907,6 +3009,12 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3041,9 +3149,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3060,10 +3176,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -3111,6 +3235,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next@15.5.14: resolution: {integrity: sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -3191,6 +3319,9 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3229,6 +3360,9 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3271,6 +3405,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3369,6 +3507,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -3401,6 +3543,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} @@ -3435,6 +3581,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3476,10 +3626,18 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3709,6 +3867,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3791,6 +3953,9 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -3828,6 +3993,14 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4131,6 +4304,10 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 + '@hono/node-server@1.19.11(hono@4.12.8)': + dependencies: + hono: 4.12.8 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -4260,6 +4437,28 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.1 @@ -5453,6 +5652,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -5465,6 +5669,10 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -5472,6 +5680,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -5590,6 +5805,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5672,10 +5901,14 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} cors@2.8.6: @@ -6184,6 +6417,17 @@ snapshots: etag@1.8.1: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + express-rate-limit@8.3.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@4.22.1: dependencies: accepts: 1.3.8 @@ -6220,6 +6464,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -6236,6 +6513,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -6269,6 +6548,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6301,6 +6591,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -6408,6 +6700,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.12.8: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -6427,6 +6721,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6453,6 +6751,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -6530,6 +6830,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6584,6 +6886,8 @@ snapshots: jiti@2.6.1: {} + jose@6.2.2: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -6600,6 +6904,10 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -6705,8 +7013,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -6718,10 +7030,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} minimatch@10.2.4: @@ -6761,6 +7079,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + next@15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.14 @@ -6854,6 +7174,10 @@ snapshots: dependencies: ee-first: 1.1.1 + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6891,6 +7215,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} + pathe@2.0.3: {} pg-int8@1.0.1: {} @@ -6940,6 +7266,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -7032,6 +7360,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -7067,6 +7402,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: dependencies: debug: 4.4.3 @@ -7128,6 +7465,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7181,6 +7528,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -7190,6 +7553,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -7490,6 +7862,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -7625,6 +8003,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + ws@8.20.0: {} xtend@4.0.2: {} @@ -7646,3 +8026,9 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {}