@openguardrails/moltguard
Version:
AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard
281 lines • 10.7 kB
JavaScript
/**
* DashboardClient - SDK for communicating with OpenGuardrails Dashboard
*
* Handles:
* - Agent registration & heartbeat
* - Detection requests (routed through dashboard → core)
* - Usage & results queries
*
* Works with both local embedded dashboard and remote standalone dashboard.
*/
import fs from "node:fs";
import path from "node:path";
import { openclawHome } from "../agent/env.js";
export class DashboardClient {
config;
debugFileLog(msg) {
try {
const logPath = path.join(openclawHome, "logs", "moltguard-debug.log");
fs.appendFileSync(logPath, `[${new Date().toISOString()}] [DashboardClient] ${msg}\n`);
}
catch { /* ignore */ }
}
constructor(config) {
this.config = {
dashboardUrl: config.dashboardUrl.replace(/\/$/, ""),
sessionToken: config.sessionToken,
agentId: config.agentId ?? "",
timeoutMs: config.timeoutMs ?? 30000,
};
}
get agentId() {
return this.config.agentId;
}
async request(path, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
try {
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.sessionToken}`,
...(options.headers || {}),
};
const res = await fetch(`${this.config.dashboardUrl}${path}`, {
...options,
headers,
signal: controller.signal,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Dashboard API ${res.status}: ${text}`);
}
return (await res.json());
}
finally {
clearTimeout(timeoutId);
}
}
// ─── Detection ──────────────────────────────────────────────────
/**
* Send messages for detection through the dashboard.
* Dashboard handles scanner config, policy evaluation, and routes to core.
*/
async detect(req) {
return this.request("/api/detect", {
method: "POST",
body: JSON.stringify({
...req,
agentId: req.agentId || this.config.agentId || undefined,
}),
});
}
// ─── Agent Management ───────────────────────────────────────────
/** Register this agent with the dashboard (upserts by name) */
async registerAgent(req) {
// Check if agent with same name already exists
try {
const list = await this.request("/api/agents");
if (list.success && list.data) {
const existing = list.data.find((a) => a.name === req.name);
if (existing) {
this.config.agentId = existing.id;
// Update status and metadata
await this.request(`/api/agents/${existing.id}`, {
method: "PUT",
body: JSON.stringify({
status: "active",
...(req.provider && { provider: req.provider }),
...(req.metadata && { metadata: req.metadata }),
}),
}).catch(() => { });
return { success: true, data: { id: existing.id } };
}
}
}
catch {
// Fall through to create
}
const result = await this.request("/api/agents", {
method: "POST",
body: JSON.stringify(req),
});
if (result.success && result.data?.id) {
this.config.agentId = result.data.id;
}
return result;
}
/** Send heartbeat to indicate this agent is alive */
async heartbeat() {
if (!this.config.agentId)
return;
await this.request(`/api/agents/${this.config.agentId}/heartbeat`, {
method: "POST",
});
}
/** Upload full agent profile (workspace files, skills, cron jobs, etc.) */
async updateProfile(profile) {
if (!this.config.agentId)
return;
await this.request(`/api/agents/${this.config.agentId}`, {
method: "PUT",
body: JSON.stringify({ metadata: profile }),
});
}
/** Start periodic heartbeat */
startHeartbeat(intervalMs = 60_000) {
// Send first heartbeat immediately so agent shows as active right away
this.heartbeat().catch(() => { });
const timer = setInterval(() => {
this.heartbeat().catch(() => { });
}, intervalMs);
timer.unref();
return timer;
}
// ─── Tool Call Observations ─────────────────────────────────────
/** Report a tool call observation to the dashboard */
async reportToolCall(data) {
await this.request("/api/observations", {
method: "POST",
body: JSON.stringify(data),
});
}
/** Report a detection result to the dashboard */
async reportDetection(data) {
await this.request("/api/detections", {
method: "POST",
body: JSON.stringify(data),
});
}
/** Get observed permissions for an agent */
async getPermissions(agentId) {
const id = agentId || this.config.agentId;
if (!id)
return [];
const result = await this.request(`/api/observations/agents/${id}/permissions`);
return result.data ?? [];
}
// ─── Agentic Hours ──────────────────────────────────────────────
/** Report agentic hours data to the dashboard */
async reportAgenticHours(data) {
await this.request("/api/agentic-hours", {
method: "POST",
body: JSON.stringify(data),
});
}
// ─── Agentic Hours Accumulator ────────────────────────────────
hoursAccum = {
toolCallDurationMs: 0,
llmDurationMs: 0,
totalDurationMs: 0,
toolCallCount: 0,
llmCallCount: 0,
sessionCount: 0,
blockCount: 0,
riskEventCount: 0,
};
hoursFlushTimer = null;
/** Record a tool call duration for agentic hours */
recordToolCallDuration(durationMs, blocked = false) {
this.hoursAccum.toolCallDurationMs += durationMs;
this.hoursAccum.totalDurationMs += durationMs;
this.hoursAccum.toolCallCount += 1;
if (blocked)
this.hoursAccum.blockCount += 1;
this.ensureHoursFlush();
}
/** Record an LLM call duration for agentic hours */
recordLlmDuration(durationMs) {
this.hoursAccum.llmDurationMs += durationMs;
this.hoursAccum.totalDurationMs += durationMs;
this.hoursAccum.llmCallCount += 1;
this.ensureHoursFlush();
}
/** Record a session start */
recordSessionStart() {
this.hoursAccum.sessionCount += 1;
this.ensureHoursFlush();
}
/** Record a risk event */
recordRiskEvent() {
this.hoursAccum.riskEventCount += 1;
this.ensureHoursFlush();
}
ensureHoursFlush() {
if (this.hoursFlushTimer)
return;
this.hoursFlushTimer = setTimeout(() => {
this.flushAgenticHours();
this.hoursFlushTimer = null;
}, 60_000);
this.hoursFlushTimer.unref();
}
async flushAgenticHours() {
this.debugFileLog(`flushAgenticHours: agentId=${this.config.agentId} accum=${JSON.stringify(this.hoursAccum)}`);
if (!this.config.agentId) {
this.debugFileLog("flushAgenticHours: no agentId, skipping");
return;
}
const accum = { ...this.hoursAccum };
// Reset
this.hoursAccum = {
toolCallDurationMs: 0,
llmDurationMs: 0,
totalDurationMs: 0,
toolCallCount: 0,
llmCallCount: 0,
sessionCount: 0,
blockCount: 0,
riskEventCount: 0,
};
// Only flush if there's data
const hasData = accum.totalDurationMs > 0 ||
accum.toolCallCount > 0 ||
accum.llmCallCount > 0 ||
accum.sessionCount > 0;
if (!hasData)
return;
try {
this.debugFileLog(`flushAgenticHours: POSTing to dashboard: ${JSON.stringify(accum)}`);
await this.reportAgenticHours({
agentId: this.config.agentId,
...accum,
});
this.debugFileLog(`flushAgenticHours: POST success`);
}
catch (err) {
this.debugFileLog(`flushAgenticHours: POST FAILED: ${err}`);
// Re-add on failure
this.hoursAccum.toolCallDurationMs += accum.toolCallDurationMs;
this.hoursAccum.llmDurationMs += accum.llmDurationMs;
this.hoursAccum.totalDurationMs += accum.totalDurationMs;
this.hoursAccum.toolCallCount += accum.toolCallCount;
this.hoursAccum.llmCallCount += accum.llmCallCount;
this.hoursAccum.sessionCount += accum.sessionCount;
this.hoursAccum.blockCount += accum.blockCount;
this.hoursAccum.riskEventCount += accum.riskEventCount;
}
}
/** Flush pending agentic hours and clean up timers */
async stop() {
if (this.hoursFlushTimer) {
clearTimeout(this.hoursFlushTimer);
this.hoursFlushTimer = null;
}
await this.flushAgenticHours();
}
// ─── Health ───────────────────────────────────────────────────────
/** Check if dashboard is reachable */
async checkHealth() {
try {
const res = await fetch(`${this.config.dashboardUrl}/health`);
const json = (await res.json());
return json.status === "ok";
}
catch {
return false;
}
}
}
// Keep PlatformClient as alias
export { DashboardClient as PlatformClient };
//# sourceMappingURL=index.js.map