fix: credential loading

This commit is contained in:
Timothy
2026-02-24 18:40:39 -08:00
parent 72a31c2a65
commit 384a1f0560
3 changed files with 90 additions and 39 deletions
+19 -1
View File
@@ -179,7 +179,25 @@ async def handle_check_agent(request: web.Request) -> web.Response:
entry["validation_message"] = f"Health check error: {exc}"
required.append(entry)
return web.json_response({"required": required})
# Determine overall status
any_missing = any(not r["available"] for r in required)
any_invalid = any(r["valid"] is False for r in required)
all_valid = not any_missing and not any_invalid
response = {"required": required, "all_valid": all_valid}
if any_missing or any_invalid:
reasons = []
if any_missing:
names = [r["credential_name"] for r in required if not r["available"]]
reasons.append(f"missing: {', '.join(names)}")
if any_invalid:
names = [r["credential_name"] for r in required if r["valid"] is False]
reasons.append(f"invalid: {', '.join(names)}")
response["error"] = "; ".join(reasons)
return web.json_response(response, status=424)
return web.json_response(response)
except Exception as e:
logger.exception(f"Error checking agent credentials: {e}")
return web.json_response({"error": str(e)}, status=500)
+22 -5
View File
@@ -17,11 +17,19 @@ export interface AgentCredentialRequirement {
tools: string[];
node_types: string[];
available: boolean;
valid: boolean | null;
validation_message: string | null;
direct_api_key_supported: boolean;
aden_supported: boolean;
credential_key: string;
}
export interface CheckAgentResponse {
required: AgentCredentialRequirement[];
all_valid: boolean;
error?: string;
}
export const credentialsApi = {
list: () =>
api.get<{ credentials: CredentialInfo[] }>("/credentials"),
@@ -38,9 +46,18 @@ export const credentialsApi = {
delete: (credentialId: string) =>
api.delete<{ deleted: boolean }>(`/credentials/${credentialId}`),
checkAgent: (agentPath: string) =>
api.post<{ required: AgentCredentialRequirement[] }>(
"/credentials/check-agent",
{ agent_path: agentPath },
),
checkAgent: async (agentPath: string): Promise<CheckAgentResponse> => {
const url = `/api/credentials/check-agent`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_path: agentPath }),
});
const body = await response.json();
// 424 = validation failures, but body still has the required array
if (response.ok || response.status === 424) {
return body as CheckAgentResponse;
}
throw new Error(body.error || response.statusText);
},
};
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { KeyRound, Check, AlertCircle, X, Shield, Loader2, Trash2, ExternalLink } from "lucide-react";
import { credentialsApi, type AgentCredentialRequirement } from "@/api/credentials";
import { credentialsApi, type AgentCredentialRequirement, type CheckAgentResponse } from "@/api/credentials";
export interface Credential {
id: string;
@@ -38,6 +38,23 @@ interface CredentialRow {
required: boolean;
credentialKey: string; // key name within the credential (e.g., "api_key")
adenSupported: boolean; // whether this credential uses OAuth via Aden
valid: boolean | null; // true = health check passed, false = failed, null = not checked
validationMessage: string | null;
}
function requirementToRow(r: AgentCredentialRequirement): CredentialRow {
return {
id: r.credential_id,
name: r.credential_name,
description: r.description,
icon: "\uD83D\uDD11",
connected: r.available,
required: true,
credentialKey: r.credential_key || "api_key",
adenSupported: r.aden_supported,
valid: r.valid,
validationMessage: r.validation_message,
};
}
// Module-level cache: credential requirements are static per agent path.
@@ -80,35 +97,16 @@ export default function CredentialsModal({
// Check cache first — credential requirements are static per agent
const cached = credentialCache.get(agentPath);
if (cached) {
setRows(cached.map((r: AgentCredentialRequirement) => ({
id: r.credential_id,
name: r.credential_name,
description: r.description,
icon: "\uD83D\uDD11",
connected: r.available,
required: true,
credentialKey: r.credential_key || "api_key",
adenSupported: r.aden_supported,
})));
setRows(cached.map(requirementToRow));
setLoading(false);
return;
}
// Real agent — ask backend what credentials it actually needs
setLoading(true);
const { required } = await credentialsApi.checkAgent(agentPath);
credentialCache.set(agentPath, required);
const newRows: CredentialRow[] = required.map((r: AgentCredentialRequirement) => ({
id: r.credential_id,
name: r.credential_name,
description: r.description,
icon: "\uD83D\uDD11",
connected: r.available,
required: true,
credentialKey: r.credential_key || "api_key",
adenSupported: r.aden_supported,
}));
setRows(newRows);
const result: CheckAgentResponse = await credentialsApi.checkAgent(agentPath);
credentialCache.set(agentPath, result.required);
setRows(result.required.map(requirementToRow));
} else {
// No real path — no credentials to show
setRows([]);
@@ -120,6 +118,8 @@ export default function CredentialsModal({
...c,
credentialKey: "api_key",
adenSupported: false,
valid: null,
validationMessage: null,
})));
} else {
setRows([]);
@@ -188,7 +188,9 @@ export default function CredentialsModal({
const connectedCount = rows.filter(c => c.connected).length;
const requiredCount = rows.filter(c => c.required).length;
const requiredConnected = rows.filter(c => c.required && c.connected).length;
const allRequiredMet = requiredConnected === requiredCount;
const invalidCount = rows.filter(c => c.valid === false).length;
const missingCount = requiredCount - requiredConnected;
const allRequiredMet = requiredConnected === requiredCount && invalidCount === 0;
return (
<>
@@ -229,7 +231,9 @@ export default function CredentialsModal({
) : (
<>
<AlertCircle className="w-3.5 h-3.5" />
{requiredCount - requiredConnected} required credential{requiredCount - requiredConnected !== 1 ? "s" : ""} missing
{missingCount > 0 && `${missingCount} missing`}
{missingCount > 0 && invalidCount > 0 && ", "}
{invalidCount > 0 && `${invalidCount} invalid`}
</>
)}
</div>
@@ -256,9 +260,11 @@ export default function CredentialsModal({
<div key={row.id}>
<div
className={`flex items-center gap-3 px-3 py-3 rounded-lg border transition-colors ${
row.connected
row.connected && row.valid !== false
? "border-primary/20 bg-primary/[0.03]"
: "border-border/60 bg-muted/20"
: row.valid === false
? "border-destructive/30 bg-destructive/[0.03]"
: "border-border/60 bg-muted/20"
}`}
>
<span className="text-lg flex-shrink-0">{row.icon}</span>
@@ -276,13 +282,23 @@ export default function CredentialsModal({
)}
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">{row.description}</p>
{row.valid === false && row.validationMessage && (
<p className="text-[11px] text-destructive mt-0.5">{row.validationMessage}</p>
)}
</div>
{row.connected ? (
<div className="flex items-center gap-1 flex-shrink-0">
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-primary/10 text-primary">
<Check className="w-3 h-3" />
Connected
</span>
{row.valid === false ? (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-destructive/10 text-destructive" title={row.validationMessage || "Invalid"}>
<AlertCircle className="w-3 h-3" />
Invalid
</span>
) : (
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-primary/10 text-primary">
<Check className="w-3 h-3" />
Connected
</span>
)}
<button
onClick={() => handleDisconnect(row)}
disabled={saving}
@@ -360,7 +376,7 @@ export default function CredentialsModal({
: "bg-muted text-muted-foreground cursor-not-allowed"
}`}
>
{allRequiredMet ? "Done" : "Connect required credentials to continue"}
{allRequiredMet ? "Done" : missingCount > 0 ? "Connect required credentials to continue" : "Fix invalid credentials to continue"}
</button>
</div>
)}