Add harness app: agent orchestrator with cluster deployment

- Next.js app for orchestrating coding agent benchmarks (Claude Code, Codex, OpenCode)
- Dockerfile installs git, gh CLI, and agent CLIs for headless execution
- K8s deployment with workspace volume, sealed credentials for Claude + OpenCode
- Traefik IngressRoute at harness.coreworlds.io with internal-only middleware + TLS
- CI pipeline path filter for harness builds
- Fix OpenCode runtime flags (subcommand-based headless mode)
This commit is contained in:
Julia McGhee
2026-03-21 15:26:09 +00:00
parent 9e7077cd82
commit 6dde7c8aef
46 changed files with 4675 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from "next/server";
import {
getAllAgentConfigs,
upsertAgentConfig,
deleteAgentConfig,
AGENT_RUNTIMES,
AgentConfig,
} from "@/lib/agents";
export async function GET() {
return NextResponse.json({
configs: getAllAgentConfigs(),
runtimes: Object.values(AGENT_RUNTIMES),
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.runtime || !body.modelId || !body.provider) {
return NextResponse.json(
{ error: "runtime, modelId, and provider are required" },
{ status: 400 }
);
}
if (!AGENT_RUNTIMES[body.runtime as keyof typeof AGENT_RUNTIMES]) {
return NextResponse.json(
{ error: `runtime must be one of: ${Object.keys(AGENT_RUNTIMES).join(", ")}` },
{ status: 400 }
);
}
const config: AgentConfig = {
id: body.id || `agent-${Date.now()}`,
name: body.name || `${body.runtime} · ${body.modelId}`,
runtime: body.runtime,
modelId: body.modelId,
provider: body.provider,
maxTokens: body.maxTokens,
env: body.env,
};
return NextResponse.json(upsertAgentConfig(config), { status: 201 });
}
export async function DELETE(request: NextRequest) {
const id = request.nextUrl.searchParams.get("id");
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
deleteAgentConfig(id);
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
service: "harness",
timestamp: new Date().toISOString(),
});
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import {
getCuratedModels,
getEnabledModels,
upsertCuratedModel,
removeCuratedModel,
toggleModelEnabled,
updateModelCost,
CuratedModel,
} from "@/lib/model-store";
export async function GET(request: NextRequest) {
const enabledOnly = request.nextUrl.searchParams.get("enabled") === "true";
return NextResponse.json(enabledOnly ? getEnabledModels() : getCuratedModels());
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (body.action === "toggle" && body.id) {
const result = toggleModelEnabled(body.id);
if (!result) return NextResponse.json({ error: "not found" }, { status: 404 });
return NextResponse.json(result);
}
if (body.action === "update-cost" && body.id) {
const result = updateModelCost(body.id, body.costPer1kInput, body.costPer1kOutput);
if (!result) return NextResponse.json({ error: "not found" }, { status: 404 });
return NextResponse.json(result);
}
if (!body.id || !body.provider) {
return NextResponse.json({ error: "id and provider are required" }, { status: 400 });
}
const model: CuratedModel = {
id: body.id,
name: body.name || body.id,
provider: body.provider,
enabled: body.enabled ?? true,
contextWindow: body.contextWindow,
costPer1kInput: body.costPer1kInput,
costPer1kOutput: body.costPer1kOutput,
};
return NextResponse.json(upsertCuratedModel(model), { status: 201 });
}
export async function DELETE(request: NextRequest) {
const id = request.nextUrl.searchParams.get("id");
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
removeCuratedModel(id);
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
import { fetchAllModels } from "@/lib/model-providers";
export async function GET() {
const models = await fetchAllModels();
return NextResponse.json(models);
}

View File

@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { getUsageSummary, getUsageLog } from "@/lib/model-store";
export async function GET() {
return NextResponse.json({
summary: getUsageSummary(),
log: getUsageLog(),
});
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from "next/server";
import {
startOrchestrator,
stopOrchestrator,
isRunning,
currentRunningTaskId,
} from "@/lib/orchestrator";
export async function GET() {
return NextResponse.json({
running: isRunning(),
currentTaskId: currentRunningTaskId(),
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
const action = body.action as string;
if (action === "start") {
startOrchestrator();
return NextResponse.json({ ok: true, running: true });
}
if (action === "stop") {
stopOrchestrator();
return NextResponse.json({ ok: true, running: false });
}
return NextResponse.json({ error: "Unknown action. Use 'start' or 'stop'" }, { status: 400 });
}

View File

@@ -0,0 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { searchRepos } from "@/lib/repo-search";
export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get("q") || "";
if (query.length < 2) {
return NextResponse.json([]);
}
const results = await searchRepos(query);
return NextResponse.json(results);
}

View File

@@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from "next/server";
import {
getAllCredentials,
getCredentialsByKind,
upsertCredential,
deleteCredential,
Credential,
GIT_PROVIDERS,
AI_PROVIDERS,
} from "@/lib/credentials";
const VALID_PROVIDERS = [...GIT_PROVIDERS, ...AI_PROVIDERS];
export async function GET(request: NextRequest) {
const kind = request.nextUrl.searchParams.get("kind");
if (kind === "git" || kind === "ai") {
return NextResponse.json(getCredentialsByKind(kind));
}
return NextResponse.json(getAllCredentials());
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.provider || !body.token || !body.label) {
return NextResponse.json(
{ error: "provider, label, and token are required" },
{ status: 400 }
);
}
if (!VALID_PROVIDERS.includes(body.provider)) {
return NextResponse.json(
{ error: `provider must be one of: ${VALID_PROVIDERS.join(", ")}` },
{ status: 400 }
);
}
const cred: Credential = {
id: body.id || `cred-${Date.now()}`,
provider: body.provider,
label: body.label,
token: body.token,
baseUrl: body.baseUrl,
};
const saved = upsertCredential(cred);
return NextResponse.json(saved, { status: 201 });
}
export async function DELETE(request: NextRequest) {
const id = request.nextUrl.searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "id is required" }, { status: 400 });
}
const deleted = deleteCredential(id);
if (!deleted) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { getTask, updateTask } from "@/lib/store";
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const task = getTask(id);
if (!task) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
}
return NextResponse.json(task);
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const body = await request.json();
const updated = updateTask(id, body);
if (!updated) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
}
return NextResponse.json(updated);
}

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { getTask } from "@/lib/store";
import { startOrchestrator } from "@/lib/orchestrator";
export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const task = getTask(id);
if (!task) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
}
if (task.status !== "pending") {
return NextResponse.json(
{ error: `Task is ${task.status}, not pending` },
{ status: 400 },
);
}
// Ensure orchestrator is running — it will pick up this task
startOrchestrator();
return NextResponse.json({ ok: true, message: "Orchestrator started, task will be picked up" });
}

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { getTask } from "@/lib/store";
import { cancelTask } from "@/lib/orchestrator";
export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const task = getTask(id);
if (!task) {
return NextResponse.json({ error: "Task not found" }, { status: 404 });
}
if (task.status !== "running") {
return NextResponse.json(
{ error: `Task is ${task.status}, not running` },
{ status: 400 },
);
}
const cancelled = cancelTask(id);
if (!cancelled) {
return NextResponse.json(
{ error: "Task is not the currently executing task" },
{ status: 400 },
);
}
return NextResponse.json({ ok: true, message: "Task cancellation requested" });
}

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from "next/server";
import { getAllTasks, createTask } from "@/lib/store";
import { getAgentConfig } from "@/lib/agents";
import { Task, TaskSpec } from "@/lib/types";
export async function GET() {
return NextResponse.json(getAllTasks());
}
export async function POST(request: NextRequest) {
const spec: TaskSpec = await request.json();
if (!spec.slug || !spec.goal) {
return NextResponse.json({ error: "slug and goal are required" }, { status: 400 });
}
if (!spec.agentId) {
return NextResponse.json({ error: "agentId is required" }, { status: 400 });
}
const agentConfig = getAgentConfig(spec.agentId);
if (!agentConfig) {
return NextResponse.json(
{ error: `Agent config not found: ${spec.agentId}` },
{ status: 400 },
);
}
const task: Task = {
id: `task-${Date.now()}`,
slug: spec.slug,
goal: spec.goal,
project: spec.project || "—",
status: "pending",
iteration: 0,
maxIterations: spec.maxIterations || 6,
startedAt: null,
evals: {},
iterations: [],
spec,
};
const created = createTask(task);
return NextResponse.json(created, { status: 201 });
}

View File

@@ -0,0 +1,18 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Harness — Agent Orchestrator",
description: "Autonomous coding agent loop orchestrator and dashboard",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body style={{ margin: 0, padding: 0 }}>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,5 @@
import HarnessDashboard from "@/components/harness-dashboard";
export default function Page() {
return <HarnessDashboard />;
}