UNPKG

@openguardrails/moltguard

Version:

AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard

162 lines 6.44 kB
/** * Agent Runner - Multi-backend analysis * * Supports two detection backends: * 1. Dashboard (preferred) - Routes through local/remote dashboard → core * 2. OpenGuardrails API (fallback) - Direct API call * * Content is always sanitized locally before being sent to any API. */ import { DEFAULT_CORE_URL, loadCoreCredentials, registerWithCore, } from "./config.js"; import { sanitizeContent } from "./sanitizer.js"; async function runViaDashboard(sanitizedContent, config, log) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs); try { const headers = { "Content-Type": "application/json", }; if (config.dashboardSessionToken) { headers["Authorization"] = `Bearer ${config.dashboardSessionToken}`; } const response = await fetch(`${config.dashboardUrl}/api/detect`, { method: "POST", headers, body: JSON.stringify({ messages: [{ role: "user", content: sanitizedContent }], }), signal: controller.signal, }); if (!response.ok) { const text = await response.text(); throw new Error(`Dashboard API error: ${response.status} ${text}`); } const result = (await response.json()); if (!result.success || !result.data) { throw new Error(`Dashboard error: ${result.error ?? "unknown"}`); } const data = result.data; const findings = data.findings.map((f) => ({ suspiciousContent: f.name, reason: f.description, confidence: data.sensitivity_score, })); return { isInjection: !data.safe, confidence: data.sensitivity_score, reason: data.safe ? "No issues detected" : `Detected: ${data.categories.join(", ")}`, findings, chunksAnalyzed: 1, }; } finally { clearTimeout(timeoutId); } } // ============================================================================= // OpenGuardrails API Detection (Fallback) // ============================================================================= async function ensureApiKey(configKey, autoRegister, coreUrl, log) { if (configKey) return configKey; const savedKey = loadCoreCredentials()?.apiKey; if (savedKey) return savedKey; if (!autoRegister) { throw new Error("No API key configured and autoRegister is disabled. " + "Please set apiKey in your OpenGuardrails plugin config or enable autoRegister."); } log.info("No API key found — registering with OpenGuardrails..."); try { const result = await registerWithCore("openclaw-agent", "OpenClaw AI Agent", coreUrl); log.info("Registered with OpenGuardrails. API key saved to ~/.openclaw/credentials/moltguard/credentials.json"); return result.credentials.apiKey; } catch (error) { throw new Error(`Failed to auto-register API key: ${error instanceof Error ? error.message : String(error)}. ` + "Please check your network connection or set apiKey manually in config."); } } export function mapApiResponseToVerdict(apiResponse) { const verdict = apiResponse.verdict; const findings = (verdict.findings ?? []).map((f) => ({ suspiciousContent: f.suspiciousContent, reason: f.reason, confidence: f.confidence, })); return { isInjection: verdict.isInjection, confidence: verdict.confidence, reason: verdict.reason, findings, chunksAnalyzed: 1, }; } async function runViaApi(sanitizedContent, config, log) { const baseUrl = config.coreUrl || DEFAULT_CORE_URL; const apiKey = await ensureApiKey(config.apiKey, config.autoRegister, baseUrl, log); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs); try { const response = await fetch(`${baseUrl}/api/check/tool-call`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ content: sanitizedContent, async: false }), signal: controller.signal, }); if (!response.ok) { throw new Error(`OpenGuardrails API error: ${response.status} ${response.statusText}`); } const apiResponse = (await response.json()); if (!apiResponse.ok) { throw new Error(`OpenGuardrails API returned error: ${apiResponse.error ?? "unknown"}`); } return mapApiResponseToVerdict(apiResponse); } finally { clearTimeout(timeoutId); } } // ============================================================================= // Main Analysis Function // ============================================================================= export async function runGuardAgent(target, config, log) { const startTime = Date.now(); log.info(`Analyzing content: ${target.content.length} chars`); // Always sanitize locally first const { sanitized, redactions, totalRedactions } = sanitizeContent(target.content); if (totalRedactions > 0) { log.info(`Sanitized ${totalRedactions} sensitive items: ${Object.entries(redactions).map(([k, v]) => `${v} ${k}`).join(", ")}`); } try { let verdict; // Route to dashboard if configured, otherwise fall back to API if (config.dashboardUrl) { log.info("Using dashboard for detection"); verdict = await runViaDashboard(sanitized, config, log); } else { verdict = await runViaApi(sanitized, config, log); } const durationMs = Date.now() - startTime; log.info(`Analysis complete in ${durationMs}ms: ${verdict.isInjection ? "INJECTION DETECTED" : "SAFE"}`); return verdict; } catch (error) { if (error.name === "AbortError") { log.warn("Analysis timed out"); return { isInjection: false, confidence: 0, reason: "Timeout", findings: [], chunksAnalyzed: 0, }; } throw error; } } //# sourceMappingURL=runner.js.map