Files
OmniRoute/src/lib/plugins/doctor.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

139 lines
4.3 KiB
TypeScript

/**
* Plugin doctor — diagnostic tool for plugin health checks.
*
* Runs 5 checks: directory exists, manifest valid, entry point exists,
* can spawn, DB status matches filesystem.
*
* @module plugins/doctor
*/
import { stat } from "fs/promises";
import { join } from "path";
import { safeValidateManifest } from "./manifest";
import { getPluginByName } from "../db/plugins";
import { readFile } from "fs/promises";
import { logger } from "../../../open-sse/utils/logger.ts";
const log = logger("PLUGIN_DOCTOR");
export interface DoctorCheck {
name: string;
status: "pass" | "fail" | "warn";
message?: string;
}
export interface DoctorResult {
pluginName: string;
checks: DoctorCheck[];
overall: "healthy" | "degraded" | "unhealthy";
}
/**
* Run diagnostic checks on a plugin.
*/
export async function runPluginDoctor(pluginDir: string, pluginName: string): Promise<DoctorResult> {
const checks: DoctorCheck[] = [];
// Check 1: directory_exists
try {
const dirStat = await stat(pluginDir);
checks.push({
name: "directory_exists",
status: dirStat.isDirectory() ? "pass" : "fail",
message: dirStat.isDirectory() ? undefined : "Path is not a directory",
});
} catch {
checks.push({ name: "directory_exists", status: "fail", message: "Directory not found" });
}
// Check 2: manifest_valid
const manifestPath = join(pluginDir, "plugin.json");
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
checks.push({
name: "manifest_valid",
status: result.success ? "pass" : "fail",
message: result.success ? undefined : (result as { success: false; errors: string[] }).errors.join("; "),
});
} catch (err: unknown) {
checks.push({
name: "manifest_valid",
status: "fail",
message: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`,
});
}
// Check 3: entry_point_exists
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
if (result.success) {
const entryPoint = join(pluginDir, result.data.main);
try {
await stat(entryPoint);
checks.push({ name: "entry_point_exists", status: "pass" });
} catch {
checks.push({ name: "entry_point_exists", status: "fail", message: `Entry point not found: ${result.data.main}` });
}
} else {
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped — manifest invalid" });
}
} catch {
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped — manifest unreadable" });
}
// Check 4: can_spawn (simplified — check entry point is .js/.mjs)
try {
const raw = await readFile(manifestPath, "utf-8");
const parsed = JSON.parse(raw);
const result = safeValidateManifest(parsed);
if (result.success) {
const main = result.data.main;
const ext = main.split(".").pop();
checks.push({
name: "can_spawn",
status: ext === "js" || ext === "mjs" ? "pass" : "warn",
message: ext === "js" || ext === "mjs" ? undefined : `Unexpected extension: .${ext}`,
});
} else {
checks.push({ name: "can_spawn", status: "warn", message: "Skipped — manifest invalid" });
}
} catch {
checks.push({ name: "can_spawn", status: "warn", message: "Skipped — manifest unreadable" });
}
// Check 5: db_status_correct
const dbRow = getPluginByName(pluginName);
if (dbRow) {
checks.push({
name: "db_status_correct",
status: "pass",
message: `Status: ${dbRow.status}`,
});
} else {
checks.push({
name: "db_status_correct",
status: "warn",
message: "Plugin not in database",
});
}
// Overall
const failCount = checks.filter((c) => c.status === "fail").length;
const warnCount = checks.filter((c) => c.status === "warn").length;
let overall: DoctorResult["overall"];
if (failCount === 0 && warnCount === 0) {
overall = "healthy";
} else if (failCount === 0) {
overall = "degraded";
} else {
overall = "unhealthy";
}
log.info("doctor.result", { pluginName, overall, failCount, warnCount });
return { pluginName, checks, overall };
}