Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
This commit is contained in:
Dulanjana Palamakumbura
2026-08-06 06:13:16 +05:30
committed by GitHub
parent ea4bbdf7c0
commit d91f7d1c3f
21 changed files with 1176 additions and 105 deletions

View File

@@ -311,6 +311,7 @@ export default function AgentBridgePageClient({
targets={targets}
agentStates={data.agentStates}
serverRunning={data.serverState.running}
serverState={data.serverState}
mappingsMap={data.mappings}
onDnsToggle={handleDnsToggle}
onMappingsSave={handleMappingsSave}

View File

@@ -8,7 +8,7 @@ import { ModelMappingTable } from "./ModelMappingTable";
import { SetupWizard } from "./SetupWizard";
import { RiskNoticeModal } from "@/shared/components/RiskNoticeModal";
import type { MitmTargetView } from "@/mitm/types";
import type { AgentStateEntry } from "../AgentBridgePageClient";
import type { AgentStateEntry, AgentBridgeServerState } from "../AgentBridgePageClient";
import type { MappingRow } from "./ModelMappingTable";
const RISK_STORAGE_KEY_PREFIX = "omniroute-agentbridge-risk-dismissed-";
@@ -26,6 +26,7 @@ interface AgentCardProps {
target: MitmTargetView;
agentState: AgentStateEntry | undefined;
serverRunning: boolean;
serverState: AgentBridgeServerState;
mappings: MappingRow[];
onDnsToggle: (agentId: string, enabled: boolean) => Promise<void>;
onMappingsSave: (agentId: string, mappings: MappingRow[]) => Promise<void>;
@@ -38,6 +39,7 @@ export function AgentCard({
target,
agentState,
serverRunning,
serverState,
mappings,
onDnsToggle,
onMappingsSave,
@@ -50,7 +52,9 @@ export function AgentCard({
const dnsEnabled = agentState?.dns_enabled ?? false;
const setupCompleted = agentState?.setup_completed ?? false;
const certTrusted = agentState?.cert_trusted ?? false;
// Fix #8656 Issue A: Use server-level cert trust as fallback
// (one server cert applies to all agents; agentState.cert_trusted is never written to DB)
const certTrusted = agentState?.cert_trusted ?? serverState.certTrusted ?? false;
const isInvestigating = target.viability === "investigating";
const getStatusBadge = () => {
@@ -250,8 +254,11 @@ export function AgentCard({
target={target}
agentState={agentState}
serverRunning={serverRunning}
serverState={serverState}
currentMappings={mappings}
onClose={() => setWizardOpen(false)}
onDnsToggle={onDnsToggle}
onMappingsSave={onMappingsSave}
/>
)}

View File

@@ -4,13 +4,14 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import { AgentCard } from "./AgentCard";
import type { MitmTargetView } from "@/mitm/types";
import type { AgentStateEntry, AgentMappingsMap } from "../AgentBridgePageClient";
import type { AgentStateEntry, AgentMappingsMap, AgentBridgeServerState } from "../AgentBridgePageClient";
import type { MappingRow } from "./ModelMappingTable";
interface AgentListProps {
targets: MitmTargetView[];
agentStates: AgentStateEntry[];
serverRunning: boolean;
serverState: AgentBridgeServerState;
mappingsMap: AgentMappingsMap;
onDnsToggle: (agentId: string, enabled: boolean) => Promise<void>;
onMappingsSave: (agentId: string, mappings: MappingRow[]) => Promise<void>;
@@ -26,6 +27,7 @@ export function AgentList({
targets,
agentStates,
serverRunning,
serverState,
mappingsMap,
onDnsToggle,
onMappingsSave,
@@ -130,6 +132,7 @@ export function AgentList({
target={target}
agentState={stateByAgent[target.id]}
serverRunning={serverRunning}
serverState={serverState}
mappings={mappingsMap[target.id] ?? []}
onDnsToggle={onDnsToggle}
onMappingsSave={onMappingsSave}

View File

@@ -29,6 +29,18 @@ export function ModelMappingTable({ agentId, mappings, onSave }: ModelMappingTab
setSelectorOpen(null);
};
const addMapping = () => {
setRows((prev) => [...prev, { source: "", target: "" }]);
};
const removeMapping = (index: number) => {
setRows((prev) => prev.filter((_, i) => i !== index));
};
const updateSource = (index: number, source: string) => {
setRows((prev) => prev.map((r, i) => (i === index ? { ...r, source } : r)));
};
const handleSave = async () => {
setSaving(true);
try {
@@ -38,66 +50,101 @@ export function ModelMappingTable({ agentId, mappings, onSave }: ModelMappingTab
}
};
if (rows.length === 0) {
return (
<p className="text-xs text-text-muted italic">
{t("noMappings") || "No model mappings configured. Run setup wizard to auto-detect models."}
</p>
);
}
return (
<div className="flex flex-col gap-3">
<div className="rounded-lg border border-border/40 overflow-hidden bg-surface">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/40 bg-surface/60">
<th className="px-3 py-2 text-left text-xs font-medium text-text-muted">
{t("sourceModel") || "Source model (agent native)"}
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-text-muted">
{t("targetModel") || "Target model (OmniRoute)"}
</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="border-b border-border/20 last:border-0">
<td className="px-3 py-2">
<span className="font-mono text-xs text-text-muted">{row.source}</span>
</td>
<td className="px-3 py-2">
<button
type="button"
onClick={() => setSelectorOpen(i)}
className="inline-flex items-center gap-1.5 rounded-lg border border-border/40 bg-card px-2.5 py-1 text-xs hover:bg-surface transition-colors font-mono"
>
{row.target || (
<span className="text-text-muted italic">
{t("selectModel") || "Select…"}
</span>
)}
<span className="material-symbols-outlined text-[12px] text-text-muted">
expand_more
</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{rows.length === 0 ? (
<div className="rounded-lg border border-border/40 bg-surface/30 px-4 py-6 text-center">
<p className="text-xs text-text-muted mb-3">
{t("noMappingsDesc") || "No model mappings configured yet. Add mappings to route agent requests through OmniRoute."}
</p>
<button
type="button"
onClick={addMapping}
className="inline-flex items-center gap-1.5 rounded-lg bg-primary/10 text-primary px-3 py-1.5 text-xs font-medium hover:bg-primary/20 transition-colors"
>
<span className="material-symbols-outlined text-[14px]">add</span>
{t("addMapping") || "Add mapping"}
</button>
</div>
) : (
<>
<div className="rounded-lg border border-border/40 overflow-hidden bg-surface">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border/40 bg-surface/60">
<th className="px-3 py-2 text-left text-xs font-medium text-text-muted">
{t("sourceModel") || "Source model (agent native)"}
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-text-muted">
{t("targetModel") || "Target model (OmniRoute)"}
</th>
<th className="px-3 py-2 w-12"></th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="border-b border-border/20 last:border-0">
<td className="px-3 py-2">
<input
type="text"
value={row.source}
onChange={(e) => updateSource(i, e.target.value)}
placeholder="e.g., gpt-4"
className="w-full rounded border border-border/40 bg-card px-2 py-1 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</td>
<td className="px-3 py-2">
<button
type="button"
onClick={() => setSelectorOpen(i)}
className="inline-flex items-center gap-1.5 rounded-lg border border-border/40 bg-card px-2.5 py-1 text-xs hover:bg-surface transition-colors font-mono w-full justify-between"
>
{row.target || (
<span className="text-text-muted italic">
{t("selectModel") || "Select…"}
</span>
)}
<span className="material-symbols-outlined text-[12px] text-text-muted">
expand_more
</span>
</button>
</td>
<td className="px-3 py-2">
<button
type="button"
onClick={() => removeMapping(i)}
className="text-text-muted hover:text-red-500 transition-colors"
aria-label="Remove mapping"
>
<span className="material-symbols-outlined text-[16px]">delete</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={handleSave}
disabled={saving}
className="rounded-lg bg-primary/10 text-primary px-4 py-1.5 text-sm font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
>
{saving ? t("saving") || "Saving…" : t("saveMappings") || "Save mappings"}
</button>
</div>
<div className="flex justify-between items-center">
<button
type="button"
onClick={addMapping}
className="inline-flex items-center gap-1.5 rounded-lg border border-border/40 bg-card px-3 py-1.5 text-xs font-medium hover:bg-surface transition-colors"
>
<span className="material-symbols-outlined text-[14px]">add</span>
{t("addMapping") || "Add mapping"}
</button>
<button
type="button"
onClick={handleSave}
disabled={saving}
className="rounded-lg bg-primary/10 text-primary px-4 py-1.5 text-sm font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
>
{saving ? t("saving") || "Saving…" : t("saveMappings") || "Save mappings"}
</button>
</div>
</>
)}
{selectorOpen !== null && (
<ModelSelectorModal

View File

@@ -2,19 +2,28 @@
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import type { AgentStateEntry } from "../AgentBridgePageClient";
import type { AgentStateEntry, AgentBridgeServerState } from "../AgentBridgePageClient";
import type { MitmTargetView } from "@/mitm/types";
interface SetupWizardProps {
target: MitmTargetView;
agentState: AgentStateEntry | undefined;
serverRunning: boolean;
serverState: AgentBridgeServerState;
currentMappings: { source: string; target: string }[]; // Current mappings for this agent
onClose: () => void;
onDnsToggle: (agentId: string, enabled: boolean) => Promise<void>;
onMappingsSave: (agentId: string, mappings: { source: string; target: string }[]) => Promise<void>;
}
type Step = "verify" | "dns" | "mappings";
interface DetectedModelsResponse {
agentId: string;
detectedModels: string[];
requestCount: number;
}
/**
* 3-step setup wizard for a single agent.
* Step 1: Verify server + cert
@@ -25,13 +34,19 @@ export function SetupWizard({
target,
agentState,
serverRunning,
serverState,
currentMappings,
onClose,
onDnsToggle,
onMappingsSave,
}: SetupWizardProps) {
const t = useTranslations("agentBridge");
const tc = useTranslations("common");
const [step, setStep] = useState<Step>("verify");
const [enablingDns, setEnablingDns] = useState(false);
const [detectedModels, setDetectedModels] = useState<string[]>([]);
const [loadingModels, setLoadingModels] = useState(false);
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
useEffect(() => {
const handler = (e: KeyboardEvent) => {
@@ -41,7 +56,26 @@ export function SetupWizard({
return () => document.removeEventListener("keydown", handler);
}, [onClose]);
const certTrusted = agentState?.cert_trusted ?? false;
// Fetch detected models when we reach the mappings step
useEffect(() => {
if (step === "mappings") {
setLoadingModels(true);
fetch(`/api/tools/agent-bridge/agents/${target.id}/detected-models`)
.then((res) => res.json())
.then((data: DetectedModelsResponse) => {
setDetectedModels(data.detectedModels || []);
})
.catch(() => {
setDetectedModels([]);
})
.finally(() => {
setLoadingModels(false);
});
}
}, [step, target.id]);
// Fix #8656 Issue A: Use server-level cert trust as fallback
const certTrusted = agentState?.cert_trusted ?? serverState.certTrusted ?? false;
const dnsEnabled = agentState?.dns_enabled ?? false;
const handleEnableDns = async () => {
@@ -54,6 +88,44 @@ export function SetupWizard({
}
};
const toggleModelSelection = (model: string) => {
setSelectedModels((prev) => {
const next = new Set(prev);
if (next.has(model)) {
next.delete(model);
} else {
next.add(model);
}
return next;
});
};
const handleAddSelectedModels = async () => {
if (selectedModels.size === 0) return;
// Merge detected models with existing mappings instead of replacing
// Filter out models that already exist in current mappings
const existingSources = new Set(currentMappings.map((m) => m.source));
const newMappings = Array.from(selectedModels)
.filter((source) => !existingSources.has(source)) // Only add new ones
.map((source) => ({
source,
target: "", // Will be selected later in the main card
}));
// Combine existing + new mappings
const allMappings = [...currentMappings, ...newMappings];
try {
await onMappingsSave(target.id, allMappings);
// Wait a bit for the parent to refresh state before closing
await new Promise((resolve) => setTimeout(resolve, 300));
onClose();
} catch {
// Error handling in parent component
}
};
const steps: { id: Step; label: string }[] = [
{ id: "verify", label: t("wizardStep1Label") },
{ id: "dns", label: t("wizardStep2Label") },
@@ -192,7 +264,49 @@ export function SetupWizard({
<span className="material-symbols-outlined text-[20px]">check_circle</span>
<p className="text-sm font-medium">{t("wizardStep3Success")}</p>
</div>
<p className="text-sm text-text-muted">{t("wizardStep3Desc")}</p>
{loadingModels ? (
<div className="flex items-center gap-2 text-sm text-text-muted">
<span className="material-symbols-outlined text-[16px] animate-spin">progress_activity</span>
Detecting models from intercepted traffic...
</div>
) : detectedModels.length > 0 ? (
<div className="flex flex-col gap-2">
<p className="text-sm text-text-muted">
Found {detectedModels.length} model{detectedModels.length !== 1 ? "s" : ""} in intercepted traffic. Select the ones you want to add:
</p>
<div className="rounded-lg border border-border/40 bg-surface p-3 flex flex-col gap-2 max-h-[200px] overflow-y-auto">
{detectedModels.map((model) => (
<label
key={model}
className="flex items-center gap-2 cursor-pointer hover:bg-surface/50 p-2 rounded transition-colors"
>
<input
type="checkbox"
checked={selectedModels.has(model)}
onChange={() => toggleModelSelection(model)}
className="rounded border-border/50 text-primary focus:ring-2 focus:ring-primary/50"
/>
<span className="font-mono text-xs text-text-main">{model}</span>
</label>
))}
</div>
{selectedModels.size > 0 && (
<p className="text-xs text-text-muted">
{selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""} selected. You'll map them to OmniRoute models in the next screen.
</p>
)}
</div>
) : (
<div className="rounded-lg border border-border/40 bg-surface/30 p-3">
<p className="text-sm text-text-muted">
No models detected yet. Use {target.name} to make a request, then run this wizard again to auto-detect models from traffic.
</p>
<p className="text-xs text-text-muted mt-2">
Or close this wizard and add mappings manually in the agent card.
</p>
</div>
)}
</div>
)}
</div>
@@ -247,13 +361,25 @@ export function SetupWizard({
)}
{step === "mappings" && (
<button
type="button"
onClick={onClose}
className="rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-400 transition-colors"
>
{t("done")}
</button>
<>
{detectedModels.length > 0 && selectedModels.size > 0 ? (
<button
type="button"
onClick={handleAddSelectedModels}
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90 transition-colors"
>
Add {selectedModels.size} model{selectedModels.size !== 1 ? "s" : ""}
</button>
) : (
<button
type="button"
onClick={onClose}
className="rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-400 transition-colors"
>
{t("done")}
</button>
)}
</>
)}
</div>
</div>

View File

@@ -0,0 +1,66 @@
/**
* GET /api/tools/agent-bridge/agents/[id]/detected-models
*
* Returns unique source models detected from intercepted traffic for the given agent.
* Used by Setup Wizard to auto-suggest model mappings.
*
* LOCAL_ONLY: covered by the "/api/tools/agent-bridge/" prefix in routeGuard.ts.
*/
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { createErrorResponse } from "@/lib/api/errorResponse";
import { globalTrafficBuffer } from "@/mitm/inspector/buffer";
import type { AgentId } from "@/mitm/types";
const VALID_IDS = new Set<AgentId>([
"antigravity",
"kiro",
"copilot",
"codex",
"cursor",
"zed",
"claude-code",
"open-code",
"trae",
"windsurf",
"jules",
]);
type Params = { params: Promise<{ id: string }> };
export async function GET(_request: Request, { params }: Params): Promise<Response> {
const { id } = await params;
if (!VALID_IDS.has(id as AgentId)) {
return createErrorResponse({ status: 404, message: `Unknown agent id: ${id}` });
}
try {
const agentId = id as AgentId;
// Get all intercepted requests for this agent
const allRequests = globalTrafficBuffer.list();
const agentRequests = allRequests.filter(
(req) => req.source === "agent-bridge" && req.agent === agentId
);
// Extract unique source models (filter out nulls/undefined)
const uniqueModels = new Set<string>();
for (const req of agentRequests) {
if (req.sourceModel && typeof req.sourceModel === "string") {
uniqueModels.add(req.sourceModel);
}
}
// Sort alphabetically for consistent ordering
const models = Array.from(uniqueModels).sort();
return Response.json({
agentId,
detectedModels: models,
requestCount: agentRequests.length,
});
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return createErrorResponse({ status: 500, message: msg });
}
}

View File

@@ -4,15 +4,16 @@
* LOCAL_ONLY: registered in routeGuard.ts
*/
import { AgentBridgeMappingPutSchema } from "@/shared/schemas/agentBridge";
import { getMappingsForAgent, setMappings } from "@/lib/db/agentBridgeMappings";
import { getMappingsForAgent, setMappings, syncAgentBridgeMappingsToMitmAlias } from "@/lib/db/agentBridgeMappings";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { createErrorResponse } from "@/lib/api/errorResponse";
type Params = { params: { id: string } };
type Params = { params: Promise<{ id: string }> };
export async function GET(_request: Request, { params }: Params): Promise<Response> {
try {
const mappings = getMappingsForAgent(params.id);
const { id } = await params;
const mappings = getMappingsForAgent(id);
return Response.json({ mappings });
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
@@ -38,8 +39,12 @@ export async function PUT(request: Request, { params }: Params): Promise<Respons
}
try {
setMappings(params.id, parsed.data.mappings);
const mappings = getMappingsForAgent(params.id);
const { id } = await params;
setMappings(id, parsed.data.mappings);
// Fix #8656: Sync to mitmAlias in key_value table so the MITM proxy
// (server.cjs) reads user-configured mappings during interception.
syncAgentBridgeMappingsToMitmAlias(id);
const mappings = getMappingsForAgent(id);
return Response.json({ ok: true, mappings });
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));

View File

@@ -8,6 +8,9 @@
* `healthy` verdict). Answers "why is nothing being captured?" in one call.
*
* LOCAL_ONLY: covered by the "/api/tools/agent-bridge/" prefix in routeGuard.ts.
*
* Fix #8656 follow-up: Compute aggregate dnsConfigured when no agentId provided
* (matches state route behavior for consistency).
*/
import net from "node:net";
import path from "node:path";
@@ -18,6 +21,8 @@ import { getMitmStatus } from "@/mitm/manager";
import { checkCertInstalled } from "@/mitm/cert/install";
import { resolveMitmDataDir } from "@/mitm/dataDir";
import { summarizeDiagnostics } from "@/mitm/inspector/diagnostics";
import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState";
import { checkDNSEntryForAgent } from "@/mitm/dns/dnsConfig";
/** Best-effort TCP reachability probe; resolves false on error/timeout. */
function probeTcp(port: number, host = "127.0.0.1", timeoutMs = 1500): Promise<boolean> {
@@ -48,12 +53,23 @@ export async function GET(request: Request): Promise<Response> {
Number(process.env.MITM_LOCAL_PORT) > 0 ? Number(process.env.MITM_LOCAL_PORT) : 443;
const serverReachable = status.running ? await probeTcp(port) : false;
// Compute aggregate dnsConfigured when no agentId provided (matches state route)
// This fixes diagnose showing DNS ❌ for non-Antigravity agents (Kiro, Codex, Cursor)
let dnsConfigured = status.dnsConfigured;
if (!agentId) {
// Check if ANY agent has DNS configured (aggregate view)
const agentStates = await getAllAgentBridgeStates();
dnsConfigured =
agentStates.length > 0 &&
agentStates.some((s) => s.dns_enabled && checkDNSEntryForAgent(s.agent_id));
}
const report = summarizeDiagnostics({
serverRunning: status.running,
serverReachable,
certExists,
certTrusted,
dnsConfigured: status.dnsConfigured,
dnsConfigured,
});
return Response.json({ ...report, port });

View File

@@ -2,21 +2,82 @@
* GET /api/tools/agent-bridge/state
* Returns global MITM server status + per-agent detection/status.
* LOCAL_ONLY: registered in routeGuard.ts
*
* Fix #8656: Now returns the full payload shape the UI expects:
* { serverState, agentStates, bypassPatterns, mappings } while maintaining
* backward-compat legacy keys { server, agents } for integration tests.
*/
import { getMitmStatus, getAllAgentsStatus, getCachedPassword } from "@/mitm/manager";
import { isSudoPasswordRequired } from "@/mitm/dns/dnsConfig";
import { isSudoPasswordRequired, checkDNSEntryForAgent } from "@/mitm/dns/dnsConfig";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { createErrorResponse } from "@/lib/api/errorResponse";
import { getAllAgentBridgeStates } from "@/lib/db/agentBridgeState";
import { getAllBypassPatterns } from "@/lib/db/agentBridgeBypass";
import { getMappingsForAgent } from "@/lib/db/agentBridgeMappings";
import { checkCertInstalled } from "@/mitm/cert/install";
import { resolveMitmDataDir } from "@/mitm/dataDir";
import { ALL_TARGETS } from "@/mitm/targets/index";
import path from "path";
import fs from "fs";
export async function GET(): Promise<Response> {
try {
const [server, agents] = await Promise.all([getMitmStatus(), getAllAgentsStatus()]);
// Fetch all data in parallel for performance
const [serverStatus, agents, agentStates, bypassPatterns] = await Promise.all([
getMitmStatus(),
getAllAgentsStatus(),
getAllAgentBridgeStates(),
getAllBypassPatterns(),
]);
// Load mappings for all registered agents
const mappingsEntries = await Promise.all(
ALL_TARGETS.map(async (t) => {
const mappings = getMappingsForAgent(t.id);
return [
t.id,
mappings.map((m) => ({ source: m.source_model, target: m.target_model })),
] as const;
})
);
const mappings = Object.fromEntries(mappingsEntries);
// Compute REAL certTrusted (OS trust store check, not just file exists)
const certDir = path.join(resolveMitmDataDir(), "mitm");
const certPath = path.join(certDir, "server.crt");
const certExists = fs.existsSync(certPath);
const certTrusted = certExists ? await checkCertInstalled(certPath) : false;
// Compute aggregate dnsConfigured: true if ANY agent has hosts spoofed
// This fixes the Maintenance card "dns-configured" showing ❌ for non-Antigravity agents
const dnsConfigured =
agentStates.length > 0 &&
agentStates.some((s) => s.dns_enabled && checkDNSEntryForAgent(s.agent_id));
const isWin = process.platform === "win32";
const hasCachedPassword = !!getCachedPassword();
const needsSudoPassword = !isWin && !hasCachedPassword && isSudoPasswordRequired();
// Build enriched server state
const enrichedServer = {
...serverStatus,
certExists,
certTrusted,
dnsConfigured,
hasCachedPassword,
needsSudoPassword,
isWin,
};
return Response.json({
server: { ...server, hasCachedPassword, needsSudoPassword, isWin },
// Legacy keys for backward compat (integration tests + settings/mitm depend on these)
server: enrichedServer,
agents,
// New keys the UI actually reads (fix #8656)
serverState: enrichedServer,
agentStates,
bypassPatterns: bypassPatterns.map((b) => b.pattern),
mappings,
});
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));

View File

@@ -10778,6 +10778,8 @@
"sourceModel": "Source model (agent native)",
"targetModel": "Target model (OmniRoute)",
"noMappings": "No model mappings configured. Run setup wizard to auto-detect models.",
"noMappingsDesc": "No model mappings configured yet. Add mappings to route agent requests through OmniRoute.",
"addMapping": "Add mapping",
"selectModel": "Select…",
"saveMappings": "Save mappings",
"setupWizard": "Setup wizard",

View File

@@ -5,6 +5,10 @@
import { getDbInstance } from "./core";
import type { AgentBridgeMappingRow } from "./_rowTypes";
import { setMitmAliasAll } from "./models/mitmAlias";
/** Agents that have a registered alias key in standaloneRouting.cjs::AGENT_ROUTE_CONFIG. */
const MITM_ALIAS_AGENTS = new Set(["antigravity", "claude-code", "kiro"]);
export function getMappingsForAgent(agentId: string): AgentBridgeMappingRow[] {
const db = getDbInstance();
@@ -45,3 +49,27 @@ export function deleteMapping(agentId: string, source: string): void {
"DELETE FROM agent_bridge_mappings WHERE agent_id = ? AND source_model = ?"
).run(agentId, source);
}
/**
* Sync agent_bridge_mappings for the given agent to the key_value table as
* mitmAlias entries so the MITM proxy (server.cjs) can read them during
* interception.
*
* Only syncs agents that have a dedicated alias key in
* standaloneRouting.cjs::AGENT_ROUTE_CONFIG (antigravity, claude-code, kiro).
* Other agents fall through to the antigravity config and don't need their own
* entry.
*
* Fix #8656: model mappings saved via the UI were invisible to the MITM proxy
* because the proxy reads from key_value (namespace='mitmAlias'), not from
* agent_bridge_mappings.
*/
export function syncAgentBridgeMappingsToMitmAlias(agentId: string): void {
if (!MITM_ALIAS_AGENTS.has(agentId)) return;
const rows = getMappingsForAgent(agentId);
const mappings: Record<string, string> = {};
for (const row of rows) {
mappings[row.source_model] = row.target_model;
}
setMitmAliasAll(agentId, mappings);
}

View File

@@ -33,8 +33,11 @@ async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
}
/** Run the capture-pipeline self-test (server/cert/dns reachability). */
export function runDiagnose(): Promise<DiagnoseResult> {
return requestJson<DiagnoseResult>("/api/tools/agent-bridge/diagnose");
export function runDiagnose(agentId?: string): Promise<DiagnoseResult> {
const url = agentId
? `/api/tools/agent-bridge/diagnose?agentId=${encodeURIComponent(agentId)}`
: "/api/tools/agent-bridge/diagnose";
return requestJson<DiagnoseResult>(url);
}
/** Untrust + remove the MITM root CA from the OS store (explicit, idempotent). */

View File

@@ -2,7 +2,7 @@ import { spawn, type ChildProcess } from "child_process";
import path from "path";
import fs from "fs";
import { resolveMitmDataDir } from "./dataDir.ts";
import { removeDNSEntry, removeDNSEntries, checkDNSEntryForAgent } from "./dns/dnsConfig.ts";
import { removeDNSEntry, removeDNSEntries, checkDNSEntryForAgent, checkDNSEntry } from "./dns/dnsConfig.ts";
import { provisionDnsEntries } from "./dns/provision.ts";
import { generateCert } from "./cert/generate.ts";
import { installCertResult, installCaCert } from "./cert/install.ts";
@@ -395,15 +395,16 @@ export async function getMitmStatus(agentId?: string): Promise<{
}
// Check DNS configuration. When an agentId is provided, check THAT agent's
// own hosts (#8466) instead of always checking the Antigravity host set
// callers that don't pass agentId keep the legacy Antigravity-only check.
// own hosts (#8466) instead of always checking the Antigravity host set.
// Fix #8656: no-agentId path now uses checkDNSEntry() which is Windows-aware
// (reads HOSTS_FILE = C:\Windows\System32\drivers\etc\hosts on Windows).
let dnsConfigured = false;
try {
if (agentId) {
dnsConfigured = checkDNSEntryForAgent(agentId);
} else {
const hostsContent = fs.readFileSync("/etc/hosts", "utf-8");
dnsConfigured = /\bdaily-cloudcode-pa\.googleapis\.com\b/.test(hostsContent);
// Use Windows-aware checkDNSEntry() instead of hardcoded /etc/hosts
dnsConfigured = checkDNSEntry();
}
} catch {
// Ignore

View File

@@ -341,12 +341,40 @@ function collectBodyRaw(req) {
});
}
function extractModel(body) {
/**
* Extract the source model name from request body or URL.
*
* For Antigravity (Gemini format):
* - Body may have top-level `model` field: { model: "gemini-2.0-flash", request: {...} }
* - URL may encode model: /v1beta/models/gemini-2.0-flash:generateContent
*
* For other agents (OpenAI format):
* - Body has `model` field: { model: "gpt-4", messages: [...] }
*
* @param {Buffer} body - Request body buffer
* @param {string} url - Request URL path
* @returns {string|null} Extracted model name or null
*/
function extractModel(body, url) {
// Try to extract from body first
try {
return JSON.parse(body.toString()).model || null;
const parsed = JSON.parse(body.toString());
if (parsed && typeof parsed.model === "string" && parsed.model) {
return parsed.model;
}
} catch {
return null;
// Invalid JSON or no model field
}
// Try to extract from URL path (Gemini format: /v1beta/models/<model>:generateContent)
if (url && typeof url === "string") {
const match = url.match(/\/models\/([^/:]+)(?::|\/)/);
if (match && match[1]) {
return match[1];
}
}
return null;
}
/**
@@ -604,7 +632,7 @@ async function startMitmServer() {
const host = String(req.headers.host || "")
.split(":")[0]
.toLowerCase();
const model = bodyBuffer.length > 0 ? extractModel(bodyBuffer) : null;
const model = bodyBuffer.length > 0 ? extractModel(bodyBuffer, req.url) : null;
vlog(
1,
@@ -632,6 +660,30 @@ async function startMitmServer() {
return passthrough(req, res, bodyBuffer);
}
// FIX #8656: Capture ALL agent traffic (even passthrough) so Traffic Inspector
// and model auto-detection work WITHOUT requiring mappings first.
// This fixes the circular dependency: need mappings to see traffic, but need
// to see traffic to create mappings.
//
// Capture happens BEFORE checking for mappings, so requests appear in Traffic
// Inspector even when no mappings exist yet. Status is set to "in-flight"
// initially; will be updated to the actual status code if intercepted.
const startedAt = Date.now();
captureToInspector({
req,
bodyBuffer,
agentId,
sourceModel: model,
mappedModel: model, // Will be overridden if intercepted
status: "in-flight", // Valid schema value (not "passthrough")
respHeaders: {},
respBody: null,
respSize: 0,
error: null,
proxyLatencyMs: 0,
upstreamLatencyMs: 0,
});
const mappedOverride = getMappedOverride(model, agentId);
if (!mappedOverride) {