fix: credential loading
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user