feat(services): expose sanitized CLIProxyAPI account health (#11314)

Validated on a 17-PR combined board: cliproxy-accounts + cliproxy-tab + cliproxy-account-health + cliproxy-resolve-spawn-args-6877 (16/16) within the board's 287/287, typecheck:core clean, env-doc-sync clean. Exposes a sanitized read-only CLIProxyAPI account health view (5s-bounded client, explicit allowlist excluding names/paths/emails/tokens/status messages) through a management-authenticated API + dashboard card. Closes #6342. Thank you @RaviTharuma!
This commit is contained in:
Ravi Tharuma
2026-08-24 06:55:31 +02:00
committed by GitHub
parent c21460f22a
commit c3cd1f94c0
14 changed files with 539 additions and 5 deletions

View File

@@ -2021,6 +2021,9 @@ APP_LOG_TO_FILE=true
# CLIPROXYAPI_HOST=127.0.0.1
# CLIPROXYAPI_PORT=5544
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
# Management key for an externally managed instance. Embedded instances use
# OmniRoute's encrypted service key.
# CLIPROXYAPI_MANAGEMENT_KEY=
# ── Mux embedded service ──
# Override the port where the embedded Mux (coder/mux) agent-orchestration

View File

@@ -0,0 +1 @@
- feat(services): show sanitized CLIProxyAPI account health from its authenticated management API without exposing credentials, file paths, or raw account metadata (#6342)

View File

@@ -1044,6 +1044,7 @@ desktop install.
| `EMBED_WS_PROXY_PORT` | `20131` | `src/lib/services/embedWsProxy.ts` | Port for the embedded-service WebSocket proxy server. |
| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). |
| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. |
| `CLIPROXYAPI_MANAGEMENT_KEY` | _(empty)_ | `src/lib/services/cliproxyAccountHealth.ts` | Management key for account-health reads from an externally managed CLIProxyAPI instance. |
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. |
| `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). |
| `DARIO_HOST` | `127.0.0.1` | `open-sse/executors/dario.ts` | Dario embedded-service bind/connect host (loopback only by default). |

View File

@@ -0,0 +1,95 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Badge, Button, Card } from "@/shared/components";
import type {
CliproxyAccountHealth,
CliproxyAccountHealthResult,
} from "@/lib/services/cliproxyAccountHealth";
const STATE_LABELS: Record<CliproxyAccountHealthResult["state"], string> = {
ready: "Account health",
disabled: "CLIProxyAPI is not installed",
missing_key: "Management key is not configured",
unreachable: "Management API is unreachable",
unauthorized: "Management key was rejected",
unsupported: "This CLIProxyAPI version does not expose account health",
invalid_response: "Management API returned an unsupported response",
};
function AccountRow({ account }: { account: CliproxyAccountHealth }) {
const state = account.disabled ? "Disabled" : account.unavailable ? "Unavailable" : account.status;
return (
<li className="flex flex-wrap items-center justify-between gap-3 border-t border-border py-3 first:border-t-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-text-main">
{account.label || account.authIndex}
</span>
<Badge variant={account.disabled || account.unavailable ? "warning" : "success"}>
{state || "Unknown"}
</Badge>
</div>
<p className="mt-1 text-xs text-text-muted">
{[account.provider || account.type, account.label ? account.authIndex : ""]
.filter(Boolean)
.join(" · ")}
</p>
</div>
<div className="text-right text-xs text-text-muted">
<div>{account.success.toLocaleString()} succeeded</div>
<div>{account.failed.toLocaleString()} failed</div>
</div>
</li>
);
}
export function CliproxyAccountHealthCard() {
const [result, setResult] = useState<CliproxyAccountHealthResult | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
try {
const response = await fetch("/api/services/cliproxy/accounts", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setResult(await response.json());
} catch {
setResult({ state: "unreachable", accounts: [], version: null });
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
return (
<Card
title="CLIProxyAPI accounts"
subtitle="Read-only status from the authenticated management API"
action={
<Button variant="secondary" size="sm" onClick={() => void load()} loading={loading}>
Refresh
</Button>
}
>
{result?.state === "ready" ? (
result.accounts.length > 0 ? (
<ul aria-label="CLIProxyAPI account health">
{result.accounts.map((account) => (
<AccountRow key={account.authIndex} account={account} />
))}
</ul>
) : (
<p className="text-sm text-text-muted">No CLIProxyAPI accounts found.</p>
)
) : (
<p className="text-sm text-text-muted">
{loading && !result ? "Loading account health…" : STATE_LABELS[result?.state ?? "unreachable"]}
</p>
)}
</Card>
);
}

View File

@@ -8,6 +8,7 @@ import { AutoStartToggle } from "../components/AutoStartToggle";
import { AutoRestartAdoptedToggle } from "../components/AutoRestartAdoptedToggle";
import { CliproxyConnectionPanel } from "../components/CliproxyConnectionPanel";
import { CliproxyProviderExposureCard } from "../components/CliproxyProviderExposureCard";
import { CliproxyAccountHealthCard } from "../components/CliproxyAccountHealthCard";
const NAME = "cliproxy";
@@ -19,6 +20,7 @@ export function CliproxyServiceTab() {
<AutoStartToggle name={NAME} />
<AutoRestartAdoptedToggle name={NAME} />
<CliproxyConnectionPanel />
<CliproxyAccountHealthCard />
<CliproxyProviderExposureCard />
<CliproxyModelMappingEditor />
<ServiceLogsPanel name={NAME} />

View File

@@ -6,6 +6,7 @@
import { getSupervisor, registerSupervisor } from "@/lib/services/registry";
import { ServiceSupervisor } from "@/lib/services/ServiceSupervisor";
import { resolveSpawnArgs, CLIPROXY_DEFAULT_PORT } from "@/lib/services/installers/cliproxy";
import { getOrCreateApiKey } from "@/lib/services/apiKey";
const TOOL = "cliproxy";
const PORT = parseInt(process.env.CLIPROXYAPI_PORT ?? String(CLIPROXY_DEFAULT_PORT), 10);
@@ -14,10 +15,11 @@ export async function getOrInitSupervisor(): Promise<ServiceSupervisor> {
const existing = getSupervisor(TOOL);
if (existing) return existing;
const managementKey = await getOrCreateApiKey(TOOL);
const sup = new ServiceSupervisor({
tool: TOOL,
port: PORT,
spawnArgs: () => resolveSpawnArgs(PORT),
spawnArgs: () => resolveSpawnArgs(PORT, managementKey),
healthUrl: () => `http://127.0.0.1:${PORT}/v1/models`,
healthIntervalMs: 5_000,
stopTimeoutMs: 15_000,

View File

@@ -0,0 +1,13 @@
import { getCliproxyAccountHealth } from "@/lib/services/cliproxyAccountHealth";
import { isAuthenticated } from "@/shared/utils/apiAuth";
export const dynamic = "force-dynamic";
export async function GET(request: Request): Promise<Response> {
if (!(await isAuthenticated(request))) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json(await getCliproxyAccountHealth(), {
headers: { "Cache-Control": "no-store" },
});
}

View File

@@ -63,7 +63,7 @@ const SERVICES: ServiceEntry[] = [
healthIntervalMs: 5_000,
stopTimeoutMs: 15_000,
logsBufferBytes: 5_242_880,
needsApiKey: false,
needsApiKey: true,
},
{
tool: "mux",
@@ -115,7 +115,7 @@ function buildSpawnArgsFactory(
if (cfg.tool === "dario") {
return () => darioSpawnArgs(apiKey, cfg.port);
}
return () => cliproxySpawnArgs(cfg.port);
return () => cliproxySpawnArgs(cfg.port, apiKey);
}
export async function bootstrapEmbeddedServices(): Promise<void> {

View File

@@ -0,0 +1,204 @@
import { getServiceRow } from "@/lib/db/versionManager";
import { getOrCreateApiKey } from "@/lib/services/apiKey";
import { CLIPROXY_DEFAULT_PORT } from "@/lib/services/installers/cliproxy";
const DEFAULT_TIMEOUT_MS = 5_000;
const AUTH_FILES_PATH = "/v0/management/auth-files";
export type CliproxyAccountHealthState =
| "ready"
| "disabled"
| "missing_key"
| "unreachable"
| "unauthorized"
| "unsupported"
| "invalid_response";
export interface CliproxyRecentRequest {
time: string;
success: number;
failed: number;
}
export interface CliproxyAccountHealth {
authIndex: string;
provider: string;
type: string;
label: string;
status: string;
disabled: boolean;
unavailable: boolean;
createdAt: string | null;
updatedAt: string | null;
success: number;
failed: number;
recentRequests: CliproxyRecentRequest[];
}
export interface CliproxyAccountHealthResult {
state: CliproxyAccountHealthState;
accounts: CliproxyAccountHealth[];
version: string | null;
}
type FetchLike = typeof fetch;
interface GetCliproxyAccountHealthOptions {
fetchImpl?: FetchLike;
timeoutMs?: number;
host?: string;
port?: number;
managementKey?: string | null;
embedded?: boolean;
}
function record(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function string(value: unknown): string {
return typeof value === "string" ? value : "";
}
function nullableTimestamp(value: unknown): string | null {
const text = string(value);
return text && !Number.isNaN(Date.parse(text)) ? text : null;
}
function count(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
}
function sanitizeRecentRequests(value: unknown): CliproxyRecentRequest[] {
if (!Array.isArray(value)) return [];
return value
.slice(0, 20)
.map(record)
.filter((bucket): bucket is Record<string, unknown> => bucket !== null)
.map((bucket) => ({
time: nullableTimestamp(bucket.time) ?? "",
success: count(bucket.success),
failed: count(bucket.failed),
}))
.filter((bucket) => bucket.time !== "");
}
export function sanitizeCliproxyAuthFiles(payload: unknown): CliproxyAccountHealth[] | null {
const files = record(payload)?.files;
if (!Array.isArray(files)) return null;
return files
.map(record)
.filter((file): file is Record<string, unknown> => file !== null)
.map((file) => ({
authIndex: string(file.auth_index),
provider: string(file.provider),
type: string(file.type),
label: string(file.label),
status: string(file.status),
disabled: file.disabled === true,
unavailable: file.unavailable === true,
createdAt: nullableTimestamp(file.created_at),
updatedAt: nullableTimestamp(file.updated_at ?? file.modtime),
success: count(file.success),
failed: count(file.failed),
recentRequests: sanitizeRecentRequests(file.recent_requests),
}))
.filter((file) => file.authIndex !== "");
}
async function resolveConnection(
options: GetCliproxyAccountHealthOptions
): Promise<
| { state: "ready"; host: string; port: number; managementKey: string }
| { state: "disabled" | "missing_key" }
> {
if (options.managementKey !== undefined) {
const key = options.managementKey?.trim();
if (!key) return { state: "missing_key" };
return {
state: "ready",
host: options.host ?? "127.0.0.1",
port: options.port ?? CLIPROXY_DEFAULT_PORT,
managementKey: key,
};
}
const externalHost = process.env.CLIPROXYAPI_HOST?.trim();
const externalKey = process.env.CLIPROXYAPI_MANAGEMENT_KEY?.trim();
const embedded = options.embedded ?? !(externalHost || externalKey);
if (embedded) {
const row = await getServiceRow("cliproxy");
if (!row || row.status === "not_installed") return { state: "disabled" };
return {
state: "ready",
host: options.host ?? "127.0.0.1",
port: options.port ?? row.port ?? CLIPROXY_DEFAULT_PORT,
managementKey: await getOrCreateApiKey("cliproxy"),
};
}
if (!externalKey) return { state: "missing_key" };
const configuredPort = Number.parseInt(process.env.CLIPROXYAPI_PORT ?? "", 10);
return {
state: "ready",
host: options.host ?? externalHost,
port:
options.port ??
(Number.isInteger(configuredPort) && configuredPort > 0
? configuredPort
: CLIPROXY_DEFAULT_PORT),
managementKey: externalKey,
};
}
export async function getCliproxyAccountHealth(
options: GetCliproxyAccountHealthOptions = {}
): Promise<CliproxyAccountHealthResult> {
let connection: Awaited<ReturnType<typeof resolveConnection>>;
try {
connection = await resolveConnection(options);
} catch {
return { state: "missing_key", accounts: [], version: null };
}
if (connection.state !== "ready") {
return { state: connection.state, accounts: [], version: null };
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
try {
const response = await (options.fetchImpl ?? fetch)(
`http://${connection.host}:${connection.port}${AUTH_FILES_PATH}`,
{
headers: { Authorization: `Bearer ${connection.managementKey}` },
signal: controller.signal,
}
);
const version = response.headers.get("x-cpa-version");
if (response.status === 401 || response.status === 403) {
return { state: "unauthorized", accounts: [], version };
}
if (response.status === 404) {
return { state: "unsupported", accounts: [], version };
}
if (!response.ok) {
return { state: "unreachable", accounts: [], version };
}
let payload: unknown;
try {
payload = await response.json();
} catch {
return { state: "invalid_response", accounts: [], version };
}
const accounts = sanitizeCliproxyAuthFiles(payload);
return accounts
? { state: "ready", accounts, version }
: { state: "invalid_response", accounts: [], version };
} catch {
return { state: "unreachable", accounts: [], version: null };
} finally {
clearTimeout(timeout);
}
}

View File

@@ -101,7 +101,7 @@ export async function update(): Promise<InstallResult> {
* ServiceSupervisor calls spawnArgs() synchronously just before spawn(), so
* async file I/O is not available here.
*/
export function resolveSpawnArgs(port: number): SpawnArgs {
export function resolveSpawnArgs(port: number, managementKey?: string): SpawnArgs {
// #11236 (bug 3 residual): runtime os.platform() read — a process.platform
// literal here is constant-folded to the Linux build machine when the
// published artifact is bundled, dropping the `.exe` suffix from the spawn
@@ -116,10 +116,12 @@ export function resolveSpawnArgs(port: number): SpawnArgs {
fs.writeFileSync(configPath, `port: ${port}\nhost: 127.0.0.1\nlog_level: warn\n`, "utf8");
}
const env = { ...process.env };
if (managementKey) env.MANAGEMENT_PASSWORD = managementKey;
return {
command: symlinkPath,
args: ["--config", configPath],
env: { ...process.env },
env,
cwd: CONFIG_DIR,
};
}

View File

@@ -0,0 +1,47 @@
import { before, after, it } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cliproxy-accounts-api-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "cliproxy-accounts-api-test-secret";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../../../src/lib/db/core.ts");
const settingsDb = await import("../../../../src/lib/db/settings.ts");
const apiKeysDb = await import("../../../../src/lib/db/apiKeys.ts");
const { GET } = await import("../../../../src/app/api/services/cliproxy/accounts/route.ts");
before(async () => {
await settingsDb.updateSettings({ requireLogin: true });
process.env.INITIAL_PASSWORD = "cliproxy-accounts-test-password";
});
after(() => {
delete process.env.INITIAL_PASSWORD;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
it("requires OmniRoute management authentication", async () => {
const response = await GET(
new Request("http://localhost/api/services/cliproxy/accounts")
);
assert.equal(response.status, 401);
});
it("accepts a scoped OmniRoute management API key", async () => {
const { key } = await apiKeysDb.createApiKey("cliproxy-accounts", "test", ["manage"]);
const response = await GET(
new Request("http://localhost/api/services/cliproxy/accounts", {
headers: { Authorization: `Bearer ${key}` },
})
);
assert.equal(response.status, 200);
assert.equal(response.headers.get("cache-control"), "no-store");
const body = await response.json();
assert.equal(body.state, "disabled");
assert.deepEqual(body.accounts, []);
});

View File

@@ -17,6 +17,14 @@ describe("CliproxyServiceTab — module shape", () => {
});
});
describe("CliproxyServiceTab — account health", () => {
it("exports the read-only account health card", async () => {
const mod =
await import("../../../../../src/app/(dashboard)/dashboard/providers/services/components/CliproxyAccountHealthCard.tsx");
assert.equal(typeof mod.CliproxyAccountHealthCard, "function");
});
});
// ── URL validation (mirrors isValidUrl inside the tab) ────────────────────────
function isValidUrl(value: string): boolean {

View File

@@ -0,0 +1,148 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
getCliproxyAccountHealth,
sanitizeCliproxyAuthFiles,
} from "../../../src/lib/services/cliproxyAccountHealth.ts";
describe("CLIProxyAPI account health", () => {
it("keeps only the documented health allowlist", () => {
const accounts = sanitizeCliproxyAuthFiles({
files: [
{
auth_index: "acct-1",
provider: "codex",
type: "codex",
label: "Work",
status: "active",
disabled: false,
unavailable: true,
created_at: "2026-08-23T10:00:00Z",
updated_at: "2026-08-23T11:00:00Z",
success: 9,
failed: 2,
recent_requests: [
{ time: "2026-08-23T11:00:00Z", success: 3, failed: 1, token: "secret" },
],
path: "/home/user/.cli-proxy-api/acct.json",
access_token: "secret",
metadata: { refresh_token: "secret" },
email: "private@example.com",
},
],
});
assert.deepEqual(accounts, [
{
authIndex: "acct-1",
provider: "codex",
type: "codex",
label: "Work",
status: "active",
disabled: false,
unavailable: true,
createdAt: "2026-08-23T10:00:00Z",
updatedAt: "2026-08-23T11:00:00Z",
success: 9,
failed: 2,
recentRequests: [{ time: "2026-08-23T11:00:00Z", success: 3, failed: 1 }],
},
]);
const serialized = JSON.stringify(accounts);
for (const secret of ["path", "access_token", "refresh_token", "private@example.com"]) {
assert.equal(serialized.includes(secret), false);
}
});
it("rejects malformed payloads", () => {
assert.equal(sanitizeCliproxyAuthFiles({ files: "not-an-array" }), null);
assert.equal(sanitizeCliproxyAuthFiles(null), null);
});
it("uses management auth and never forwards the key", async () => {
let observed: { url: string; authorization: string | null } | undefined;
const result = await getCliproxyAccountHealth({
managementKey: "management-secret",
host: "127.0.0.1",
port: 8317,
fetchImpl: async (input, init) => {
const headers = new Headers(init?.headers);
observed = { url: String(input), authorization: headers.get("authorization") };
return Response.json(
{ files: [{ auth_index: "acct-1", status: "active" }] },
{ headers: { "x-cpa-version": "7.5.0" } }
);
},
});
assert.deepEqual(observed, {
url: "http://127.0.0.1:8317/v0/management/auth-files",
authorization: "Bearer management-secret",
});
assert.equal(result.state, "ready");
assert.equal(result.version, "7.5.0");
assert.equal(JSON.stringify(result).includes("management-secret"), false);
});
it("distinguishes missing, unauthorized, unsupported, invalid, and unreachable states", async () => {
assert.equal(
(await getCliproxyAccountHealth({ managementKey: null, embedded: false })).state,
"missing_key"
);
assert.equal(
(
await getCliproxyAccountHealth({
managementKey: "key",
fetchImpl: async () => new Response(null, { status: 401 }),
})
).state,
"unauthorized"
);
assert.equal(
(
await getCliproxyAccountHealth({
managementKey: "key",
fetchImpl: async () => new Response(null, { status: 404 }),
})
).state,
"unsupported"
);
assert.equal(
(
await getCliproxyAccountHealth({
managementKey: "key",
fetchImpl: async () => Response.json({ unexpected: true }),
})
).state,
"invalid_response"
);
assert.equal(
(
await getCliproxyAccountHealth({
managementKey: "key",
fetchImpl: async () => {
throw new Error("connection refused");
},
})
).state,
"unreachable"
);
});
it("bounds a hanging request", async () => {
const started = Date.now();
const result = await getCliproxyAccountHealth({
managementKey: "key",
timeoutMs: 10,
fetchImpl: (_input, init) =>
new Promise((_resolve, reject) => {
init?.signal?.addEventListener("abort", () =>
reject(new DOMException("aborted", "AbortError"))
);
}),
});
assert.equal(result.state, "unreachable");
assert.ok(Date.now() - started < 1_000);
});
});

View File

@@ -66,6 +66,14 @@ describe("resolveSpawnArgs (#6877 — real filesystem)", () => {
);
assert.ok(!result.args.includes("-c"), "args must never contain the short -c flag");
});
it("injects the management password without persisting it in config.yaml", async () => {
const { resolveSpawnArgs } =
await import("../../../../src/lib/services/installers/cliproxy.ts");
const result = resolveSpawnArgs(8317, "management-secret");
assert.equal(result.env.MANAGEMENT_PASSWORD, "management-secret");
const configPath = path.join(dataDir, "services", "cliproxy", "config.yaml");
assert.equal(fs.readFileSync(configPath, "utf8").includes("management-secret"), false);
});
it("uses the .exe command name on Windows", async () => {
// resolveSpawnArgs reads os.platform() at call time (#11236 — a