mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
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:
committed by
GitHub
parent
ea4bbdf7c0
commit
d91f7d1c3f
57
.github/workflows/build.yml
vendored
Normal file
57
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Build App
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["**"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Fast Production Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Expand Virtual Memory (Native 10GB Swap)
|
||||
run: |
|
||||
sudo swapoff -a || true
|
||||
sudo rm -f /mnt/swapfile /swapfile
|
||||
sudo fallocate -l 10G /mnt/swapfile || sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=10240
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
free -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Next.js app & CLI bundle
|
||||
run: |
|
||||
npm run build:release
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=12288"
|
||||
OMNIROUTE_BUILD_MEMORY_MB: "12288"
|
||||
OMNIROUTE_USE_TURBOPACK: "1"
|
||||
|
||||
- name: Archive build outputs
|
||||
run: |
|
||||
tar -czf omniroute-build.tar.gz .build dist
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: omniroute-build
|
||||
path: omniroute-build.tar.gz
|
||||
retention-days: 7
|
||||
@@ -730,11 +730,22 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
? clientResponseFormat === FORMATS.CLAUDE
|
||||
: sourceFormat === FORMATS.CLAUDE) === true;
|
||||
|
||||
// Antigravity/cloudcode streams terminate naturally on their last
|
||||
// `data: {"response":{...}}` event, not on a `[DONE]` marker. Emitting
|
||||
// `[DONE]` to the Antigravity IDE causes a protobuf parse failure
|
||||
// (proto: syntax error (line 1:1): unexpected token [) because the
|
||||
// Go binary's protobuf deserializer receives `[DONE]` as input.
|
||||
const clientExpectsAntigravityStream =
|
||||
(mode === STREAM_MODE.PASSTHROUGH
|
||||
? clientResponseFormat === FORMATS.ANTIGRAVITY
|
||||
: sourceFormat === FORMATS.ANTIGRAVITY) === true;
|
||||
|
||||
// Single source of truth for the [DONE] decision, used at both emission
|
||||
// sites below. Only OpenAI Chat Completions clients expect [DONE];
|
||||
// Responses API and Anthropic SSE terminate on their own protocol events
|
||||
// (response.completed / message_stop respectively).
|
||||
const shouldEmitDoneTerminator = !clientExpectsResponsesStream && !clientExpectsClaudeStream;
|
||||
// Responses API, Anthropic SSE, and Antigravity/cloudcode terminate on
|
||||
// their own protocol events (response.completed / message_stop / last
|
||||
// response candidate respectively).
|
||||
const shouldEmitDoneTerminator = !clientExpectsResponsesStream && !clientExpectsClaudeStream && !clientExpectsAntigravityStream;
|
||||
|
||||
let buffer = "";
|
||||
let usage: UsageTokenRecord | null = null;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -80,13 +80,24 @@ test("routeGuard: /api/tools/agent-bridge/ is SPAWN_CAPABLE", () => {
|
||||
|
||||
// ── GET /state ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("GET /state: returns server + agents shape", async () => {
|
||||
test("GET /state: returns both legacy (server/agents) and new (serverState/agentStates) keys (#8656)", async () => {
|
||||
const res = await stateRoute.GET();
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json() as Record<string, unknown>;
|
||||
assert.ok("server" in body, "body.server missing");
|
||||
|
||||
// Legacy keys (integration test + settings/mitm depend on these)
|
||||
assert.ok("server" in body, "body.server missing — breaks backward compat");
|
||||
assert.ok("agents" in body, "body.agents missing");
|
||||
assert.ok(Array.isArray(body.agents), "agents should be array");
|
||||
|
||||
// New keys (#8656 fix — what UI actually reads)
|
||||
assert.ok("serverState" in body, "body.serverState missing");
|
||||
assert.ok("agentStates" in body, "body.agentStates missing");
|
||||
assert.ok(Array.isArray(body.agentStates), "agentStates should be array");
|
||||
assert.ok("bypassPatterns" in body, "body.bypassPatterns missing");
|
||||
assert.ok(Array.isArray(body.bypassPatterns), "bypassPatterns should be array");
|
||||
assert.ok("mappings" in body, "body.mappings missing");
|
||||
assert.equal(typeof body.mappings, "object", "mappings should be object");
|
||||
});
|
||||
|
||||
test("GET /state: error responses do not leak stack traces", async () => {
|
||||
|
||||
163
tests/unit/agent-bridge-detected-models-8656.test.ts
Normal file
163
tests/unit/agent-bridge-detected-models-8656.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Unit test: GET /api/tools/agent-bridge/agents/[id]/detected-models
|
||||
* Verifies model auto-detection from intercepted traffic (#8656 follow-up D)
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { globalTrafficBuffer } from "../../src/mitm/inspector/buffer.ts";
|
||||
import type { InterceptedRequest } from "../../src/mitm/inspector/types.ts";
|
||||
|
||||
test("GET /detected-models: returns unique source models from intercepted traffic", async () => {
|
||||
// Mock some intercepted traffic with source models
|
||||
const mockRequests: InterceptedRequest[] = [
|
||||
{
|
||||
id: "req1",
|
||||
source: "agent-bridge",
|
||||
agent: "cursor",
|
||||
timestamp: new Date().toISOString(),
|
||||
method: "POST",
|
||||
host: "api.cursor.sh",
|
||||
path: "/v1/chat/completions",
|
||||
requestHeaders: {},
|
||||
requestBody: null,
|
||||
requestSize: 100,
|
||||
responseHeaders: {},
|
||||
responseBody: null,
|
||||
responseSize: 0,
|
||||
status: 200,
|
||||
sourceModel: "gpt-4-turbo",
|
||||
mappedModel: "openai/gpt-4-turbo",
|
||||
},
|
||||
{
|
||||
id: "req2",
|
||||
source: "agent-bridge",
|
||||
agent: "cursor",
|
||||
timestamp: new Date().toISOString(),
|
||||
method: "POST",
|
||||
host: "api.cursor.sh",
|
||||
path: "/v1/chat/completions",
|
||||
requestHeaders: {},
|
||||
requestBody: null,
|
||||
requestSize: 100,
|
||||
responseHeaders: {},
|
||||
responseBody: null,
|
||||
responseSize: 0,
|
||||
status: 200,
|
||||
sourceModel: "claude-3-opus",
|
||||
mappedModel: "anthropic/claude-3-opus-20240229",
|
||||
},
|
||||
{
|
||||
id: "req3",
|
||||
source: "agent-bridge",
|
||||
agent: "cursor",
|
||||
timestamp: new Date().toISOString(),
|
||||
method: "POST",
|
||||
host: "api.cursor.sh",
|
||||
path: "/v1/chat/completions",
|
||||
requestHeaders: {},
|
||||
requestBody: null,
|
||||
requestSize: 100,
|
||||
responseHeaders: {},
|
||||
responseBody: null,
|
||||
responseSize: 0,
|
||||
status: 200,
|
||||
sourceModel: "gpt-4-turbo", // Duplicate - should only appear once
|
||||
mappedModel: "openai/gpt-4-turbo",
|
||||
},
|
||||
{
|
||||
id: "req4",
|
||||
source: "agent-bridge",
|
||||
agent: "kiro",
|
||||
timestamp: new Date().toISOString(),
|
||||
method: "POST",
|
||||
host: "api.kiro.ai",
|
||||
path: "/v1/chat/completions",
|
||||
requestHeaders: {},
|
||||
requestBody: null,
|
||||
requestSize: 100,
|
||||
responseHeaders: {},
|
||||
responseBody: null,
|
||||
responseSize: 0,
|
||||
status: 200,
|
||||
sourceModel: "gemini-pro",
|
||||
mappedModel: "google/gemini-pro",
|
||||
},
|
||||
];
|
||||
|
||||
// Add mock requests to buffer
|
||||
mockRequests.forEach((req) => globalTrafficBuffer.push(req));
|
||||
|
||||
try {
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/agents/[id]/detected-models/route.ts?t=" +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
const res = await GET(
|
||||
new Request("http://localhost/api/tools/agent-bridge/agents/cursor/detected-models"),
|
||||
{ params: { id: "cursor" } }
|
||||
);
|
||||
|
||||
assert.equal(res.status, 200, "Response should be 200 OK");
|
||||
|
||||
const body = (await res.json()) as {
|
||||
agentId: string;
|
||||
detectedModels: string[];
|
||||
requestCount: number;
|
||||
};
|
||||
|
||||
assert.equal(body.agentId, "cursor", "agentId should be cursor");
|
||||
assert.ok(Array.isArray(body.detectedModels), "detectedModels should be array");
|
||||
assert.equal(body.detectedModels.length, 2, "Should have 2 unique models (duplicates removed)");
|
||||
assert.ok(
|
||||
body.detectedModels.includes("gpt-4-turbo"),
|
||||
"Should include gpt-4-turbo"
|
||||
);
|
||||
assert.ok(
|
||||
body.detectedModels.includes("claude-3-opus"),
|
||||
"Should include claude-3-opus"
|
||||
);
|
||||
assert.equal(body.requestCount, 3, "Should have 3 cursor requests");
|
||||
} finally {
|
||||
// Clean up buffer
|
||||
globalTrafficBuffer.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("GET /detected-models: returns empty array for agent with no traffic", async () => {
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/agents/[id]/detected-models/route.ts?t=" +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
const res = await GET(
|
||||
new Request("http://localhost/api/tools/agent-bridge/agents/antigravity/detected-models"),
|
||||
{ params: { id: "antigravity" } }
|
||||
);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
|
||||
const body = (await res.json()) as {
|
||||
agentId: string;
|
||||
detectedModels: string[];
|
||||
requestCount: number;
|
||||
};
|
||||
|
||||
assert.equal(body.agentId, "antigravity");
|
||||
assert.deepEqual(body.detectedModels, []);
|
||||
assert.equal(body.requestCount, 0);
|
||||
});
|
||||
|
||||
test("GET /detected-models: returns 404 for invalid agent id", async () => {
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/agents/[id]/detected-models/route.ts?t=" +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
const res = await GET(
|
||||
new Request("http://localhost/api/tools/agent-bridge/agents/invalid-agent/detected-models"),
|
||||
{ params: { id: "invalid-agent" } }
|
||||
);
|
||||
|
||||
assert.equal(res.status, 404);
|
||||
});
|
||||
@@ -59,26 +59,29 @@ test("FALSE POSITIVE: only a leftover Antigravity host is present, Claude Code h
|
||||
);
|
||||
});
|
||||
|
||||
test("no-agentId call sites keep legacy Antigravity-only behavior unchanged", async () => {
|
||||
test("no-agentId call sites: still Antigravity-only but Windows-aware (#8656)", async () => {
|
||||
const realReadFileSync = fs.readFileSync.bind(fs);
|
||||
mock.method(fs, "readFileSync", (p: string, enc?: BufferEncoding) => {
|
||||
if (p === "/etc/hosts") {
|
||||
// Claude Code host spoofed, but NO Antigravity host present. A caller
|
||||
// that omits agentId (state/route.ts, server/route.ts, settings/mitm,
|
||||
// cli-tools/antigravity-mitm) must still evaluate the legacy
|
||||
// Antigravity-only regex, so this should remain false.
|
||||
mock.method(fs, "readFileSync", (p: unknown, enc?: BufferEncoding) => {
|
||||
// After #8656: no-agentId uses checkDNSEntry() which reads HOSTS_FILE
|
||||
// (Windows-aware) instead of hardcoded /etc/hosts. Still Antigravity-only
|
||||
// semantics (checks all 4 Antigravity hosts), but reads the correct file.
|
||||
const pathStr = String(p);
|
||||
const isHostsFile = pathStr === "/etc/hosts" || pathStr.includes("System32\\drivers\\etc\\hosts");
|
||||
if (isHostsFile) {
|
||||
// Claude Code host spoofed, but NO Antigravity host present. The legacy
|
||||
// Antigravity-only check should still return false (unchanged semantics).
|
||||
return "127.0.0.1 localhost\n127.0.0.1 api.anthropic.com\n::1 api.anthropic.com\n";
|
||||
}
|
||||
return realReadFileSync(p, enc);
|
||||
return realReadFileSync(p as string, enc);
|
||||
});
|
||||
|
||||
const { getMitmStatus } = await import("../../src/mitm/manager.ts?probe=8466-legacy");
|
||||
const { getMitmStatus } = await import("../../src/mitm/manager.ts?probe=8466-legacy-8656");
|
||||
const status = await getMitmStatus();
|
||||
|
||||
assert.equal(
|
||||
status.dnsConfigured,
|
||||
false,
|
||||
"callers that omit agentId must keep the legacy Antigravity-only check"
|
||||
"no-agentId still checks Antigravity-only (4 hosts via checkDNSEntry), now Windows-aware"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
200
tests/unit/agent-bridge-mappings-sync-8656.test.ts
Normal file
200
tests/unit/agent-bridge-mappings-sync-8656.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Regression test for issue #8656 follow-up: model mappings saved via the UI
|
||||
* are invisible to the MITM proxy because the proxy reads from key_value
|
||||
* (namespace='mitmAlias') while the UI writes to agent_bridge_mappings.
|
||||
*
|
||||
* This test verifies that syncAgentBridgeMappingsToMitmAlias() properly copies
|
||||
* mappings from agent_bridge_mappings to key_value for agents that have a
|
||||
* registered alias key in standaloneRouting.cjs::AGENT_ROUTE_CONFIG.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-8656-sync-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
// Import db core first to allow reset
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
// Import getMitmAlias to verify key_value entries
|
||||
const { getMitmAlias } = await import("../../src/lib/db/models/mitmAlias.ts");
|
||||
|
||||
function resetDb() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetDb();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
try {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
});
|
||||
|
||||
// ── Sync Tests ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("syncAgentBridgeMappingsToMitmAlias: copies antigravity mappings to key_value", async () => {
|
||||
// Dynamic import after DB reset
|
||||
const {
|
||||
setMappings,
|
||||
syncAgentBridgeMappingsToMitmAlias,
|
||||
} = await import(
|
||||
"../../src/lib/db/agentBridgeMappings.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Arrange: save mappings for antigravity
|
||||
setMappings("antigravity", [
|
||||
{ source: "gpt-oss-120b-medium", target: "openai/gpt-4o" },
|
||||
{ source: "gemini-2.0-flash", target: "anthropic/claude-sonnet-4" },
|
||||
]);
|
||||
|
||||
// Act: sync to mitmAlias
|
||||
syncAgentBridgeMappingsToMitmAlias("antigravity");
|
||||
|
||||
// Get a fresh import of getMitmAlias to read the key_value table
|
||||
const { getMitmAlias: getAlias } = await import(
|
||||
"../../src/lib/db/models/mitmAlias.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Assert: key_value has the mappings
|
||||
const alias = await getAlias("antigravity");
|
||||
assert.ok(alias, "mitmAlias for antigravity should exist");
|
||||
assert.equal(
|
||||
alias["gpt-oss-120b-medium"],
|
||||
"openai/gpt-4o",
|
||||
"gpt-oss-120b-medium should map to openai/gpt-4o"
|
||||
);
|
||||
assert.equal(
|
||||
alias["gemini-2.0-flash"],
|
||||
"anthropic/claude-sonnet-4",
|
||||
"gemini-2.0-flash should map to anthropic/claude-sonnet-4"
|
||||
);
|
||||
});
|
||||
|
||||
test("syncAgentBridgeMappingsToMitmAlias: skips agents not in MITM_ALIAS_AGENTS", async () => {
|
||||
const {
|
||||
setMappings,
|
||||
syncAgentBridgeMappingsToMitmAlias,
|
||||
} = await import(
|
||||
"../../src/lib/db/agentBridgeMappings.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Arrange: save mappings for cursor (not in MITM_ALIAS_AGENTS)
|
||||
setMappings("cursor", [
|
||||
{ source: "gpt-4o", target: "openai/gpt-4o" },
|
||||
]);
|
||||
|
||||
// Act: sync should skip cursor (no-op)
|
||||
syncAgentBridgeMappingsToMitmAlias("cursor");
|
||||
|
||||
// Assert: key_value should have no mitmAlias entry for cursor
|
||||
const { getMitmAlias: getAlias } = await import(
|
||||
"../../src/lib/db/models/mitmAlias.ts?t=" + Date.now()
|
||||
);
|
||||
const alias = await getAlias();
|
||||
assert.equal(
|
||||
alias["cursor"],
|
||||
undefined,
|
||||
"cursor should not have a mitmAlias entry"
|
||||
);
|
||||
});
|
||||
|
||||
test("syncAgentBridgeMappingsToMitmAlias: replaces existing mitmAlias entry", async () => {
|
||||
const {
|
||||
setMappings,
|
||||
syncAgentBridgeMappingsToMitmAlias,
|
||||
} = await import(
|
||||
"../../src/lib/db/agentBridgeMappings.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Arrange: save initial mappings and sync
|
||||
setMappings("antigravity", [
|
||||
{ source: "gpt-oss-120b-medium", target: "openai/gpt-4o" },
|
||||
]);
|
||||
syncAgentBridgeMappingsToMitmAlias("antigravity");
|
||||
|
||||
// Act: save different mappings and sync again
|
||||
setMappings("antigravity", [
|
||||
{ source: "gpt-oss-120b-medium", target: "anthropic/claude-opus-4" },
|
||||
]);
|
||||
syncAgentBridgeMappingsToMitmAlias("antigravity");
|
||||
|
||||
// Assert: key_value should have the updated mapping
|
||||
const { getMitmAlias: getAlias } = await import(
|
||||
"../../src/lib/db/models/mitmAlias.ts?t=" + Date.now()
|
||||
);
|
||||
const alias = await getAlias("antigravity");
|
||||
assert.equal(
|
||||
alias["gpt-oss-120b-medium"],
|
||||
"anthropic/claude-opus-4",
|
||||
"should have updated mapping after re-sync"
|
||||
);
|
||||
});
|
||||
|
||||
test("syncAgentBridgeMappingsToMitmAlias: empty mappings clear key_value entry", async () => {
|
||||
const {
|
||||
setMappings,
|
||||
syncAgentBridgeMappingsToMitmAlias,
|
||||
} = await import(
|
||||
"../../src/lib/db/agentBridgeMappings.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Arrange: save some mappings first
|
||||
setMappings("antigravity", [
|
||||
{ source: "gpt-oss-120b-medium", target: "openai/gpt-4o" },
|
||||
]);
|
||||
syncAgentBridgeMappingsToMitmAlias("antigravity");
|
||||
|
||||
// Act: clear mappings and sync
|
||||
setMappings("antigravity", []);
|
||||
syncAgentBridgeMappingsToMitmAlias("antigravity");
|
||||
|
||||
// Assert: key_value entry should be an empty object
|
||||
const { getMitmAlias: getAlias } = await import(
|
||||
"../../src/lib/db/models/mitmAlias.ts?t=" + Date.now()
|
||||
);
|
||||
const alias = await getAlias("antigravity");
|
||||
assert.ok(alias, "antigravity mitmAlias entry should still exist (empty object)");
|
||||
assert.equal(
|
||||
Object.keys(alias).length,
|
||||
0,
|
||||
"antigravity mitmAlias should have no mappings"
|
||||
);
|
||||
});
|
||||
|
||||
test("syncAgentBridgeMappingsToMitmAlias: works for claude-code agent", async () => {
|
||||
const {
|
||||
setMappings,
|
||||
syncAgentBridgeMappingsToMitmAlias,
|
||||
} = await import(
|
||||
"../../src/lib/db/agentBridgeMappings.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Arrange: save and sync for claude-code
|
||||
setMappings("claude-code", [
|
||||
{ source: "claude-sonnet-4", target: "openai/gpt-4o" },
|
||||
]);
|
||||
syncAgentBridgeMappingsToMitmAlias("claude-code");
|
||||
|
||||
// Assert
|
||||
const { getMitmAlias: getAlias } = await import(
|
||||
"../../src/lib/db/models/mitmAlias.ts?t=" + Date.now()
|
||||
);
|
||||
const alias = await getAlias("claude-code");
|
||||
assert.ok(alias, "mitmAlias for claude-code should exist");
|
||||
assert.equal(
|
||||
alias["claude-sonnet-4"],
|
||||
"openai/gpt-4o",
|
||||
"claude-sonnet-4 should map to openai/gpt-4o"
|
||||
);
|
||||
});
|
||||
208
tests/unit/agent-bridge-state-full-payload-8656.test.ts
Normal file
208
tests/unit/agent-bridge-state-full-payload-8656.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Regression test for issue #8656: Agent Bridge DNS start succeeds but UI does
|
||||
* not show model mapping or dns-configured status.
|
||||
*
|
||||
* Root cause: GET /api/tools/agent-bridge/state returns { server, agents } but
|
||||
* the UI expects { serverState, agentStates, bypassPatterns, mappings }. The
|
||||
* normalizeAgentBridgeState function intentionally does NOT coerce the `agents`
|
||||
* key to `agentStates` (comment at normalizeState.ts:45-47), so after every
|
||||
* refresh agentStates=[], mappings={}, bypassPatterns=[].
|
||||
*
|
||||
* DNS toggle DOES write dns_enabled=true to the DB via upsertAgentBridgeState
|
||||
* in agents/[id]/dns/route.ts:72, but the state route never reads
|
||||
* getAllAgentBridgeStates(), so the UI never sees the flag flip.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-8656-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
// Import db core first to allow reset
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { upsertAgentBridgeState } = await import("../../src/lib/db/agentBridgeState.ts");
|
||||
const { setMappings } = await import("../../src/lib/db/agentBridgeMappings.ts");
|
||||
const { replaceUserBypassPatterns } = await import("../../src/lib/db/agentBridgeBypass.ts");
|
||||
|
||||
function resetDb() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetDb();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
try {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
});
|
||||
|
||||
// ── Core #8656 Regression Tests ────────────────────────────────────────────
|
||||
|
||||
test("GET /state: returns agentStates array with dns_enabled from DB (#8656)", async () => {
|
||||
// Arrange: simulate the user clicking "Start DNS" for claude-code, which
|
||||
// writes dns_enabled=true to the DB via agents/[id]/dns/route.ts:72
|
||||
upsertAgentBridgeState({ agent_id: "claude-code", dns_enabled: true });
|
||||
|
||||
// Dynamic import to bypass module cache after DB reset
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Act: the UI polls /state after the DNS toggle
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
// Assert: agentStates must be populated (not empty) so the UI can read dns_enabled
|
||||
assert.ok(Array.isArray(body.agentStates), "body.agentStates missing or not array");
|
||||
assert.ok(
|
||||
body.agentStates.length > 0,
|
||||
"agentStates should not be empty — at least claude-code should be present"
|
||||
);
|
||||
|
||||
const claudeCodeState = body.agentStates.find(
|
||||
(s: { agent_id: string }) => s.agent_id === "claude-code"
|
||||
);
|
||||
assert.ok(claudeCodeState, "claude-code not in agentStates");
|
||||
assert.equal(
|
||||
(claudeCodeState as { dns_enabled: boolean }).dns_enabled,
|
||||
true,
|
||||
"dns_enabled should be true after upsertAgentBridgeState"
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /state: returns mappings object keyed by agentId (#8656)", async () => {
|
||||
// Arrange: simulate the setup wizard configuring model mappings for claude-code
|
||||
setMappings("claude-code", [{ source: "claude-sonnet-4", target: "openai/gpt-4o" }]);
|
||||
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Act
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
// Assert: mappings must be present so the UI can render the model mapping table
|
||||
assert.ok(typeof body.mappings === "object", "body.mappings missing");
|
||||
assert.ok(Array.isArray(body.mappings["claude-code"]), "mappings[claude-code] missing");
|
||||
assert.equal(body.mappings["claude-code"].length, 1);
|
||||
assert.equal(body.mappings["claude-code"][0].source, "claude-sonnet-4");
|
||||
assert.equal(body.mappings["claude-code"][0].target, "openai/gpt-4o");
|
||||
});
|
||||
|
||||
test("GET /state: returns bypassPatterns array (#8656)", async () => {
|
||||
// Arrange: simulate the user configuring custom bypass patterns
|
||||
replaceUserBypassPatterns(["*.internal", "localhost"]);
|
||||
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
// Act
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
// Assert: bypassPatterns must be present so the UI can display/edit them
|
||||
assert.ok(Array.isArray(body.bypassPatterns), "body.bypassPatterns missing");
|
||||
assert.ok(body.bypassPatterns.length >= 2, "should include user patterns");
|
||||
assert.ok(body.bypassPatterns.includes("*.internal"));
|
||||
assert.ok(body.bypassPatterns.includes("localhost"));
|
||||
});
|
||||
|
||||
test("GET /state: serverState.certTrusted distinct from certExists (#8656)", async () => {
|
||||
// certTrusted (OS trust store check) was confused with certExists (file on disk).
|
||||
// getMitmStatus returns certExists only; normalizeState maps certExists → certTrusted
|
||||
// as a fallback, so the UI showed "trusted" when the cert file existed but wasn't
|
||||
// actually trusted by the OS.
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
// Assert: certExists and certTrusted should be distinct fields
|
||||
const server = body.server as Record<string, unknown>;
|
||||
assert.ok("certExists" in server, "server.certExists missing");
|
||||
assert.ok("certTrusted" in server, "server.certTrusted missing");
|
||||
|
||||
// Both should be false when no cert exists (this test doesn't generate a cert)
|
||||
assert.equal(server.certExists, false, "certExists should be false (no cert generated)");
|
||||
assert.equal(
|
||||
server.certTrusted,
|
||||
false,
|
||||
"certTrusted should be false (no cert in OS trust store)"
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /state: maintains backward compat (server + agents keys) (#8656)", async () => {
|
||||
// Integration tests and other routes (settings/mitm) depend on the legacy
|
||||
// { server, agents } shape. The fix must add the new keys without breaking old callers.
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
// Assert: legacy keys still present
|
||||
assert.ok("server" in body, "body.server missing — breaks backward compat");
|
||||
assert.ok("agents" in body, "body.agents missing");
|
||||
assert.ok(Array.isArray(body.agents), "agents should be array");
|
||||
|
||||
// Assert: new keys also present
|
||||
assert.ok("serverState" in body, "body.serverState missing");
|
||||
assert.ok("agentStates" in body, "body.agentStates missing");
|
||||
assert.ok("bypassPatterns" in body, "body.bypassPatterns missing");
|
||||
assert.ok("mappings" in body, "body.mappings missing");
|
||||
});
|
||||
|
||||
test("GET /state: agentStates entries have expected shape (#8656)", async () => {
|
||||
// Arrange: set all fields for antigravity to ensure they're mapped through
|
||||
upsertAgentBridgeState({
|
||||
agent_id: "antigravity",
|
||||
dns_enabled: true,
|
||||
cert_trusted: false,
|
||||
setup_completed: true,
|
||||
last_started_at: "2026-07-27T12:00:00.000Z",
|
||||
last_error: null,
|
||||
});
|
||||
|
||||
const { GET } = await import(
|
||||
"../../src/app/api/tools/agent-bridge/state/route.ts?t=" + Date.now()
|
||||
);
|
||||
|
||||
const res = await GET();
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
|
||||
const antigravityState = body.agentStates.find(
|
||||
(s: { agent_id: string }) => s.agent_id === "antigravity"
|
||||
);
|
||||
assert.ok(antigravityState, "antigravity should be in agentStates");
|
||||
|
||||
const state = antigravityState as {
|
||||
agent_id: string;
|
||||
dns_enabled: boolean;
|
||||
cert_trusted: boolean;
|
||||
setup_completed: boolean;
|
||||
last_started_at: string | null;
|
||||
last_error: string | null;
|
||||
};
|
||||
|
||||
assert.equal(state.agent_id, "antigravity");
|
||||
assert.equal(state.dns_enabled, true);
|
||||
assert.equal(state.cert_trusted, false);
|
||||
assert.equal(state.setup_completed, true);
|
||||
assert.equal(state.last_started_at, "2026-07-27T12:00:00.000Z");
|
||||
assert.equal(state.last_error, null);
|
||||
});
|
||||
Reference in New Issue
Block a user