import type { OcxConfig } from "../../types"; import type { PersistedConfigMutation, PersistedConfigMutationOutcome } from "../../config"; import type { NativeProfileApiDeps } from "../../codex/native-profile-api"; import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protection"; import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance"; import type { StartupHealth } from "../../codex/autostart-health"; import type { StartupInstallAction } from "../startup-action-control"; import type { ManagementPrincipal, ManagementSessionControl } from "../management-auth"; import type { CatalogModel } from "../../codex/catalog"; import type { refreshOwnedCatalogIntegrations } from "../../integrations/catalog-refresh"; import type { Paths as CodexPromptPaths } from "../../codex/prompt-layers"; import type { injectGrokConfig } from "../../grok/inject"; import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p"; import type { probeClaudeDesktopPolicy } from "../../claude/desktop-policy"; import type { RuntimePortState } from "../../config/process-state"; import type { CursorInstall } from "../../integrations/cursor-detect"; import type { CursorEffortTable } from "../../integrations/cursor-effort-table"; import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types"; import type { performCodexRestart, readCodexAppServerState, } from "../../codex/app-server-restart-service"; export interface ManagementApiDeps { /** Canonical origin captured from the running listener; management writes must not change it. */ activeServerOrigin?: string; /** Listener settings captured at bind time so same-origin certificate/path edits still require restart. */ activeServerConfig?: Pick; /** Isolates automatic owned-client writes in route tests. */ refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations; /** Platform seam for capability projections; does not alter host-level startup behavior. */ platform?: NodeJS.Platform; toggleCodexMultiAgentV2?: (enabled: boolean) => void; /** Test seam for ordered V2 scalar side effects. */ v2ScalarWriters?: Partial<{ setAgentsEnabled: (value: boolean | null) => { ok: true; changed: boolean } | { ok: false; error: string }; setAgentsMaxDepth: (value: number | null) => { ok: true; changed: boolean } | { ok: false; error: string }; setSubagentDeveloperInstructions: (value: string | null) => { ok: true; changed: boolean } | { ok: false; error: string }; setMultiAgentModeHintText: (value: string | null) => { ok: true; changed: boolean } | { ok: false; error: string }; }>; toggleDefaultModeRequestUserInput?: (enabled: boolean) => void; createManagementConvergeCodex?: (config: Readonly) => ConvergeCodex; /** Test-only destination for best-effort Claude agent-definition sync. */ claudeAgentConfigDir?: string; /** Startup-health seam keeps route tests from launching platform probes. */ getCachedStartupHealth?: (config: Pick) => Promise; /** * Persistence seam for route-level tests. Production leaves this unset and uses * `saveConfigPreservingClaudeCode`; tests that pass an in-memory fixture config * MUST inject a no-op/spy so the fixture can never overwrite the user's real * OPENCODEX_HOME (incident: devlog 260730.../070). */ saveConfigPreservingClaudeCode?: (config: OcxConfig) => void; /** Config-mutation seam for routes that commit through `mutatePersistedConfig`. */ mutatePersistedConfig?: typeof import("../../config").mutatePersistedConfig; /** Storage-policy job seam keeps management routes off persistence-bearing worker modules. */ storageCleanupPolicyJob?: { getState: typeof import("../../storage/policy-job").getStorageCleanupPolicyJobState; getTestStream: typeof import("../../storage/policy-job").getStorageCleanupPolicyTestStreamResponse; requestRun: typeof import("../../storage/policy-job").requestStorageCleanupPolicyRun; }; /** Test-only fetch injection for Replit gateway install probes. */ probeFetch?: typeof globalThis.fetch; /** * Catalog seam for the Grok toggle (WP2, devlog 260803_integrations_toggle_all * Rev 3 N2). Production leaves this unset and the route dynamic-imports the * real one — a static import would close a cycle with management-api.ts. * Tests stub it to orphan the fixture file mid-fetch (the r7 recheck test). */ fetchAllModels?: (config: OcxConfig) => Promise; /** * Writer seam for the Grok toggle: lets a test place the file in any state * between the pre-write recheck and the write itself (the r8 post-inspection * tests). Production leaves this unset and uses the real writer. */ injectGrokConfig?: typeof injectGrokConfig; /** Desktop mutation seams keep route tests inside temporary config libraries. */ removeDesktop3pStandardPivot?: typeof removeDesktop3pStandardPivot; writeDesktop3pConfig?: typeof writeDesktop3pConfig; /** Read-only Windows MDM policy seam for status/apply tests. */ probeClaudeDesktopPolicy?: typeof probeClaudeDesktopPolicy; /** * Runtime-state seam: the fence must name the host/port the RUNNING process * bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend * on the developer's real runtime state file. */ readRuntimePort?: (pid: number) => RuntimePortState | null; loadCursorEffortTable?: (install: CursorInstall | undefined) => CursorEffortTable | null; clearThreadAccountMap?: () => void; clearProviderQuotaCache?: () => void; primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise | void; runStartupInstallAction?: ( action: StartupInstallAction, options?: { repair?: boolean }, ) => Promise<{ message: string }>; /** * Native-main profile persistence seam for server-boundary tests. Production * leaves this unset, so the route creates its normal NativeProfileManager. */ /** * Codex app-server restart seam (devlog/_fin/260815_gui_codex_restart). * Grouped rather than three separate fields: the route is an adapter over one * service, and a route test that could not stub it would really terminate the * developer's own Codex app-servers. */ codexRestartService?: { readState: typeof readCodexAppServerState; performRestart: typeof performCodexRestart; }; nativeProfileApi?: NativeProfileApiDeps; /** * Log Guard mutation seam. Production leaves this unset and therefore uses the * owner-verified process enumerator, trusted L namespace and real config store. * Route tests inject all three so they cannot depend on local Codex processes * or create lock/config state outside the fixture. */ codexLogGuardProtectionDeps?: CodexLogGuardProtectionDeps; /** * Log Guard maintenance seam. Production reuses the same fail-closed process * enumerator and L namespace as Protect; route tests keep all maintenance * state inside their temporary Codex home. */ codexLogGuardMaintenanceDeps?: CodexLogGuardMaintenanceDeps; /** * Prompt-layer path seam. Production leaves this unset and * `src/codex/prompt-layers.ts` resolves the real CODEX_HOME. A route test * that could not inject paths would read AND WRITE the developer's live * ~/.codex/config.toml — the same class of incident * `saveConfigPreservingClaudeCode` above exists to prevent. */ codexPromptPaths?: CodexPromptPaths; } /** A direct route dispatch has no authority to write the operator's config. */ export class MissingManagementPersistenceError extends Error { constructor() { super("Management config persistence is unavailable."); this.name = "MissingManagementPersistenceError"; } } /** Marks a failed management persistence dependency so dispatch can restore its live snapshot. */ export class ManagementPersistenceError extends Error { readonly code: unknown; response?: Response; constructor(cause: unknown) { const message = cause instanceof Error && cause.message ? `Management config persistence failed: ${cause.message}` : "Management config persistence failed."; super(message, { cause }); this.name = "ManagementPersistenceError"; this.code = cause && typeof cause === "object" ? (cause as { code?: unknown }).code : undefined; } } /** The only whole-live-config persistence boundary available to management routes. */ export function saveManagementConfig(deps: ManagementApiDeps, config: OcxConfig): void { if (!deps.saveConfigPreservingClaudeCode) throw new MissingManagementPersistenceError(); try { deps.saveConfigPreservingClaudeCode(config); } catch (error) { throw new ManagementPersistenceError(error); } } /** The only locked, field-scoped on-disk mutation boundary available to management routes. */ export function mutateManagementConfig( deps: ManagementApiDeps, mutate: (config: OcxConfig) => PersistedConfigMutation, ): PersistedConfigMutationOutcome { if (!deps.mutatePersistedConfig) throw new MissingManagementPersistenceError(); try { return deps.mutatePersistedConfig(mutate); } catch (error) { throw new ManagementPersistenceError(error); } } export interface ManagementContext { req: Request; url: URL; config: OcxConfig; deps: ManagementApiDeps; /** Installed package version projected through bounded system identity routes. */ version: string; /** * Which credential authorized this request, resolved by the auth gate before * dispatch. Routes that spend the USER's identity (not just the proxy's) must * branch on this instead of on request headers: the admin token is readable by * anything running as the user, so a token holder can forge any header a route * might otherwise treat as browser evidence. Undefined only in direct-dispatch * tests, which are treated as the untrusted `admin-token` case. */ principal?: ManagementPrincipal; /** Narrow current-session revocation seam; contains neither the token nor session map. */ sessionControl?: ManagementSessionControl; convergeCodexCatalog: () => Promise; syncClaudeAgentDefsBestEffort: () => Promise; }