Files
OmniRoute/src/lib/db/pluginMetrics.ts
Diego Rodrigues de Sa e Souza 20c31493af feat(plugins): plugins framework + per-API-key disable-non-public-models (#3041)
Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution.

- **Plugins framework** (#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off).
- **API key option: disable non-published models** (#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed).

Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean.

Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
2026-06-01 15:43:55 -03:00

89 lines
2.9 KiB
TypeScript

/**
* Plugin Metrics DB module — per-plugin hook execution tracking.
*
* STATUS: The `plugin_metrics` table (migration 090) is a reserved aggregate table.
* `recordPluginMetric` is currently NOT called from any production code path — the
* per-execution row store is `plugin_analytics` (migration 091, read by the
* `plugin_executions` MCP tool). `plugin_metrics` is intended for future aggregate
* rollups (e.g. bumping per-(plugin, event) counters from `recordPluginExecution`),
* but that write path has not been wired yet. Do NOT remove the migration — it is
* harmless and reserves the schema for the planned rollup feature.
*
* @module db/pluginMetrics
*/
import { getDbInstance } from "./core";
export interface PluginMetricRow {
pluginName: string;
event: string;
calls: number;
errors: number;
totalDurationMs: number;
lastCalledAt: string | null;
}
function rowToMetric(row: Record<string, unknown>): PluginMetricRow {
return {
pluginName: row.plugin_name as string,
event: row.event as string,
calls: row.calls as number,
errors: row.errors as number,
totalDurationMs: row.total_duration_ms as number,
lastCalledAt: row.last_called_at as string | null,
};
}
/**
* Record a hook execution metric. Uses UPSERT to increment counters.
*/
export function recordPluginMetric(
pluginName: string,
event: string,
durationMs: number,
isError: boolean
): void {
try {
const db = getDbInstance();
const now = new Date().toISOString();
db.prepare(
`INSERT INTO plugin_metrics (plugin_name, event, calls, errors, total_duration_ms, last_called_at)
VALUES (?, ?, 1, ?, ?, ?)
ON CONFLICT(plugin_name, event) DO UPDATE SET
calls = calls + 1,
errors = errors + excluded.errors,
total_duration_ms = total_duration_ms + excluded.total_duration_ms,
last_called_at = excluded.last_called_at`
).run(pluginName, event, isError ? 1 : 0, durationMs, now);
} catch {
// Best-effort: DB hiccup should never break hook execution
}
}
/**
* Get plugin metrics, optionally filtered by plugin name.
*/
export function getPluginMetrics(pluginName?: string): PluginMetricRow[] {
try {
const db = getDbInstance();
const rows = pluginName
? db.prepare("SELECT * FROM plugin_metrics WHERE plugin_name = ? ORDER BY event").all(pluginName)
: db.prepare("SELECT * FROM plugin_metrics ORDER BY plugin_name, event").all();
return (rows as Record<string, unknown>[]).map(rowToMetric);
} catch {
return [];
}
}
/**
* Clear plugin metrics, optionally filtered by plugin name.
*/
export function clearPluginMetrics(pluginName?: string): number {
const db = getDbInstance();
const result = pluginName
? db.prepare("DELETE FROM plugin_metrics WHERE plugin_name = ?").run(pluginName)
: db.prepare("DELETE FROM plugin_metrics").run();
return result.changes;
}