@openguardrails/moltguard
Version:
AI agent security plugin for OpenClaw: prompt injection detection, PII sanitization, and monitoring dashboard
130 lines • 4.71 kB
JavaScript
/**
* ConfigSync - Periodically pulls team-level configuration from Core.
*
* Only active when the agent's account is on the "business" plan.
* Pulls policies and gateway config every 5 minutes from
* GET /api/v1/business/config and applies them locally.
*/
// =============================================================================
// Constants
// =============================================================================
/** Poll interval in ms (5 minutes) */
const POLL_INTERVAL_MS = 5 * 60_000;
/** Timeout for Core API calls */
const API_TIMEOUT_MS = 5_000;
// =============================================================================
// ConfigSync Class
// =============================================================================
export class ConfigSync {
enabled = false;
options;
log;
credentials = null;
pollInterval = null;
/** Last fetched config (for change detection) */
lastConfigHash = "";
/** Last successfully fetched config */
currentConfig = null;
constructor(options, log) {
this.options = options;
this.log = log;
}
// ─── Lifecycle ───────────────────────────────────────────────────
/**
* Initialize config sync. Only enables if plan is "business".
*/
async initialize(plan) {
if (plan !== "business") {
this.log.debug?.(`ConfigSync: plan is "${plan}", not enabling`);
return;
}
this.enabled = true;
// Pull immediately on startup
await this.pull();
// Start periodic polling
this.startPolling();
this.log.info("ConfigSync: enabled, polling every 5 minutes");
}
/** Set Core credentials */
setCredentials(credentials) {
this.credentials = credentials;
}
/** Get current config (may be null if not yet fetched) */
getConfig() {
return this.currentConfig;
}
/** Whether sync is active */
isEnabled() {
return this.enabled;
}
/** Stop polling */
stop() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
this.enabled = false;
}
// ─── Polling ───────────────────────────────────────────────────
startPolling() {
if (this.pollInterval)
return;
this.pollInterval = setInterval(() => {
this.pull().catch((err) => {
this.log.debug?.(`ConfigSync: poll error: ${err}`);
});
}, POLL_INTERVAL_MS);
this.pollInterval.unref();
}
/** Pull config from Core */
async pull() {
if (!this.enabled || !this.credentials)
return null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
try {
const response = await fetch(`${this.options.coreUrl}/api/v1/business/config`, {
headers: {
Authorization: `Bearer ${this.credentials.apiKey}`,
},
signal: controller.signal,
});
if (!response.ok) {
this.log.debug?.(`ConfigSync: pull failed with ${response.status}`);
return null;
}
const json = (await response.json());
if (!json.success || !json.data) {
return null;
}
const config = json.data;
// Check if config changed (simple JSON hash)
const hash = JSON.stringify(config);
if (hash !== this.lastConfigHash) {
this.lastConfigHash = hash;
this.currentConfig = config;
// Notify callback
if (this.options.onUpdate) {
try {
this.options.onUpdate(config);
}
catch (err) {
this.log.error(`ConfigSync: onUpdate callback error: ${err}`);
}
}
this.log.debug?.(`ConfigSync: config updated (${config.policies.length} policies)`);
}
return config;
}
catch (err) {
if (err.name !== "AbortError") {
this.log.debug?.(`ConfigSync: pull error: ${err}`);
}
return null;
}
finally {
clearTimeout(timer);
}
}
}
//# sourceMappingURL=config-sync.js.map