import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; import V2PageHead from '../components/V2PageHead'; import V2AgentRuntimeGrants, { Runtime } from '../components/V2AgentRuntimeGrants'; import { AgentControlLoader } from '../components/V2AgentControlSection'; import { AgentConnection } from '../utils/agentConnection'; import { useAgentSettings } from './useAgentSettings'; import V2AgentIdentitySection from './V2AgentIdentitySection'; import V2AgentWakePods from './V2AgentWakePods'; import V2AgentPolicy from './V2AgentPolicy'; import V2AgentSkills from '../components/V2AgentSkills'; import './v2-agent-settings.css'; interface RuntimeKey { _id: string; label: string; machineId?: string | null; machineName?: string; createdAt?: string; lastUsedAt?: string | null; revokedAt?: string | null; status: 'active' | 'revoked'; } const dateLabel = (value: string | null | undefined, t: (key: string) => string): string => { if (!value) return t('agentSettings.never'); const date = new Date(value); return Number.isNaN(date.getTime()) ? t('agentSettings.unknown') : date.toLocaleString(); }; const errorLabel = (error: unknown, fallback: string): string => ( (error as { response?: { data?: { error?: string } } })?.response?.data?.error || (error instanceof Error && error.message) || fallback ); const V2AgentSettingsPage: React.FC = () => { const { t } = useTranslation(); const { agentName = '', instanceId = '' } = useParams<{ agentName: string; instanceId: string }>(); const { access, installation, podId, ownKeyCapability, runtimeControls, runtimeObservation, runtimeInventory, processConnected, runtimeEditable, runtimeManaged, loading, error: loadError, refresh, update, updatePod, updateRuntime } = useAgentSettings(agentName, instanceId); const [removedLast, setRemovedLast] = useState(false); const [keys, setKeys] = useState([]); const [keysReadyFor, setKeysReadyFor] = useState(null); const [keysForbidden, setKeysForbidden] = useState(false); const [error, setError] = useState(null); const [pendingAction, setPendingAction] = useState<'replace' | string | null>(null); const [busy, setBusy] = useState(false); const [oneTimeKey, setOneTimeKey] = useState(null); const [connectionState, setConnectionState] = useState(null); const [connectionRefreshNonce, setConnectionRefreshNonce] = useState(0); const identityKey = `${agentName}:${instanceId}:${podId || ''}`; const currentIdentity = useRef(identityKey); currentIdentity.current = identityKey; const disconnected = connectionState === 'revoked' || connectionState === 'stale'; const onConnectionState = useCallback((state: AgentConnection['state'] | null) => { setConnectionState(state); if (state === 'revoked' || state === 'stale') setPendingAction(null); }, []); const keyUrl = useCallback((podId: string) => ( `/api/registry/pods/${encodeURIComponent(podId)}/agents/${encodeURIComponent(agentName)}/runtime-tokens` ), [agentName]); const fetchKeys = useCallback(async () => { const response = await axios.get('/api/credentials', { params: { agentName, instanceId }, }); return response.data || []; }, [agentName, instanceId]); useEffect(() => { let active = true; setKeys([]); setKeysReadyFor(null); setKeysForbidden(false); setError(null); setOneTimeKey(null); setPendingAction(null); setBusy(false); setConnectionState(null); if (access && podId) { void fetchKeys().then((rows) => { if (active) { setKeys(rows); setKeysReadyFor(identityKey); } }).catch((requestError) => { if (!active) return; if ((requestError as { response?: { status?: number } })?.response?.status === 403) { setKeysForbidden(true); setKeysReadyFor(identityKey); } else { setError(errorLabel(requestError, t('agentSettings.unavailable'))); } }); } return () => { active = false; }; }, [access, podId, fetchKeys, identityKey, t]); // A key used or revoked from outside this page only shows up when the list // is re-read. Poll while the tab is visible, and re-read on focus. useEffect(() => { if (!access || !podId) return undefined; let active = true; let pending = false; const refreshKeys = async () => { if (pending || document.hidden) return; pending = true; try { const rows = await fetchKeys(); if (active) setKeys(rows); } catch { // Keep the last list; the next tick or focus retries. } finally { pending = false; } }; const timer = window.setInterval(refreshKeys, 15_000); window.addEventListener('focus', refreshKeys); document.addEventListener('visibilitychange', refreshKeys); return () => { active = false; window.clearInterval(timer); window.removeEventListener('focus', refreshKeys); document.removeEventListener('visibilitychange', refreshKeys); }; }, [access, podId, fetchKeys]); const replaceKey = async () => { if (!access || !podId || pendingAction !== 'replace' || busy || installation?.runtime?.runtimeType === 'wrapper') return; const requestIdentity = identityKey; setBusy(true); setError(null); try { const response = await axios.post<{ token?: string }>(keyUrl(podId), { instanceId, label: 'Agent Settings replacement', force: true, }); if (currentIdentity.current !== requestIdentity) return; if (!response.data.token) throw new Error(t('agentSettings.missingReplacement')); setOneTimeKey(response.data.token); setPendingAction(null); const rows = await fetchKeys(); if (currentIdentity.current === requestIdentity) { setKeys(rows); setConnectionRefreshNonce((nonce) => nonce + 1); } } catch (requestError) { if (currentIdentity.current === requestIdentity) setError(errorLabel(requestError, t('agentSettings.updateError'))); } finally { if (currentIdentity.current === requestIdentity) setBusy(false); } }; const revokeKey = async (tokenId: string) => { if (!access || pendingAction !== tokenId || busy) return; const requestIdentity = identityKey; setBusy(true); setError(null); try { await axios.delete(`/api/credentials/${encodeURIComponent(tokenId)}`, { params: { agentName, instanceId }, }); if (currentIdentity.current !== requestIdentity) return; setPendingAction(null); setOneTimeKey(null); const rows = await fetchKeys(); if (currentIdentity.current === requestIdentity) { setKeys(rows); setConnectionRefreshNonce((nonce) => nonce + 1); } } catch (requestError) { if (currentIdentity.current === requestIdentity) setError(errorLabel(requestError, t('agentSettings.updateError'))); } finally { if (currentIdentity.current === requestIdentity) setBusy(false); } }; const onRemoved = (_podId: string, wasLastPod: boolean) => { if (!access) return; setKeys([]); setPendingAction(null); setOneTimeKey(null); if (wasLastPod) setRemovedLast(true); else void refresh().catch((requestError) => setError(errorLabel(requestError, t('agentSettings.unavailable')))); }; if (loading) return
{t('agentSettings.loading')}
; if (removedLast) return
{t('agentSettings.noPod')}
; if (!access || !podId) return
{error || errorLabel(loadError, t('agentSettings.unavailable'))}
; if (keysReadyFor !== identityKey) return
{error || t('agentSettings.loading')}
; const environment = installation?.config?.environment; const controls = environment && typeof environment === 'object' && !Array.isArray(environment) ? environment as Record : {}; const runtime: Runtime = { runtimeType: installation?.runtime?.runtimeType, ...(installation?.runtime?.host === 'cloud' || installation?.runtime?.host === 'byo' ? { host: installation.runtime.host } : {}), adapter: installation?.runtime?.adapter, model: installation?.runtime?.model, effort: installation?.runtime?.effort, ...(typeof controls.adapter === 'string' ? { adapter: controls.adapter } : {}), ...(typeof controls.model === 'string' ? { model: controls.model } : {}), ...(typeof controls.effort === 'string' ? { effort: controls.effort } : {}), ...runtimeControls, observation: runtimeObservation, inventory: runtimeInventory, processConnected, ownKeyCapability, ...(runtimeManaged !== undefined ? { managed: runtimeManaged } : {}), }; const activeKeys = keys.filter((key) => key.status === 'active'); const revokedKeys = keys.filter((key) => key.status === 'revoked'); const keyLabel = (key: RuntimeKey): string => { const defaultLabel = !key.label || key.label === 'Runtime token' || key.label === 'Agent Settings replacement'; const kind = runtime.runtimeType === 'wrapper' ? t('agentSettings.cliKey') : t('agentSettings.runtimeKey'); const detail = key.machineId ? key.machineName : (defaultLabel ? null : key.label); return detail ? `${kind} ยท ${detail}` : kind; }; const renderKey = (key: RuntimeKey) => (
  • {keyLabel(key)} {key.status === 'revoked' ? t('agentSettings.revoked') : t('agentSettings.active')}
    {t('agentSettings.created', { date: dateLabel(key.createdAt, t) })} {t('agentSettings.lastUsed', { date: dateLabel(key.lastUsedAt, t) })} {key.status === 'revoked' && {t('agentSettings.revokedAt', { date: dateLabel(key.revokedAt, t) })}}
    {key.status === 'active' && pendingAction === key._id && (

    {t('agentSettings.revokeWarning')}

    )} {key.status === 'active' && pendingAction !== key._id && ( )}
  • ); return (
    {t('agentSettings.yourTeam')}} />

    {t('agentSettings.key')}

    {keysForbidden ? null : installation?.runtime?.runtimeType === 'wrapper' ? ( {t('agentSettings.wrapperReconnect')} ) : !disconnected && ( )}
    {error &&

    {error}

    } {pendingAction === 'replace' && !keysForbidden && (

    {t('agentByo.rekey.warning', { name: access.displayName || agentName })}

    )} {oneTimeKey && (
    {t('agentSettings.copyNow')} {oneTimeKey}
    )} {keysForbidden ?

    {t('agentSettings.keyUnavailable')}

    : ( <> {activeKeys.length ?
      {activeKeys.map(renderKey)}
    :

    {t('agentSettings.noActiveKeys')}

    } {revokedKeys.length > 0 &&
    {t('agentSettings.revokedCount', { count: revokedKeys.length })}
      {revokedKeys.map(renderKey)}
    } )}
    {installation && } { await updateRuntime(changes); } : undefined} />
    pod.id === podId)?.name || podId} agentName={agentName} instanceId={instanceId} />
    ); }; export default V2AgentSettingsPage;