UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

735 lines (734 loc) 30.1 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js"; import { i as formatErrorMessage, u as toErrorObject } from "./errors-BXgSefBE.js"; import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js"; import { b as isPlainObject } from "./utils-CCC-BEJH.js"; import { t as isBlockedObjectKey } from "./prototype-keys-D2nJOZIy.js"; import "./auth-DXSWhsnk.js"; import { n as resolveGatewayAuth, t as resolveEffectiveSharedGatewayAuth } from "./auth-resolve-BSKl_nHz.js"; import { E as validateConfigObjectWithPlugins, H as projectSourceOntoRuntimeShape, T as validateConfigObjectRawWithPlugins, V as createMergePatch, d as readConfigFileSnapshotForWrite, o as parseConfigJson5, r as createConfigIO, u as readConfigFileSnapshot, y as resolveConfigSnapshotHash } from "./io-Gi7-pyU-.js"; import { L as isBuiltInModelProviderOverlayId } from "./zod-schema.core-1wQTyhOa.js"; import { n as formatConfigIssueLines } from "./issue-format-CwCoGNbt.js"; import { t as applyMergePatch } from "./merge-patch-DFjTrPP1.js"; import { i as replaceConfigFile } from "./config-C9RxTsn1.js"; import { p as scheduleGatewaySigusr1Restart } from "./restart-CiO1ILZU.js"; import { t as extractDeliveryInfo } from "./delivery-info-DYbymE-x.js"; import "./sessions-D6zZvoxX.js"; import { d as writeRestartSentinel, r as formatDoctorNonInteractiveHint } from "./restart-sentinel-BFAG1Tav.js"; import { i as restoreRedactedValues, n as redactConfigObject, r as redactConfigSnapshot } from "./redact-snapshot-CuoBedjD.js"; import { o as getActiveSecretsRuntimeSnapshot } from "./runtime-state-B3MH_XLp.js"; import { t as normalizeConfigPatchReplacePaths } from "./patch-replace-paths-DLvXobiX.js"; import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js"; import { A as validateConfigPatchParams, M as validateConfigSchemaLookupResult, N as validateConfigSchemaParams, O as validateConfigApplyParams, P as validateConfigSetParams, j as validateConfigSchemaLookupParams, k as validateConfigGetParams, t as formatValidationErrors } from "./src-oj0IwW6K.js"; import { r as lookupConfigSchema, t as loadGatewayRuntimeConfigSchema } from "./runtime-schema-BjT8eJxD.js"; import { a as diffConfigPaths, i as resolveConfigReloadMetadata, t as buildGatewayReloadPlan } from "./config-reload-plan-D2TNMpG_.js"; import { t as resolveGatewayReloadSettings } from "./config-reload-settings-BUK2a-7x.js"; import { n as resolveControlPlaneActor, r as summarizeChangedPaths, t as formatControlPlaneActor } from "./control-plane-audit-CfUQZQQ4.js"; import { t as assertValidParams } from "./validation-H8b44ME1.js"; import { o as prepareSecretsRuntimeSnapshot } from "./runtime-CLmjiLBt.js"; import { t as resolveBaseHashParam } from "./base-hash-BJkn_bB6.js"; import { t as parseRestartRequestParams } from "./restart-request-ss08KcjN.js"; import { execFile } from "node:child_process"; import { isDeepStrictEqual } from "node:util"; //#region src/gateway/server-methods/config-write-flow.ts /** Resolves the on-disk config path used in config method responses. */ function resolveGatewayConfigPath(snapshot) { return snapshot?.path ?? createConfigIO().configPath; } function normalizeStringListForAuthCompare(items) { return [...items ?? []].toSorted(); } function normalizeTrustedProxyAuthForCompare(auth) { return { userHeader: auth.trustedProxy?.userHeader, requiredHeaders: normalizeStringListForAuthCompare(auth.trustedProxy?.requiredHeaders), allowUsers: normalizeStringListForAuthCompare(auth.trustedProxy?.allowUsers), allowLoopback: auth.trustedProxy?.allowLoopback }; } /** Compares the effective shared Gateway auth surface that active clients use. */ function didSharedGatewayAuthChange(prev, next) { const prevResolvedAuth = resolveGatewayAuth({ authConfig: prev.gateway?.auth, env: process.env, tailscaleMode: prev.gateway?.tailscale?.mode }); const nextResolvedAuth = resolveGatewayAuth({ authConfig: next.gateway?.auth, env: process.env, tailscaleMode: next.gateway?.tailscale?.mode }); if (prevResolvedAuth.mode === "trusted-proxy" || nextResolvedAuth.mode === "trusted-proxy") { if (prevResolvedAuth.mode !== nextResolvedAuth.mode) return true; return !isDeepStrictEqual(normalizeTrustedProxyAuthForCompare(prevResolvedAuth), normalizeTrustedProxyAuthForCompare(nextResolvedAuth)) || !isDeepStrictEqual(normalizeStringListForAuthCompare(prev.gateway?.trustedProxies), normalizeStringListForAuthCompare(next.gateway?.trustedProxies)); } const prevAuth = resolveEffectiveSharedGatewayAuth({ authConfig: prev.gateway?.auth, env: process.env, tailscaleMode: prev.gateway?.tailscale?.mode }); const nextAuth = resolveEffectiveSharedGatewayAuth({ authConfig: next.gateway?.auth, env: process.env, tailscaleMode: next.gateway?.tailscale?.mode }); if (prevAuth === null || nextAuth === null) return prevAuth !== nextAuth; return prevAuth.mode !== nextAuth.mode || !isDeepStrictEqual(prevAuth.secret, nextAuth.secret); } /** Compares against the active secrets-expanded config when one is available. */ function didActiveSharedGatewayAuthChange(params) { return didSharedGatewayAuthChange(getActiveSecretsRuntimeSnapshot()?.config ?? params.fallbackPrev, params.next); } function queueSharedGatewayAuthDisconnect(shouldDisconnect, context) { if (!shouldDisconnect) return; queueMicrotask(() => { context?.disconnectClientsUsingSharedGatewayAuth?.(); }); } function queueSharedGatewayAuthGenerationRefresh(shouldRefresh, nextConfig, context) { if (!shouldRefresh) return; queueMicrotask(() => { context?.enforceSharedGatewayAuthGenerationForConfigWrite?.(nextConfig); }); } function shouldScheduleDirectConfigRestart(params) { const reloadSettings = resolveGatewayReloadSettings(params.nextConfig); if (reloadSettings.mode === "off") return true; const plan = buildGatewayReloadPlan(params.changedPaths); if (reloadSettings.mode === "hot" && plan.restartGateway) return true; return false; } function resolveConfigRestartRequest(params) { const { sessionKey, deliveryContext: requestedDeliveryContext, threadId: requestedThreadId, note, restartDelayMs } = parseRestartRequestParams(params); const { deliveryContext: sessionDeliveryContext, threadId: sessionThreadId } = extractDeliveryInfo(sessionKey); return { sessionKey, note, restartDelayMs, deliveryContext: requestedDeliveryContext ?? sessionDeliveryContext, threadId: requestedThreadId ?? sessionThreadId }; } function buildConfigRestartSentinelPayload(params) { return { kind: params.kind, status: "ok", ts: Date.now(), sessionKey: params.sessionKey, deliveryContext: params.deliveryContext, threadId: params.threadId, message: params.note ?? null, doctorHint: formatDoctorNonInteractiveHint(), stats: { mode: params.mode, root: params.configPath } }; } async function tryWriteRestartSentinelPayload(payload) { try { return await writeRestartSentinel(payload); } catch { return null; } } /** Persists a gateway config write and returns follow-up work that must run after response. */ async function commitGatewayConfigWrite(params) { const result = await replaceConfigFile({ nextConfig: params.nextConfig, writeOptions: { ...params.writeOptions, runtimeRefresh: { ...params.writeOptions.runtimeRefresh, includeAuthStoreRefs: false } }, afterWrite: { mode: "auto" } }); return { path: resolveGatewayConfigPath(params.snapshot), config: result.nextConfig, queueFollowUp: () => { queueSharedGatewayAuthGenerationRefresh(true, result.nextConfig, params.context); queueSharedGatewayAuthDisconnect(Boolean(params.disconnectSharedAuthClients), params.context); } }; } /** Builds restart sentinel/queue state for config.patch and config.apply writes. */ async function resolveGatewayConfigRestartWriteResult(params) { const { sessionKey, note, restartDelayMs, deliveryContext, threadId } = resolveConfigRestartRequest(params.requestParams); const payload = buildConfigRestartSentinelPayload({ kind: params.kind, mode: params.mode, configPath: params.configPath, sessionKey, deliveryContext, threadId, note }); const sentinelPath = await tryWriteRestartSentinelPayload(payload); const restart = shouldScheduleDirectConfigRestart({ changedPaths: params.changedPaths, nextConfig: params.nextConfig }) ? scheduleGatewaySigusr1Restart({ delayMs: restartDelayMs, reason: params.mode, audit: { actor: params.actor.actor, deviceId: params.actor.deviceId, clientIp: params.actor.clientIp, changedPaths: params.changedPaths } }) : void 0; if (restart?.coalesced) params.context?.logGateway?.warn(`${params.mode} restart coalesced ${formatControlPlaneActor(params.actor)} delayMs=${restart.delayMs}`); return { payload, sentinelPath, restart }; } //#endregion //#region src/gateway/server-methods/config.ts const MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE = 3; const CONFIG_SCHEMA_RESPONSE_CACHE_TTL_MS = 5e3; let configSchemaResponseCache = null; function requireConfigBaseHash(params, snapshot, respond) { if (!snapshot.exists) return true; const snapshotHash = resolveConfigSnapshotHash(snapshot); if (!snapshotHash) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config base hash unavailable; re-run config.get and retry")); return false; } const baseHash = resolveBaseHashParam(params); if (!baseHash) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config base hash required; re-run config.get and retry")); return false; } if (baseHash !== snapshotHash) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config changed since last load; re-run config.get and retry")); return false; } return true; } function formatConfigPatchPath(parentPath, key) { return parentPath ? `${parentPath}.${key}` : key; } function readConfigPatchReplacePaths(params) { const rawPaths = params.replacePaths; return normalizeConfigPatchReplacePaths(Array.isArray(rawPaths) ? rawPaths : void 0); } function collectDestructiveArrayPatchPaths(params) { if (!isPlainObject(params.patch) || !isPlainObject(params.base)) return []; const merged = isPlainObject(params.merged) ? params.merged : {}; const paths = []; for (const [key, patchValue] of Object.entries(params.patch)) { if (isBlockedObjectKey(key)) continue; const path = formatConfigPatchPath(params.path ?? "", key); const baseValue = params.base[key]; const mergedValue = merged[key]; if (Array.isArray(baseValue)) { if (patchValue === null || !Array.isArray(patchValue)) { paths.push(path); continue; } if (Array.isArray(mergedValue)) { if (isConfigPatchIdKeyedArray(baseValue)) { if (!idKeyedArrayPreservesBaseIds(baseValue, mergedValue)) { paths.push(path); continue; } paths.push(...collectDestructiveIdKeyedArrayEntryPatchPaths({ base: baseValue, patch: patchValue, merged: mergedValue, path })); } else if (!arrayPreservesBaseEntries(baseValue, mergedValue)) { paths.push(path); continue; } } } else if (isPlainObject(baseValue) && !isPlainObject(patchValue)) { paths.push(...collectBaseArrayPaths(baseValue, path)); continue; } if (isPlainObject(patchValue)) paths.push(...collectDestructiveArrayPatchPaths({ base: baseValue, patch: patchValue, merged: mergedValue, path })); } return paths; } function collectBaseArrayPaths(base, path) { if (Array.isArray(base)) return [path]; if (!isPlainObject(base)) return []; const paths = []; for (const [key, value] of Object.entries(base)) { if (isBlockedObjectKey(key)) continue; paths.push(...collectBaseArrayPaths(value, formatConfigPatchPath(path, key))); } return paths; } function isConfigPatchObjectWithStringId(value) { return isPlainObject(value) && typeof value.id === "string" && value.id.length > 0; } function isConfigPatchIdKeyedArray(value) { return value.every(isConfigPatchObjectWithStringId); } function idKeyedArrayPreservesBaseIds(base, merged) { const mergedIds = new Set(merged.filter(isConfigPatchObjectWithStringId).map((entry) => entry.id)); return base.every((entry) => mergedIds.has(entry.id)); } function arrayPreservesBaseEntries(base, merged) { const unmatchedMerged = [...merged]; for (const baseEntry of base) { const matchIndex = unmatchedMerged.findIndex((mergedEntry) => isDeepStrictEqual(mergedEntry, baseEntry)); if (matchIndex === -1) return false; unmatchedMerged.splice(matchIndex, 1); } return true; } function collectDestructiveIdKeyedArrayEntryPatchPaths(params) { if (!isConfigPatchIdKeyedArray(params.base)) return []; const baseById = new Map(params.base.map((entry) => [entry.id, entry])); const mergedById = new Map(params.merged.filter(isConfigPatchObjectWithStringId).map((entry) => [entry.id, entry])); const paths = []; for (const patchEntry of params.patch) { if (!isConfigPatchObjectWithStringId(patchEntry)) continue; const baseEntry = baseById.get(patchEntry.id); const mergedEntry = mergedById.get(patchEntry.id); if (!baseEntry || !mergedEntry) continue; paths.push(...collectDestructiveArrayPatchPaths({ base: baseEntry, patch: patchEntry, merged: mergedEntry, path: `${params.path}[]` })); } return paths; } function rejectDestructiveArrayPatchWithoutIntent(params) { const unconfirmedPaths = collectDestructiveArrayPatchPaths({ base: params.currentConfig, patch: params.patch, merged: params.mergedConfig }).filter((path) => !params.replacePaths.has(path)); if (unconfirmedPaths.length === 0) return false; params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `config.patch would remove entries from array path(s): ${unconfirmedPaths.join(", ")}. Pass replacePaths with the exact path(s) when this is intentional, or use config.apply for full-config replacement.`)); return true; } async function readConfigWriteSnapshotOrRespond(params, respond) { const result = await readConfigFileSnapshotForWrite(); if (!requireConfigBaseHash(params, result.snapshot, respond)) return null; return result; } function parseRawConfigOrRespond(params, requestName, respond) { const rawValue = params.raw; if (typeof rawValue !== "string") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid ${requestName} params: raw (string) required`)); return null; } return rawValue; } function sanitizeLookupPathForLog(path) { const sanitized = Array.from(path, (char) => { const code = char.charCodeAt(0); return code < 32 || code === 127 ? "?" : char; }).join(""); return sanitized.length > 120 ? `${sanitized.slice(0, 117)}...` : sanitized; } function escapePowerShellSingleQuotedString(value) { return value.replaceAll("'", "''"); } function resolveConfigOpenCommand(configPath, platform = process.platform) { if (platform === "win32") return { command: "powershell.exe", args: [ "-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath '${escapePowerShellSingleQuotedString(configPath)}'` ] }; return { command: platform === "darwin" ? "open" : "xdg-open", args: [configPath] }; } function execConfigOpenCommand(command) { return new Promise((resolve, reject) => { execFile(command.command, command.args, (error) => { if (error) { reject(toErrorObject(error, "Non-Error rejection")); return; } resolve(); }); }); } function formatConfigOpenError(error) { if (typeof error === "object" && error && "message" in error && typeof error.message === "string") return error.message; return String(error); } function hasOwnRecordValue(value, key) { return isRecord(value) && Object.hasOwn(value, key); } function stripBundledProviderRuntimeDefaults(params) { if (!isRecord(params.candidate)) return params.candidate; const models = params.candidate.models; if (!isRecord(models) || !isRecord(models.providers)) return params.candidate; const sourceModels = isRecord(params.sourceConfig) ? params.sourceConfig.models : void 0; const sourceProviders = isRecord(sourceModels) ? sourceModels.providers : void 0; let nextProviders; for (const [providerId, provider] of Object.entries(models.providers)) { if (!isBuiltInModelProviderOverlayId(providerId) || !isRecord(provider)) continue; const sourceProvider = isRecord(sourceProviders) ? sourceProviders[providerId] : void 0; let nextProvider; if (provider.baseUrl === "" && !hasOwnRecordValue(sourceProvider, "baseUrl")) { nextProvider = { ...provider }; delete nextProvider.baseUrl; } if (Array.isArray(provider.models) && provider.models.length === 0 && !hasOwnRecordValue(sourceProvider, "models")) { nextProvider ??= { ...provider }; delete nextProvider.models; } if (nextProvider) { nextProviders ??= { ...models.providers }; nextProviders[providerId] = nextProvider; } } if (!nextProviders) return params.candidate; return { ...params.candidate, models: { ...models, providers: nextProviders } }; } function parseValidateConfigFromRawOrRespond(params, requestName, snapshot, respond) { const rawValue = parseRawConfigOrRespond(params, requestName, respond); if (!rawValue) return null; const parsedRes = parseConfigJson5(rawValue); if (!parsedRes.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error)); return null; } const schema = loadSchemaWithPlugins(); const restored = restoreRedactedValues(parsedRes.parsed, snapshot.config, schema.uiHints); if (!restored.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, restored.humanReadableMessage ?? "invalid config")); return null; } const validationCandidate = stripBundledProviderRuntimeDefaults({ candidate: snapshot.valid ? applyMergePatch(projectSourceOntoRuntimeShape(snapshot.resolved, snapshot.config), createMergePatch(snapshot.config, restored.result)) : restored.result, sourceConfig: snapshot.parsed }); const sourceValidated = validateConfigObjectRawWithPlugins(validationCandidate); if (!sourceValidated.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(sourceValidated.issues), { details: { issues: sourceValidated.issues } })); return null; } const validated = validateConfigObjectWithPlugins(validationCandidate); if (!validated.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), { details: { issues: validated.issues } })); return null; } return { config: validated.config, writeConfig: validationCandidate, schema }; } function summarizeConfigValidationIssues(issues) { const lines = normalizeStringEntries(formatConfigIssueLines(issues.slice(0, MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE), "", { normalizeRoot: true })); if (lines.length === 0) return "invalid config"; const hiddenCount = Math.max(0, issues.length - lines.length); return `invalid config: ${lines.join("; ")}${hiddenCount > 0 ? ` (+${hiddenCount} more issue${hiddenCount === 1 ? "" : "s"})` : ""}`; } async function ensureResolvableSecretRefsOrRespond(params) { try { return await prepareSecretsRuntimeSnapshot({ config: params.config, includeAuthStoreRefs: false }); } catch (error) { const details = formatErrorMessage(error); params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid config: active SecretRef resolution failed (${details})`)); return null; } } function clearConfigSchemaResponseCacheForTests() { configSchemaResponseCache = null; } function loadConfigSchemaResponseForTests() { return loadSchemaWithPlugins(); } function clearConfigSchemaResponseCache() { configSchemaResponseCache = null; } async function respondWithConfigRestartWrite(params) { clearConfigSchemaResponseCache(); const { payload, sentinelPath, restart } = await resolveGatewayConfigRestartWriteResult({ requestParams: params.requestParams, kind: params.kind, mode: params.mode, configPath: params.writeResult.path, changedPaths: params.changedPaths, nextConfig: params.writeResult.config, actor: params.actor, context: params.context }); params.respond(true, { ok: true, path: params.writeResult.path, config: redactConfigObject(params.writeResult.config, params.uiHints), restart, sentinel: { path: sentinelPath, payload } }, void 0); params.writeResult.queueFollowUp(); } function shouldDisconnectSharedAuthClientsForConfigWrite(params) { return didSharedGatewayAuthChange(params.prevConfig, params.nextConfig) || didActiveSharedGatewayAuthChange({ fallbackPrev: params.prevConfig, next: params.preparedSecretsSnapshot.config }); } function respondConfigPatchNoop(params) { params.context?.logGateway?.info(`config.patch noop ${formatControlPlaneActor(params.actor)} (no changed paths)`); params.respond(true, { ok: true, noop: true, path: resolveGatewayConfigPath(params.snapshot), config: redactConfigObject(params.config, params.uiHints) }, void 0); } function loadSchemaWithPlugins() { const now = asDateTimestampMs(Date.now()); const cachedExpiresAt = configSchemaResponseCache === null ? void 0 : asDateTimestampMs(configSchemaResponseCache.expiresAtMs); if (configSchemaResponseCache && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) return configSchemaResponseCache.response; if (configSchemaResponseCache) configSchemaResponseCache = null; const response = loadGatewayRuntimeConfigSchema(); const expiresAtMs = resolveExpiresAtMsFromDurationMs(CONFIG_SCHEMA_RESPONSE_CACHE_TTL_MS); if (expiresAtMs !== void 0) configSchemaResponseCache = { expiresAtMs, response }; return response; } const configHandlers = { "config.get": async ({ params, respond }) => { if (!assertValidParams(params, validateConfigGetParams, "config.get", respond)) return; respond(true, redactConfigSnapshot(await readConfigFileSnapshot(), loadSchemaWithPlugins().uiHints), void 0); }, "config.schema": ({ params, respond }) => { if (!assertValidParams(params, validateConfigSchemaParams, "config.schema", respond)) return; respond(true, loadSchemaWithPlugins(), void 0); }, "config.schema.lookup": ({ params, respond, context }) => { if (!assertValidParams(params, validateConfigSchemaLookupParams, "config.schema.lookup", respond)) return; const path = params.path; const result = lookupConfigSchema(loadSchemaWithPlugins(), path, resolveConfigReloadMetadata); if (!result) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config schema path not found")); return; } if (!validateConfigSchemaLookupResult(result)) { const errors = validateConfigSchemaLookupResult.errors ?? []; context.logGateway.warn(`config.schema.lookup produced invalid payload for ${sanitizeLookupPathForLog(path)}: ${formatValidationErrors(errors)}`); respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "config.schema.lookup returned invalid payload", { details: { errors } })); return; } respond(true, result, void 0); }, "config.set": async ({ params, respond, context }) => { if (!assertValidParams(params, validateConfigSetParams, "config.set", respond)) return; const writeSnapshot = await readConfigWriteSnapshotOrRespond(params, respond); if (!writeSnapshot) return; const { snapshot, writeOptions } = writeSnapshot; const parsed = parseValidateConfigFromRawOrRespond(params, "config.set", snapshot, respond); if (!parsed) return; if (!await ensureResolvableSecretRefsOrRespond({ config: parsed.config, respond })) return; const writeResult = await commitGatewayConfigWrite({ snapshot, writeOptions, nextConfig: parsed.writeConfig, context }); clearConfigSchemaResponseCache(); respond(true, { ok: true, path: writeResult.path, config: redactConfigObject(writeResult.config, parsed.schema.uiHints) }, void 0); writeResult.queueFollowUp(); }, "config.patch": async ({ params, respond, client, context }) => { if (!assertValidParams(params, validateConfigPatchParams, "config.patch", respond)) return; const writeSnapshot = await readConfigWriteSnapshotOrRespond(params, respond); if (!writeSnapshot) return; const { snapshot, writeOptions } = writeSnapshot; if (!snapshot.valid) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config; fix before patching")); return; } const rawValue = params.raw; if (typeof rawValue !== "string") { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid config.patch params: raw (string) required")); return; } const parsedRes = parseConfigJson5(rawValue); if (!parsedRes.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error)); return; } if (!parsedRes.parsed || typeof parsedRes.parsed !== "object" || Array.isArray(parsedRes.parsed)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config.patch raw must be an object")); return; } const replacePaths = readConfigPatchReplacePaths(params); const merged = applyMergePatch(snapshot.config, parsedRes.parsed, { mergeObjectArraysById: true, replaceArrayPaths: replacePaths }); const schemaPatch = loadSchemaWithPlugins(); const restoredMerge = restoreRedactedValues(merged, snapshot.config, schemaPatch.uiHints); if (!restoredMerge.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, restoredMerge.humanReadableMessage ?? "invalid config")); return; } if (rejectDestructiveArrayPatchWithoutIntent({ currentConfig: snapshot.config, mergedConfig: restoredMerge.result, patch: parsedRes.parsed, replacePaths, respond })) return; const restoredChangedPaths = diffConfigPaths(snapshot.config, restoredMerge.result); const actor = resolveControlPlaneActor(client); if (restoredChangedPaths.length === 0) { respondConfigPatchNoop({ snapshot, config: snapshot.config, uiHints: schemaPatch.uiHints, actor, context, respond }); return; } const validated = validateConfigObjectWithPlugins(restoredMerge.result); if (!validated.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), { details: { issues: validated.issues } })); return; } const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({ config: validated.config, respond }); if (!preparedSecretsSnapshot) return; const changedPaths = diffConfigPaths(snapshot.config, validated.config); if (changedPaths.length === 0) { respondConfigPatchNoop({ snapshot, config: validated.config, uiHints: schemaPatch.uiHints, actor, context, respond }); return; } context?.logGateway?.info(`config.patch write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.patch`); const disconnectSharedAuthClients = shouldDisconnectSharedAuthClientsForConfigWrite({ prevConfig: snapshot.config, nextConfig: validated.config, preparedSecretsSnapshot }); await respondWithConfigRestartWrite({ requestParams: params, kind: "config-patch", mode: "config.patch", writeResult: await commitGatewayConfigWrite({ snapshot, writeOptions, nextConfig: validated.config, context, disconnectSharedAuthClients }), changedPaths, actor, context, respond, uiHints: schemaPatch.uiHints }); }, "config.apply": async ({ params, respond, client, context }) => { if (!assertValidParams(params, validateConfigApplyParams, "config.apply", respond)) return; const writeSnapshot = await readConfigWriteSnapshotOrRespond(params, respond); if (!writeSnapshot) return; const { snapshot, writeOptions } = writeSnapshot; const parsed = parseValidateConfigFromRawOrRespond(params, "config.apply", snapshot, respond); if (!parsed) return; const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({ config: parsed.config, respond }); if (!preparedSecretsSnapshot) return; const changedPaths = diffConfigPaths(snapshot.config, parsed.config); const actor = resolveControlPlaneActor(client); context?.logGateway?.info(`config.apply write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.apply`); const disconnectSharedAuthClients = shouldDisconnectSharedAuthClientsForConfigWrite({ prevConfig: snapshot.config, nextConfig: parsed.config, preparedSecretsSnapshot }); await respondWithConfigRestartWrite({ requestParams: params, kind: "config-apply", mode: "config.apply", writeResult: await commitGatewayConfigWrite({ snapshot, writeOptions, nextConfig: parsed.writeConfig, context, disconnectSharedAuthClients }), changedPaths, actor, context, respond, uiHints: parsed.schema.uiHints }); }, "config.openFile": async ({ params, respond, context }) => { if (!assertValidParams(params, validateConfigGetParams, "config.openFile", respond)) return; const configPath = createConfigIO().configPath; try { await execConfigOpenCommand(resolveConfigOpenCommand(configPath)); respond(true, { ok: true, path: configPath }, void 0); } catch (error) { const errorMessage = formatConfigOpenError(error); const detailedError = errorMessage.includes("xdg-open") && errorMessage.includes("no method available") ? `Cannot open file in headless environment. File path: ${configPath}. This environment appears to lack a graphical or terminal browser handler.` : `Failed to open config file: ${errorMessage}`; context?.logGateway?.warn(`config.openFile failed path=${sanitizeLookupPathForLog(configPath)}: ${errorMessage}`); respond(true, { ok: false, path: configPath, error: detailedError }, void 0); } } }; //#endregion export { clearConfigSchemaResponseCacheForTests, configHandlers, loadConfigSchemaResponseForTests, resolveConfigOpenCommand };