Files
arcadia-admin/app/routes/integrations.tsx
jules ab116f8465 refactor: rename @crema/arcadia-client → @crema/arcadia-core-client
Disambiguates the Phoenix/auth client lib from lib-arcadia-agents-client.
Dir lib-arcadia-client → lib-arcadia-core-client; alias updated in
tsconfig paths, vite config, app.css @source, imports, CI and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:31:56 +10:00

633 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Integrations (operator) — platform/pooled external-API arrangements across
// every scope, backed by the integration registry on arcadia-llm-gateway
// (`/api/v1/integrations*`). The operator manages pooled credentials and
// inspects cross-tenant usage metadata; secrets are write-only.
import { useCallback, useEffect, useMemo, useState } from "react"
import {
AlertTriangle,
CheckCircle2,
FlaskConical,
KeyRound,
Pencil,
Plug,
Plus,
Trash2,
} from "lucide-react"
import { ArcadiaError } from "@crema/arcadia-core-client"
import { AppShell } from "~/components/layout/app-shell"
import { Badge } from "~/components/ui/badge"
import { Button } from "~/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog"
import { Input } from "~/components/ui/input"
import { Label } from "~/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select"
import { Switch } from "~/components/ui/switch"
import { useGatewayClient } from "~/lib/gateway"
import {
addCredential,
createIntegration,
credentialHealth,
deleteIntegration,
formatUsd,
listIntegrations,
testIntegration,
updateIntegration,
usageSummary,
type AuthKind,
type Integration,
type Scope,
type UsageEntry,
} from "~/lib/arcadia/integrations"
const AUTH_KINDS: AuthKind[] = ["bearer_static", "api_key_header", "basic", "oauth2"]
const SCOPES: Scope[] = ["platform", "tenant", "app", "user", "agent"]
const SCOPE_FILTERS: Array<Scope | "all"> = ["all", ...SCOPES]
type Form = {
scope: Scope
scope_id: string
provider: string
capability: string
display_name: string
unit: string
price_usd: string
monthly_budget_usd: string
secret_name: string
auth_kind: AuthKind
secret: string
pooled: boolean
}
const emptyForm: Form = {
scope: "platform",
scope_id: "",
provider: "",
capability: "",
display_name: "",
unit: "call",
price_usd: "",
monthly_budget_usd: "",
secret_name: "",
auth_kind: "bearer_static",
secret: "",
pooled: true,
}
export default function IntegrationsRoute() {
const gw = useGatewayClient()
const [items, setItems] = useState<Integration[]>([])
const [usage, setUsage] = useState<UsageEntry[]>([])
const [scopeFilter, setScopeFilter] = useState<Scope | "all">("all")
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [editing, setEditing] = useState<Integration | "new" | null>(null)
const [tests, setTests] = useState<Record<string, { ok: boolean; message: string }>>({})
const refresh = useCallback(async () => {
setError(null)
const filter = scopeFilter === "all" ? {} : { scope: scopeFilter }
try {
const [list, use] = await Promise.all([
listIntegrations(gw, filter),
usageSummary(gw, filter).catch(() => [] as UsageEntry[]),
])
setItems(list)
setUsage(use)
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load integrations.")
} finally {
setLoading(false)
}
}, [gw, scopeFilter])
useEffect(() => {
void refresh()
}, [refresh])
const usageById = useMemo(
() => new Map(usage.map((u) => [u.integration_id, u] as const)),
[usage],
)
const runTest = useCallback(
async (it: Integration) => {
setTests((t) => ({ ...t, [it.id]: { ok: true, message: "Testing…" } }))
try {
const verdict = await testIntegration(gw, it.id)
const remaining = verdict.policy?.remaining_budget_usd
setTests((t) => ({
...t,
[it.id]: {
ok: true,
message:
verdict.status === "ok"
? `OK — within budget & rate${remaining ? ` (${formatUsd(remaining)} left)` : ""}`
: verdict.status,
},
}))
} catch (e) {
const msg =
e instanceof ArcadiaError
? e.status === 409
? "Credential expired — rotate it"
: e.status === 429
? "Over budget / rate limit"
: e.status === 404
? "No credential to test"
: e.message
: "Test failed"
setTests((t) => ({ ...t, [it.id]: { ok: false, message: msg } }))
}
},
[gw],
)
const toggleEnabled = useCallback(
async (it: Integration, enabled: boolean) => {
setItems((xs) => xs.map((x) => (x.id === it.id ? { ...x, enabled } : x)))
try {
await updateIntegration(gw, it.id, { enabled })
} catch {
setItems((xs) => xs.map((x) => (x.id === it.id ? { ...x, enabled: !enabled } : x)))
}
},
[gw],
)
const remove = useCallback(
async (it: Integration) => {
if (!window.confirm(`Delete ${it.display_name || it.provider} and its credentials?`)) return
await deleteIntegration(gw, it.id)
await refresh()
},
[gw, refresh],
)
return (
<AppShell>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Plug className="size-5" />
</div>
<div>
<h1 className="text-2xl font-semibold">Integrations</h1>
<p className="text-sm text-muted-foreground">
Platform &amp; pooled external-API credentials across every scope.
Keys are stored encrypted and never shown; usage is metadata only.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Select value={scopeFilter} onValueChange={(v) => setScopeFilter((v as Scope | "all") ?? "all")}>
<SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SCOPE_FILTERS.map((s) => (
<SelectItem key={s} value={s}>
{s === "all" ? "All scopes" : s}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setEditing("new")}>
<Plus className="size-4" /> Add integration
</Button>
</div>
</div>
{error ? (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-destructive">Couldnt load integrations</CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
</Card>
) : loading ? (
<p className="text-sm text-muted-foreground">Loading</p>
) : items.length === 0 ? (
<Card>
<CardHeader>
<CardTitle>No integrations in this scope</CardTitle>
<CardDescription>
Register a platform/pooled arrangement a shared key the platform
meters and bills to tenants who opt in.
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => setEditing("new")}>
<Plus className="size-4" /> Add integration
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{items.map((it) => {
const u = usageById.get(it.id)
const test = tests[it.id]
return (
<Card key={it.id}>
<CardHeader>
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2">
{it.display_name || it.provider}
<Badge>{it.scope}</Badge>
{it.scope_id ? (
<span className="font-mono text-xs text-muted-foreground">
{it.scope_id}
</span>
) : null}
{it.capability ? (
<Badge variant="secondary">{it.capability}</Badge>
) : null}
</CardTitle>
<CardDescription>
{it.provider}
{it.cost_model?.price_usd
? ` · ${formatUsd(it.cost_model.price_usd)}/${it.cost_model.unit ?? "call"}`
: ""}
{it.constraints?.monthly_budget_usd
? ` · budget ${formatUsd(it.constraints.monthly_budget_usd)}/mo`
: ""}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Label htmlFor={`en-${it.id}`} className="text-xs text-muted-foreground">
{it.enabled ? "Enabled" : "Disabled"}
</Label>
<Switch
id={`en-${it.id}`}
checked={it.enabled}
onCheckedChange={(v) => toggleEnabled(it, v)}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-1">
{it.credentials.length === 0 ? (
<p className="text-sm text-muted-foreground">No credential set.</p>
) : (
it.credentials.map((cred) => {
const health = credentialHealth(cred)
return (
<div key={cred.id} className="flex items-center gap-2 text-sm">
<KeyRound className="size-4 text-muted-foreground" />
<span className="font-mono">{cred.secret_name}</span>
<Badge variant="outline">{cred.source}</Badge>
<HealthBadge health={health} />
{cred.expires_at ? (
<span className="text-xs text-muted-foreground">
expires {new Date(cred.expires_at).toLocaleDateString()}
</span>
) : null}
</div>
)
})
)}
</div>
<p className="text-sm text-muted-foreground">
{u ? `${u.calls} calls · ${formatUsd(u.cost_usd)} this month` : "No usage yet"}
</p>
{test ? (
<p
className={`text-sm ${test.ok ? "text-emerald-600 dark:text-emerald-400" : "text-destructive"}`}
>
{test.message}
</p>
) : null}
<div className="flex flex-wrap gap-2 pt-1">
<Button variant="outline" size="sm" onClick={() => runTest(it)}>
<FlaskConical className="size-4" /> Test
</Button>
<Button variant="outline" size="sm" onClick={() => setEditing(it)}>
<Pencil className="size-4" /> Edit
</Button>
<Button
variant="ghost"
size="sm"
className="text-destructive"
onClick={() => remove(it)}
>
<Trash2 className="size-4" /> Delete
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
{editing ? (
<IntegrationDialog
mode={editing === "new" ? "new" : "edit"}
initial={editing === "new" ? null : editing}
onClose={() => setEditing(null)}
onSaved={async () => {
setEditing(null)
await refresh()
}}
/>
) : null}
</AppShell>
)
}
function HealthBadge({ health }: { health: ReturnType<typeof credentialHealth> }) {
if (health === "ok")
return (
<Badge variant="secondary" className="gap-1">
<CheckCircle2 className="size-3" /> healthy
</Badge>
)
const label = health === "missing" ? "no secret" : health
return (
<Badge variant="destructive" className="gap-1">
<AlertTriangle className="size-3" /> {label}
</Badge>
)
}
function IntegrationDialog({
mode,
initial,
onClose,
onSaved,
}: {
mode: "new" | "edit"
initial: Integration | null
onClose: () => void
onSaved: () => void | Promise<void>
}) {
const gw = useGatewayClient()
const [form, setForm] = useState<Form>(() =>
initial
? {
...emptyForm,
scope: initial.scope,
scope_id: initial.scope_id ?? "",
provider: initial.provider,
capability: initial.capability ?? "",
display_name: initial.display_name ?? "",
unit: initial.cost_model?.unit ?? "call",
price_usd: initial.cost_model?.price_usd?.toString() ?? "",
monthly_budget_usd: initial.constraints?.monthly_budget_usd?.toString() ?? "",
}
: emptyForm,
)
const [saving, setSaving] = useState(false)
const [err, setErr] = useState<string | null>(null)
const set = (patch: Partial<Form>) => setForm((f) => ({ ...f, ...patch }))
const needsScopeId = form.scope !== "platform"
const submit = async () => {
setSaving(true)
setErr(null)
try {
const cost_model = form.price_usd
? { unit: form.unit as "call" | "search" | "1k_tokens", price_usd: form.price_usd, currency: "USD" }
: undefined
const constraints = form.monthly_budget_usd
? { monthly_budget_usd: form.monthly_budget_usd }
: undefined
if (mode === "edit" && initial) {
await updateIntegration(gw, initial.id, {
provider: form.provider.trim(),
capability: form.capability.trim() || undefined,
display_name: form.display_name.trim() || undefined,
cost_model,
constraints,
})
} else {
const created = await createIntegration(gw, {
scope: form.scope,
scope_id: needsScopeId ? form.scope_id.trim() || undefined : undefined,
provider: form.provider.trim(),
capability: form.capability.trim() || undefined,
display_name: form.display_name.trim() || undefined,
cost_model,
constraints,
})
if (form.secret_name.trim() && form.secret.trim()) {
await addCredential(gw, created.id, {
secret_name: form.secret_name.trim(),
auth_kind: form.auth_kind,
secret: form.secret,
source: form.pooled ? "pooled" : "byo",
})
}
}
await onSaved()
} catch (e) {
setErr(e instanceof Error ? e.message : "Save failed.")
} finally {
setSaving(false)
}
}
return (
<Dialog open onOpenChange={(o) => (!o ? onClose() : undefined)}>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{mode === "new" ? "Add integration" : "Edit integration"}</DialogTitle>
<DialogDescription>
Register an external-API arrangement. Platform scope = a pooled key
the platform meters and bills.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
{mode === "new" ? (
<div className="grid grid-cols-2 gap-3">
<Field label="Scope">
<Select value={form.scope} onValueChange={(v) => set({ scope: (v as Scope) ?? "platform" })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{SCOPES.map((s) => (
<SelectItem key={s} value={s}>
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<Field label="Scope ID" hint={needsScopeId ? "tenant/app/user/agent id" : "n/a for platform"}>
<Input
value={form.scope_id}
onChange={(e) => set({ scope_id: e.target.value })}
disabled={!needsScopeId}
placeholder={needsScopeId ? "acme" : "—"}
/>
</Field>
</div>
) : null}
<Field label="Provider" hint="e.g. tavily, google_maps, duffel">
<Input
value={form.provider}
onChange={(e) => set({ provider: e.target.value })}
placeholder="tavily"
/>
</Field>
<Field label="Capability (optional)" hint="e.g. web_search, geocode">
<Input
value={form.capability}
onChange={(e) => set({ capability: e.target.value })}
placeholder="web_search"
/>
</Field>
<Field label="Display name (optional)">
<Input
value={form.display_name}
onChange={(e) => set({ display_name: e.target.value })}
/>
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label="Price (USD)" hint="per unit, for metering">
<Input
inputMode="decimal"
value={form.price_usd}
onChange={(e) => set({ price_usd: e.target.value })}
placeholder="0.01"
/>
</Field>
<Field label="Unit">
<Select value={form.unit} onValueChange={(v) => set({ unit: v ?? "call" })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="call">call</SelectItem>
<SelectItem value="search">search</SelectItem>
<SelectItem value="1k_tokens">1k_tokens</SelectItem>
</SelectContent>
</Select>
</Field>
</div>
<Field label="Monthly budget (USD, optional)" hint="resolve is refused past this">
<Input
inputMode="decimal"
value={form.monthly_budget_usd}
onChange={(e) => set({ monthly_budget_usd: e.target.value })}
placeholder="500"
/>
</Field>
{mode === "new" ? (
<div className="space-y-3 rounded-lg border p-3">
<p className="text-sm font-medium">Credential (optional)</p>
<Field label="Secret name" hint="the stable handle tools resolve by">
<Input
value={form.secret_name}
onChange={(e) => set({ secret_name: e.target.value })}
placeholder="tavily_default"
/>
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label="Auth kind">
<Select
value={form.auth_kind}
onValueChange={(v) => set({ auth_kind: (v as AuthKind) ?? "bearer_static" })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{AUTH_KINDS.map((k) => (
<SelectItem key={k} value={k}>
{k}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<Field label="Source">
<div className="flex h-9 items-center gap-2">
<Switch
id="pooled"
checked={form.pooled}
onCheckedChange={(v) => set({ pooled: v })}
/>
<Label htmlFor="pooled" className="text-sm">
{form.pooled ? "pooled (billed)" : "BYO key"}
</Label>
</div>
</Field>
</div>
<Field label="Secret value" hint="stored encrypted, never shown again">
<Input
type="password"
value={form.secret}
onChange={(e) => set({ secret: e.target.value })}
placeholder="sk-…"
/>
</Field>
</div>
) : null}
{err ? <p className="text-sm text-destructive">{err}</p> : null}
</div>
<DialogFooter>
<Button variant="ghost" onClick={onClose} disabled={saving}>
Cancel
</Button>
<Button onClick={submit} disabled={saving || !form.provider.trim()}>
{saving ? "Saving…" : mode === "new" ? "Create" : "Save"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function Field({
label,
hint,
children,
}: {
label: string
hint?: string
children: React.ReactNode
}) {
return (
<div className="grid gap-1.5">
<Label>{label}</Label>
{children}
{hint ? <p className="text-xs text-muted-foreground">{hint}</p> : null}
</div>
)
}