credential updated

This commit is contained in:
bryan
2026-02-24 18:33:09 -08:00
parent 72a31c2a65
commit 4bd1b1b9e6
3 changed files with 126 additions and 7 deletions
+55 -5
View File
@@ -100,7 +100,11 @@ async def handle_check_agent(request: web.Request) -> web.Response:
import os
from framework.credentials.setup import CredentialSetupSession
from framework.credentials.storage import CompositeStorage, EncryptedFileStorage, EnvVarStorage
from framework.credentials.storage import (
CompositeStorage,
EncryptedFileStorage,
EnvVarStorage,
)
from framework.credentials.validation import _presync_aden_tokens, ensure_credential_key_env
# Load env vars from shell config (same as runtime startup)
@@ -116,8 +120,7 @@ async def handle_check_agent(request: web.Request) -> web.Response:
_presync_aden_tokens(CREDENTIAL_SPECS)
env_mapping = {
(spec.credential_id or name): spec.env_var
for name, spec in CREDENTIAL_SPECS.items()
(spec.credential_id or name): spec.env_var for name, spec in CREDENTIAL_SPECS.items()
}
env_storage = EnvVarStorage(env_mapping=env_mapping)
if os.environ.get("HIVE_CREDENTIAL_KEY"):
@@ -134,6 +137,7 @@ async def handle_check_agent(request: web.Request) -> web.Response:
if verify:
try:
from aden_tools.credentials import check_credential_health
check_health = check_credential_health
except ImportError:
pass
@@ -179,16 +183,62 @@ 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})
return web.json_response({
"required": required,
"has_aden_key": bool(os.environ.get("ADEN_API_KEY")),
})
except Exception as e:
logger.exception(f"Error checking agent credentials: {e}")
return web.json_response({"error": str(e)}, status=500)
async def handle_save_aden_key(request: web.Request) -> web.Response:
"""POST /api/credentials/aden-key — save the user's ADEN_API_KEY.
Sets the key in the current process environment and persists it to shell
config so future terminals pick it up. Then triggers an Aden token sync
so OAuth credentials resolve immediately.
Body: {"key": "..."}
"""
import os
body = await request.json()
key = body.get("key", "").strip()
if not key:
return web.json_response({"error": "key is required"}, status=400)
os.environ["ADEN_API_KEY"] = key
# Persist to shell config (best-effort, same pattern as TUI setup)
try:
from aden_tools.credentials.shell_config import add_env_var_to_shell_config
add_env_var_to_shell_config(
"ADEN_API_KEY",
key,
comment="Aden Platform API key",
)
except Exception as exc:
logger.warning("Could not persist ADEN_API_KEY to shell config: %s", exc)
# Immediately sync OAuth tokens from Aden
try:
from aden_tools.credentials import CREDENTIAL_SPECS
from framework.credentials.validation import _presync_aden_tokens
_presync_aden_tokens(CREDENTIAL_SPECS)
except Exception as exc:
logger.warning("Aden token sync after key save failed: %s", exc)
return web.json_response({"saved": True}, status=201)
def register_routes(app: web.Application) -> None:
"""Register credential routes on the application."""
# check-agent must be registered BEFORE the {credential_id} wildcard
# check-agent and aden-key must be registered BEFORE the {credential_id} wildcard
app.router.add_post("/api/credentials/check-agent", handle_check_agent)
app.router.add_post("/api/credentials/aden-key", handle_save_aden_key)
app.router.add_get("/api/credentials", handle_list_credentials)
app.router.add_post("/api/credentials", handle_save_credential)
app.router.add_get("/api/credentials/{credential_id}", handle_get_credential)
+4 -1
View File
@@ -39,8 +39,11 @@ export const credentialsApi = {
api.delete<{ deleted: boolean }>(`/credentials/${credentialId}`),
checkAgent: (agentPath: string) =>
api.post<{ required: AgentCredentialRequirement[] }>(
api.post<{ required: AgentCredentialRequirement[]; has_aden_key: boolean }>(
"/credentials/check-agent",
{ agent_path: agentPath },
),
saveAdenKey: (key: string) =>
api.post<{ saved: boolean }>("/credentials/aden-key", { key }),
};
@@ -72,6 +72,9 @@ export default function CredentialsModal({
const [editingId, setEditingId] = useState<string | null>(null);
const [inputValue, setInputValue] = useState("");
const [saving, setSaving] = useState(false);
const [hasAdenKey, setHasAdenKey] = useState(true); // assume true until backend says otherwise
const [adenKeyInput, setAdenKeyInput] = useState("");
const [savingAdenKey, setSavingAdenKey] = useState(false);
const fetchStatus = useCallback(async () => {
setError(null);
@@ -96,7 +99,8 @@ export default function CredentialsModal({
// Real agent — ask backend what credentials it actually needs
setLoading(true);
const { required } = await credentialsApi.checkAgent(agentPath);
const { required, has_aden_key } = await credentialsApi.checkAgent(agentPath);
setHasAdenKey(has_aden_key);
credentialCache.set(agentPath, required);
const newRows: CredentialRow[] = required.map((r: AgentCredentialRequirement) => ({
id: r.credential_id,
@@ -135,9 +139,26 @@ export default function CredentialsModal({
fetchStatus();
setEditingId(null);
setInputValue("");
setAdenKeyInput("");
}
}, [open, fetchStatus]);
const handleSaveAdenKey = async () => {
if (!adenKeyInput.trim()) return;
setSavingAdenKey(true);
try {
await credentialsApi.saveAdenKey(adenKeyInput.trim());
setAdenKeyInput("");
if (agentPath) credentialCache.delete(agentPath);
onCredentialChange?.();
await fetchStatus();
} catch {
setError("Failed to save Aden API Key");
} finally {
setSavingAdenKey(false);
}
};
const handleConnect = async (row: CredentialRow) => {
if (row.adenSupported) {
// OAuth credential — redirect to Aden platform
@@ -189,6 +210,7 @@ export default function CredentialsModal({
const requiredCount = rows.filter(c => c.required).length;
const requiredConnected = rows.filter(c => c.required && c.connected).length;
const allRequiredMet = requiredConnected === requiredCount;
const needsAdenKeyInput = !hasAdenKey && rows.some(r => r.adenSupported);
return (
<>
@@ -249,6 +271,50 @@ export default function CredentialsModal({
</div>
)}
{/* Aden API Key section */}
{!loading && needsAdenKeyInput && (
<div className="mx-5 mt-4 px-3 py-3 rounded-lg border border-amber-500/30 bg-amber-500/5">
<div className="flex items-center gap-2 mb-1">
<KeyRound className="w-3.5 h-3.5 text-amber-600" />
<span className="text-sm font-medium text-foreground">Aden API Key</span>
<span className="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded text-destructive/70 bg-destructive/10">
Required
</span>
</div>
<p className="text-[11px] text-muted-foreground mb-2">
Required to connect OAuth integrations below.{" "}
<a
href="https://hive.adenhq.com/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center gap-0.5"
>
Get your key at hive.adenhq.com
<ExternalLink className="w-2.5 h-2.5" />
</a>
</p>
<div className="flex gap-2">
<input
type="password"
value={adenKeyInput}
onChange={(e) => setAdenKeyInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveAdenKey();
}}
placeholder="Paste your ADEN_API_KEY..."
className="flex-1 px-3 py-1.5 rounded-md border border-border bg-background text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/40"
/>
<button
onClick={handleSaveAdenKey}
disabled={savingAdenKey || !adenKeyInput.trim()}
className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{savingAdenKey ? <Loader2 className="w-3 h-3 animate-spin" /> : "Save"}
</button>
</div>
</div>
)}
{/* Credential list */}
{!loading && (
<div className="p-5 space-y-2">