Add operator Integrations page (integration registry console)

The operator surface for the integration registry: manage platform/pooled
external-API credentials across every scope and inspect cross-tenant usage
(metadata only — secrets are write-only). Talks to arcadia-llm-gateway's
/api/v1/integrations* endpoints via a gateway-pointed ArcadiaClient.

- gateway.ts: second ArcadiaClient at VITE_LLM_GATEWAY_URL, reusing the
  arcadia-app JWT (the gateway validates it via the shared Guardian secret;
  CORS already allows *.sky-ai.com + localhost — no proxy).
- lib/arcadia/integrations.ts: operator API client (any-scope create, scope
  filter, cross-tenant usage). Pure functions over an injected client —
  extraction-ready to share with arcadia-console.
- routes/integrations.tsx: scope filter + per-card scope badge, create
  platform/pooled credentials, credentials/usage, Test (surfaces the
  expiry/budget gate), enable toggle, delete.

The route/nav/capability wiring (routes.ts, app-shell, capabilities.ts) lands
with the in-flight capability framework, not here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-06-09 21:14:13 +10:00
parent a299900021
commit 06490865d3
3 changed files with 875 additions and 0 deletions

632
app/routes/integrations.tsx Normal file
View File

@@ -0,0 +1,632 @@
// 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-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>
)
}