Auto-discover credentials, models, and agents on harness startup
Read mounted secret files (Claude OAuth, OpenCode auth.json) and env vars on boot, register them as credentials, fetch available models from provider APIs, and create default agent configs for each viable runtime+provider+model combination.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
instrumentationHook: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
6
apps/harness/src/instrumentation.ts
Normal file
6
apps/harness/src/instrumentation.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
const { boot } = await import("./lib/boot");
|
||||
await boot();
|
||||
}
|
||||
}
|
||||
196
apps/harness/src/lib/boot.ts
Normal file
196
apps/harness/src/lib/boot.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
// Auto-discovery: load credentials from mounted secrets + env vars,
|
||||
// discover models from providers, and create default agent configs.
|
||||
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { upsertCredential, getRawCredentialsByProvider, type Provider } from "./credentials";
|
||||
import { upsertCuratedModel, getCuratedModels } from "./model-store";
|
||||
import { upsertAgentConfig, getAllAgentConfigs, type AgentRuntime } from "./agents";
|
||||
import { fetchAllModels } from "./model-providers";
|
||||
|
||||
let booted = false;
|
||||
|
||||
// Well-known models with pricing (used as fallback when API discovery returns
|
||||
// models without pricing info, and to enable cost tracking from the start).
|
||||
const KNOWN_MODELS: Record<string, { name: string; provider: string; contextWindow: number; costPer1kInput: number; costPer1kOutput: number }> = {
|
||||
"claude-opus-4-20250514": { name: "Claude Opus 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.015, costPer1kOutput: 0.075 },
|
||||
"claude-sonnet-4-20250514": { name: "Claude Sonnet 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.003, costPer1kOutput: 0.015 },
|
||||
"claude-haiku-4-20250514": { name: "Claude Haiku 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.0008, costPer1kOutput: 0.004 },
|
||||
"gpt-4o": { name: "GPT-4o", provider: "openai", contextWindow: 128000, costPer1kInput: 0.0025, costPer1kOutput: 0.01 },
|
||||
"gpt-4o-mini": { name: "GPT-4o Mini", provider: "openai", contextWindow: 128000, costPer1kInput: 0.00015, costPer1kOutput: 0.0006 },
|
||||
"o3": { name: "o3", provider: "openai", contextWindow: 200000, costPer1kInput: 0.01, costPer1kOutput: 0.04 },
|
||||
"o4-mini": { name: "o4 Mini", provider: "openai", contextWindow: 200000, costPer1kInput: 0.0011, costPer1kOutput: 0.0044 },
|
||||
"gemini-2.5-pro": { name: "Gemini 2.5 Pro", provider: "google", contextWindow: 1048576, costPer1kInput: 0.00125, costPer1kOutput: 0.01 },
|
||||
"gemini-2.5-flash": { name: "Gemini 2.5 Flash", provider: "google", contextWindow: 1048576, costPer1kInput: 0.00015, costPer1kOutput: 0.0006 },
|
||||
};
|
||||
|
||||
// Default agents to create per provider when credentials are available.
|
||||
// Maps provider → [{ runtime, models[] }].
|
||||
const DEFAULT_AGENTS: Record<string, { runtime: AgentRuntime; models: string[] }[]> = {
|
||||
anthropic: [
|
||||
{ runtime: "claude-code", models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"] },
|
||||
{ runtime: "opencode", models: ["claude-sonnet-4-20250514"] },
|
||||
],
|
||||
openai: [
|
||||
{ runtime: "codex", models: ["o3", "o4-mini"] },
|
||||
],
|
||||
google: [
|
||||
{ runtime: "opencode", models: ["gemini-2.5-pro", "gemini-2.5-flash"] },
|
||||
],
|
||||
openrouter: [
|
||||
{ runtime: "opencode", models: ["claude-sonnet-4-20250514"] },
|
||||
],
|
||||
};
|
||||
|
||||
const RUNTIME_LABELS: Record<AgentRuntime, string> = {
|
||||
"claude-code": "Claude Code",
|
||||
"codex": "Codex",
|
||||
"opencode": "OpenCode",
|
||||
};
|
||||
|
||||
// ─── CREDENTIAL LOADING ─────────────────────────────────────
|
||||
|
||||
function loadCredentialsFromEnv() {
|
||||
const envMap: [string, Provider, string][] = [
|
||||
["ANTHROPIC_API_KEY", "anthropic", "Anthropic (env)"],
|
||||
["OPENAI_API_KEY", "openai", "OpenAI (env)"],
|
||||
["GOOGLE_API_KEY", "google", "Google (env)"],
|
||||
["OPENROUTER_API_KEY", "openrouter", "OpenRouter (env)"],
|
||||
["OPENCODE_ZEN_API_KEY", "opencode-zen", "OpenCode Zen (env)"],
|
||||
["GITHUB_TOKEN", "github", "GitHub (env)"],
|
||||
["GH_TOKEN", "github", "GitHub (env)"],
|
||||
["GITLAB_TOKEN", "gitlab", "GitLab (env)"],
|
||||
];
|
||||
|
||||
for (const [envVar, provider, label] of envMap) {
|
||||
const token = process.env[envVar];
|
||||
if (!token) continue;
|
||||
// Don't overwrite if already loaded from file
|
||||
if (getRawCredentialsByProvider(provider).length > 0) continue;
|
||||
upsertCredential({ id: `env-${provider}`, provider, label, token });
|
||||
}
|
||||
}
|
||||
|
||||
function loadClaudeCredentials() {
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (!configDir) return;
|
||||
|
||||
const credPath = `${configDir}/.credentials.json`;
|
||||
if (!existsSync(credPath)) return;
|
||||
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(credPath, "utf-8"));
|
||||
|
||||
// Claude Code OAuth credentials → extract access token for Anthropic API
|
||||
if (raw.claudeAiOauth?.accessToken) {
|
||||
upsertCredential({
|
||||
id: "file-anthropic",
|
||||
provider: "anthropic",
|
||||
label: `Claude ${raw.claudeAiOauth.subscriptionType || "API"} (mounted)`,
|
||||
token: raw.claudeAiOauth.accessToken,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
function loadOpenCodeCredentials() {
|
||||
const configDir = process.env.OPENCODE_CONFIG_DIR;
|
||||
if (!configDir) return;
|
||||
|
||||
const authPath = `${configDir}/auth.json`;
|
||||
if (!existsSync(authPath)) return;
|
||||
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(authPath, "utf-8"));
|
||||
|
||||
// OpenCode auth.json: { "provider": { "type": "api", "key": "..." } }
|
||||
const providerMap: Record<string, Provider> = {
|
||||
anthropic: "anthropic",
|
||||
openai: "openai",
|
||||
google: "google",
|
||||
openrouter: "openrouter",
|
||||
opencode: "opencode-zen",
|
||||
};
|
||||
|
||||
for (const [key, entry] of Object.entries(raw)) {
|
||||
const provider = providerMap[key];
|
||||
if (!provider || typeof entry !== "object" || !entry) continue;
|
||||
const token = (entry as Record<string, unknown>).key;
|
||||
if (typeof token !== "string" || !token) continue;
|
||||
// Don't overwrite credentials already loaded from Claude config
|
||||
if (getRawCredentialsByProvider(provider).length > 0) continue;
|
||||
upsertCredential({
|
||||
id: `file-${provider}`,
|
||||
provider,
|
||||
label: `${key} (mounted)`,
|
||||
token,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MODEL + AGENT AUTO-DISCOVERY ───────────────────────────
|
||||
|
||||
async function discoverModelsAndAgents() {
|
||||
// Fetch live models from all providers with credentials
|
||||
const liveModels = await fetchAllModels();
|
||||
|
||||
// Upsert discovered models into curated store, enriched with known pricing
|
||||
for (const m of liveModels) {
|
||||
const known = KNOWN_MODELS[m.id];
|
||||
upsertCuratedModel({
|
||||
id: m.id,
|
||||
name: known?.name || m.name,
|
||||
provider: m.provider,
|
||||
enabled: true,
|
||||
contextWindow: m.contextWindow || known?.contextWindow,
|
||||
costPer1kInput: known?.costPer1kInput,
|
||||
costPer1kOutput: known?.costPer1kOutput,
|
||||
});
|
||||
}
|
||||
|
||||
// Also add well-known models that have credentials but weren't returned by API
|
||||
// (e.g. newer models not yet in /v1/models listing)
|
||||
for (const [id, info] of Object.entries(KNOWN_MODELS)) {
|
||||
if (getCuratedModels().some(m => m.id === id)) continue;
|
||||
if (getRawCredentialsByProvider(info.provider as Provider).length === 0) continue;
|
||||
upsertCuratedModel({ id, ...info, enabled: true });
|
||||
}
|
||||
|
||||
// Create default agent configs if none exist yet
|
||||
if (getAllAgentConfigs().length > 0) return;
|
||||
|
||||
for (const [provider, runtimes] of Object.entries(DEFAULT_AGENTS)) {
|
||||
if (getRawCredentialsByProvider(provider as Provider).length === 0) continue;
|
||||
for (const { runtime, models } of runtimes) {
|
||||
for (const modelId of models) {
|
||||
const known = KNOWN_MODELS[modelId];
|
||||
const name = `${RUNTIME_LABELS[runtime]} · ${known?.name || modelId}`;
|
||||
const id = `auto-${runtime}-${modelId}`.replace(/[^a-z0-9-]/g, "-");
|
||||
upsertAgentConfig({ id, name, runtime, modelId, provider });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── BOOT ───────────────────────────────────────────────────
|
||||
|
||||
export async function boot() {
|
||||
if (booted) return;
|
||||
booted = true;
|
||||
|
||||
// 1. Load credentials from mounted secrets (files take priority)
|
||||
loadClaudeCredentials();
|
||||
loadOpenCodeCredentials();
|
||||
// 2. Fill gaps from env vars
|
||||
loadCredentialsFromEnv();
|
||||
// 3. Discover models and create agents (async, best-effort)
|
||||
try {
|
||||
await discoverModelsAndAgents();
|
||||
} catch {
|
||||
// non-fatal — models/agents will be empty until manually configured
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user