import { getErrorMessage } from '@agentkitai/agentlens-core'; import React, { Suspense, useState, useCallback } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { useApi } from '../hooks/useApi'; import { PageSkeleton } from '../components/PageSkeleton'; import { getGuardrailRules, getGuardrailHistory, updateGuardrailRule, deleteGuardrailRule, type GuardrailRuleData, } from '../api/client'; const GuardrailActivity = React.lazy(() => import('./GuardrailActivity')); // ─── Helpers ──────────────────────────────────────────────────── function formatTimestamp(ts: string): string { try { return new Date(ts).toLocaleString(); } catch { return ts; } } const CONTENT_CONDITION_TYPES = new Set([ 'pii_detection', 'secrets_detection', 'content_regex', 'toxicity_detection', 'prompt_injection', ]); function isContentRule(rule: GuardrailRuleData): boolean { return CONTENT_CONDITION_TYPES.has(rule.conditionType); } function summarizeConditions(rule: GuardrailRuleData): string { const { conditionType, conditionConfig } = rule; switch (conditionType) { case 'error_rate_threshold': return `Error rate > ${conditionConfig.threshold ?? '?'}%`; case 'cost_limit': return `Cost > $${conditionConfig.maxCostUsd ?? '?'} (${conditionConfig.scope ?? 'daily'})`; case 'health_score_threshold': return `Health < ${conditionConfig.minScore ?? '?'}`; case 'custom_metric': return `${conditionConfig.metricKey ?? '?'} ${conditionConfig.operator ?? '?'} ${conditionConfig.value ?? '?'}`; case 'pii_detection': return conditionConfig.minConfidence != null ? `PII Detection (min conf ${conditionConfig.minConfidence})` : 'PII Detection'; case 'secrets_detection': return 'Secrets Detection'; case 'content_regex': return `Regex: ${conditionConfig.pattern ?? '?'}`; case 'toxicity_detection': return `Toxicity > ${conditionConfig.threshold ?? '?'}`; case 'prompt_injection': return `Prompt Injection (${conditionConfig.sensitivity ?? 'medium'})`; default: return conditionType; } } function summarizeActions(rule: GuardrailRuleData): string { const { actionType, actionConfig } = rule; switch (actionType) { case 'pause_agent': return '⏸ Pause Agent'; case 'notify_webhook': return `🔔 Webhook: ${actionConfig.url ?? '?'}`; case 'downgrade_model': return `⬇ Downgrade → ${actionConfig.targetModel ?? '?'}`; case 'agentgate_policy': return `🚪 Policy: ${actionConfig.policyId ?? '?'}`; case 'block': return '🚫 Block'; case 'redact': return '██ Redact'; case 'log_and_continue': return '📋 Log & Continue'; case 'alert': return '🔔 Alert'; default: return actionType; } } // ─── Main Page ────────────────────────────────────────────────── function GuardrailRules() { const rulesQuery = useApi(() => getGuardrailRules(), []); const historyQuery = useApi(() => getGuardrailHistory({ limit: 200 }), []); const rules = rulesQuery.data?.rules ?? []; const triggers = historyQuery.data?.triggers ?? []; // Build trigger count and last triggered maps const triggerCountMap = new Map(); const lastTriggeredMap = new Map(); for (const t of triggers) { triggerCountMap.set(t.ruleId, (triggerCountMap.get(t.ruleId) ?? 0) + 1); const prev = lastTriggeredMap.get(t.ruleId); if (!prev || t.triggeredAt > prev) { lastTriggeredMap.set(t.ruleId, t.triggeredAt); } } const [actionError, setActionError] = useState(null); const handleToggle = useCallback(async (rule: GuardrailRuleData) => { try { setActionError(null); await updateGuardrailRule(rule.id, { enabled: !rule.enabled }); rulesQuery.refetch(); } catch (err: unknown) { setActionError(`Failed to toggle rule: ${getErrorMessage(err) ?? String(err)}`); } }, [rulesQuery]); const handleDelete = useCallback(async (id: string) => { if (!confirm('Delete this guardrail rule? This action cannot be undone.')) return; try { setActionError(null); await deleteGuardrailRule(id); rulesQuery.refetch(); historyQuery.refetch(); } catch (err: unknown) { setActionError(`Failed to delete rule: ${getErrorMessage(err) ?? String(err)}`); } }, [rulesQuery, historyQuery]); return (

🛡️ Guardrails

+ Create Rule
{actionError &&

{actionError}

} {rulesQuery.loading &&

Loading...

} {rulesQuery.error &&

Error: {String(rulesQuery.error)}

} {!rulesQuery.loading && rules.length === 0 && (

No guardrail rules configured yet. Create one.

)} {rules.length > 0 && ( {rules.map((rule) => ( ))}
Name Agent Condition Action Enabled Last Triggered Triggers Actions
{rule.name} {rule.dryRun && [DRY RUN]} {isContentRule(rule) ? CONTENT : OPERATIONAL} {rule.direction && rule.direction !== 'both' && ( ↕{rule.direction} )} {typeof rule.priority === 'number' && rule.priority !== 0 && ( P{rule.priority} )} {rule.agentId ?? All} {summarizeConditions(rule)} {summarizeActions(rule)} {lastTriggeredMap.get(rule.id) ? formatTimestamp(lastTriggeredMap.get(rule.id)!) : '—'} {triggerCountMap.get(rule.id) ?? 0}
)}
); } // ─── Styles ───────────────────────────────────────────────────── const btnStyle: React.CSSProperties = { padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', textDecoration: 'none', display: 'inline-block', }; const thStyle: React.CSSProperties = { padding: '10px 12px', fontSize: '13px', color: '#64748b', fontWeight: 600 }; const tdStyle: React.CSSProperties = { padding: '10px 12px', fontSize: '13px' }; const toggleStyle: React.CSSProperties = { padding: '2px 10px', color: 'white', border: 'none', borderRadius: '12px', cursor: 'pointer', fontSize: '11px', fontWeight: 600, }; const deleteBtnStyle: React.CSSProperties = { padding: '4px 8px', background: 'transparent', border: '1px solid #fca5a5', borderRadius: '4px', cursor: 'pointer', fontSize: '13px', }; type GuardrailTab = 'rules' | 'activity'; function GuardrailList() { const [searchParams, setSearchParams] = useSearchParams(); const activeTab = (searchParams.get('tab') as GuardrailTab) || 'rules'; return (
{([ { key: 'rules' as const, label: 'Rules' }, { key: 'activity' as const, label: 'Activity' }, ]).map(({ key, label }) => ( ))}
{activeTab === 'rules' && } {activeTab === 'activity' && }>}
); } export default GuardrailList; export { GuardrailList };