diff --git a/.env.example b/.env.example index 850727353e..acf629530c 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/changelog.d/features/6342-cliproxy-account-health.md b/changelog.d/features/6342-cliproxy-account-health.md new file mode 100644 index 0000000000..69b57b41fd --- /dev/null +++ b/changelog.d/features/6342-cliproxy-account-health.md @@ -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) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index faeb76090d..2b5a82a231 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -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). | diff --git a/src/app/(dashboard)/dashboard/providers/services/components/CliproxyAccountHealthCard.tsx b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyAccountHealthCard.tsx new file mode 100644 index 0000000000..ab83b8d047 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/services/components/CliproxyAccountHealthCard.tsx @@ -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 = { + 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 ( +
  • +
    +
    + + {account.label || account.authIndex} + + + {state || "Unknown"} + +
    +

    + {[account.provider || account.type, account.label ? account.authIndex : ""] + .filter(Boolean) + .join(" · ")} +

    +
    +
    +
    {account.success.toLocaleString()} succeeded
    +
    {account.failed.toLocaleString()} failed
    +
    +
  • + ); +} + +export function CliproxyAccountHealthCard() { + const [result, setResult] = useState(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 ( + void load()} loading={loading}> + Refresh + + } + > + {result?.state === "ready" ? ( + result.accounts.length > 0 ? ( +
      + {result.accounts.map((account) => ( + + ))} +
    + ) : ( +

    No CLIProxyAPI accounts found.

    + ) + ) : ( +

    + {loading && !result ? "Loading account health…" : STATE_LABELS[result?.state ?? "unreachable"]} +

    + )} +
    + ); +} diff --git a/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx b/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx index 0d26cb6e6d..7beb9fef8c 100644 --- a/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/tabs/CliproxyServiceTab.tsx @@ -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() { + diff --git a/src/app/api/services/cliproxy/_lib.ts b/src/app/api/services/cliproxy/_lib.ts index bc9b9b52b8..1009c316b8 100644 --- a/src/app/api/services/cliproxy/_lib.ts +++ b/src/app/api/services/cliproxy/_lib.ts @@ -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 { 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, diff --git a/src/app/api/services/cliproxy/accounts/route.ts b/src/app/api/services/cliproxy/accounts/route.ts new file mode 100644 index 0000000000..86698d0c52 --- /dev/null +++ b/src/app/api/services/cliproxy/accounts/route.ts @@ -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 { + if (!(await isAuthenticated(request))) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + return Response.json(await getCliproxyAccountHealth(), { + headers: { "Cache-Control": "no-store" }, + }); +} diff --git a/src/lib/services/bootstrap.ts b/src/lib/services/bootstrap.ts index bcf1f69c1b..559afeae7e 100644 --- a/src/lib/services/bootstrap.ts +++ b/src/lib/services/bootstrap.ts @@ -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 { diff --git a/src/lib/services/cliproxyAccountHealth.ts b/src/lib/services/cliproxyAccountHealth.ts new file mode 100644 index 0000000000..40dfc9c1ee --- /dev/null +++ b/src/lib/services/cliproxyAccountHealth.ts @@ -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 | null { + return value !== null && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : 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 => 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 => 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 { + let connection: Awaited>; + 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); + } +} diff --git a/src/lib/services/installers/cliproxy.ts b/src/lib/services/installers/cliproxy.ts index dcb7dfc916..9d83969ac8 100644 --- a/src/lib/services/installers/cliproxy.ts +++ b/src/lib/services/installers/cliproxy.ts @@ -101,7 +101,7 @@ export async function update(): Promise { * 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, }; } diff --git a/tests/unit/api/services/cliproxy-accounts.test.ts b/tests/unit/api/services/cliproxy-accounts.test.ts new file mode 100644 index 0000000000..d282b7d11c --- /dev/null +++ b/tests/unit/api/services/cliproxy-accounts.test.ts @@ -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, []); +}); diff --git a/tests/unit/dashboard/providers/services/cliproxy-tab.test.ts b/tests/unit/dashboard/providers/services/cliproxy-tab.test.ts index f7bdc6e040..98cf7f687e 100644 --- a/tests/unit/dashboard/providers/services/cliproxy-tab.test.ts +++ b/tests/unit/dashboard/providers/services/cliproxy-tab.test.ts @@ -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 { diff --git a/tests/unit/services/cliproxy-account-health.test.ts b/tests/unit/services/cliproxy-account-health.test.ts new file mode 100644 index 0000000000..8a03c0704f --- /dev/null +++ b/tests/unit/services/cliproxy-account-health.test.ts @@ -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); + }); +}); diff --git a/tests/unit/services/installers/cliproxy-resolve-spawn-args-6877.test.ts b/tests/unit/services/installers/cliproxy-resolve-spawn-args-6877.test.ts index cc8b0a3102..98408a999d 100644 --- a/tests/unit/services/installers/cliproxy-resolve-spawn-args-6877.test.ts +++ b/tests/unit/services/installers/cliproxy-resolve-spawn-args-6877.test.ts @@ -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