openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
920 lines (919 loc) • 40.5 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { S as createConfigIO, b as createRuntimeConfigWriteApplication, l as readConfigFileSnapshotForWrite, v as attachRuntimeConfigWriteApplication } from "./io.runtime-B9iJRs3w.js";
import { t as isPlainObject } from "./plain-object-5a0EzLzX.js";
import "./utils-P__uGsPB.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { s as readAgentRosterProperty } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { n as isBuiltInModelProviderOverlayId } from "./model-provider-config-DX4Bzd5F.js";
import "./zod-schema.core-D6k6NKKA.js";
import { a as isMergePatchObjectKeyAllowed, i as formatConfigPatchPath, n as createMergePatch, o as normalizeConfigPatchReplacePaths, r as collectBaseArrayPaths, t as applyMergePatch } from "./merge-patch-DCMj2JVh.js";
import { n as projectSourceOntoRuntimeShape } from "./runtime-source-projection-DOHmC11j.js";
import { h as validateConfigObjectWithPlugins, m as validateConfigObjectRawWithPlugins } from "./io.types-BUCjdS5v.js";
import { h as resolveConfigSnapshotHash, l as parseConfigJson5 } from "./io.read-helpers-ZKp-UiGx.js";
import { t as ConfigMutationConflictError } from "./mutation-conflict-Be0wSyDG.js";
import { n as formatConfigIssueLines } from "./issue-format-CQvxWNW8.js";
import "./io-bdCzpGWJ.js";
import { r as replaceConfigFile } from "./mutate-ZNN4iFCn.js";
import "./config-Cs0XXL3x.js";
import { n as resolveGatewayAuth } from "./auth-resolve-O5AKX-sb.js";
import "./auth-CyN_wFeb.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { f as getActivePluginRegistryVersion } from "./runtime-BL4wZfTq.js";
import { $ as validateConfigPatchParams, Q as validateConfigGetParams, Z as validateConfigApplyParams, _g as formatValidationErrors, et as validateConfigSchemaLookupParams, nt as validateConfigSchemaParams, rt as validateConfigSetParams, tt as validateConfigSchemaLookupResult } from "./src-BiL5aQto.js";
import { f as scheduleGatewaySigusr1Restart } from "./restart-DwCats8x.js";
import { i as captureGatewayRootWorkAdmissionContinuationScope } from "./gateway-work-admission-R1IpuDim.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { h as redactSecretDegradationReason, l as isRetryableSecretDegradationReason } from "./runtime-degraded-state-D5EZZ925.js";
import { a as formatDoctorNonInteractiveHint, h as writeRestartSentinel } from "./restart-sentinel-FmKS9LVR.js";
import "./sessions-9nxpeTwt.js";
import { t as extractDeliveryInfo } from "./delivery-info-CfG8kavj.js";
import { i as restoreRedactedValues, n as redactConfigObject } from "./redact-snapshot-DcfZQ78H.js";
import { l as getActiveSecretsRuntimeSnapshotState } from "./runtime-state-C4aJ8Hzz.js";
import { r as lookupConfigSchema } from "./schema-DyKN69ex.js";
import { t as holdGatewayPolicyResponse } from "./ws-policy-close-CZEENNh9.js";
import { t as resolveSharedGatewaySessionGeneration } from "./ws-shared-generation-DZTjxt6J.js";
import { t as assertValidParams } from "./validation-pzrlzFvo.js";
import { n as loadGatewayRuntimeConfigSchema } from "./runtime-schema-D3-BdLtQ.js";
import { n as diffGatewayReloadPaths, t as diffConfigPaths } from "./config-diff-D4CS9rJs.js";
import { n as isNoopGatewayReloadPlan, o as resolveConfigReloadMetadata, r as listConfigReloadRefinementPrefixes, t as buildGatewayReloadPlan } from "./config-reload-plan-DUT3qlnP.js";
import { t as resolveGatewayReloadSettings } from "./config-reload-settings-q1wYjpRM.js";
import { t as normalizeSubmittedConfigModelRefs } from "./model-input-normalization-D35tM3yN.js";
import { n as resolveControlPlaneActor, r as summarizeChangedPaths, t as formatControlPlaneActor } from "./control-plane-audit-CN8L3SYx.js";
import { n as parseRestartRequestParams } from "./restart-request-DCnDLrlM.js";
import { u as prepareSecretsRuntimeSnapshot } from "./runtime-C3AQ3lWb.js";
import { n as readConfigGetResponse, t as invalidateConfigGetResponseCache } from "./config-get-response-JRi23Uhs.js";
import { t as resolveBaseHashParam } from "./base-hash-BJkn_bB6.js";
import { a as sanitizePathForLog, i as resolveOpenPathCommand, n as formatOpenPathError, r as isHeadlessOpenPathError, t as execOpenPath } from "./open-path-Dt2zNCEm.js";
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;
}
/** 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
});
return prevResolvedAuth.mode !== nextResolvedAuth.mode || resolveSharedGatewaySessionGeneration(prevResolvedAuth, prev.gateway?.trustedProxies) !== resolveSharedGatewaySessionGeneration(nextResolvedAuth, next.gateway?.trustedProxies);
}
function projectAuthoredValuesOntoRuntimeOverlay(params) {
const { source, active } = params;
if (active === void 0) return structuredClone(params.fallback);
if (!isRecord(source) || !isRecord(active)) return structuredClone(isDeepStrictEqual(source, params.activeSource) ? active : params.fallback);
const fallback = isRecord(params.fallback) ? params.fallback : {};
const activeSource = isRecord(params.activeSource) ? params.activeSource : {};
const sourceKeys = new Set(Object.keys(source));
return Object.fromEntries([...Object.entries(fallback).filter(([key]) => !sourceKeys.has(key)), ...Object.keys(source).map((key) => [key, projectAuthoredValuesOntoRuntimeOverlay({
source: source[key],
activeSource: activeSource[key],
active: active[key],
fallback: fallback[key]
})])]);
}
/** Compares against the active secrets-expanded config when one is available. */
function didActiveSharedGatewayAuthChange(params) {
const active = getActiveSecretsRuntimeSnapshotState();
if (!active) return didSharedGatewayAuthChange(params.fallbackPrev, params.next);
const currentSourceGateway = (params.fallbackSource ?? active.sourceConfig).gateway;
const activeSourceGateway = active.sourceConfig.gateway;
const activeGateway = active.config.gateway;
const fallbackGateway = params.fallbackPrev.gateway;
const selectOwnedGatewayValue = (key) => currentSourceGateway && Object.hasOwn(currentSourceGateway, key) ? projectAuthoredValuesOntoRuntimeOverlay({
source: currentSourceGateway[key],
activeSource: activeSourceGateway?.[key],
active: activeGateway?.[key],
fallback: fallbackGateway?.[key]
}) : fallbackGateway?.[key];
return didSharedGatewayAuthChange({
...params.fallbackPrev,
gateway: {
...fallbackGateway,
auth: selectOwnedGatewayValue("auth"),
tailscale: selectOwnedGatewayValue("tailscale"),
trustedProxies: selectOwnedGatewayValue("trustedProxies")
}
}, params.next);
}
function resolveConfigRestartRequirement(params) {
const reloadSettings = resolveGatewayReloadSettings(params.nextConfig);
const plan = buildGatewayReloadPlan(params.changedPaths, {
previousConfig: params.previousConfig,
candidateConfig: params.nextConfig
});
if (isNoopGatewayReloadPlan(plan)) return {
requiresRestart: false,
scheduleDirectRestart: false
};
if (reloadSettings.mode === "off") return {
requiresRestart: true,
scheduleDirectRestart: true
};
if (plan.restartGateway) return {
requiresRestart: true,
scheduleDirectRestart: false
};
return {
requiresRestart: false,
scheduleDirectRestart: false
};
}
/** Returns whether a managed config write can settle without restarting the Gateway. */
function shouldAwaitGatewayConfigApplication(params) {
return !resolveConfigRestartRequirement(params).requiresRestart;
}
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,
requiresRestart: params.requiresRestart
}
};
}
async function tryWriteRestartSentinelPayload(payload) {
try {
await writeRestartSentinel(payload);
return true;
} catch {
return false;
}
}
/** Persists a gateway config write and returns follow-up work that must run after response. */
async function commitGatewayConfigWrite(params) {
const application = params.awaitRuntimeApplication ? createRuntimeConfigWriteApplication(captureGatewayRootWorkAdmissionContinuationScope()?.run) : void 0;
holdGatewayPolicyResponse(params.respond);
const result = await replaceConfigFile({
nextConfig: params.nextConfig,
baseHash: resolveConfigSnapshotHash(params.snapshot) ?? void 0,
writeOptions: attachRuntimeConfigWriteApplication({
...params.writeOptions,
auditOrigin: "config-rpc",
runtimeRefresh: {
...params.writeOptions.runtimeRefresh,
includeAuthStoreRefs: false
}
}, application),
afterWrite: { mode: "auto" }
});
invalidateConfigGetResponseCache();
return {
path: resolveGatewayConfigPath(params.snapshot),
config: result.nextConfig,
hash: result.persistedHash,
...application ? { application: application.claimed ? application.result : Promise.resolve("unclaimed") } : {},
queueFollowUp: () => {
if (!application?.claimed) queueMicrotask(() => {
params.context?.enforceSharedGatewayAuthGenerationForConfigWrite?.(result.nextConfig);
if (params.disconnectSharedAuthClients) params.context?.disconnectClientsUsingSharedGatewayAuth?.();
});
}
};
}
/** 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 restartRequirement = resolveConfigRestartRequirement({
changedPaths: params.changedPaths,
previousConfig: params.previousConfig,
nextConfig: params.nextConfig
});
const payload = buildConfigRestartSentinelPayload({
kind: params.kind,
mode: params.mode,
configPath: params.configPath,
requiresRestart: restartRequirement.requiresRestart,
sessionKey,
deliveryContext,
threadId,
note
});
const sentinelPersisted = await tryWriteRestartSentinelPayload(payload);
const restart = restartRequirement.scheduleDirectRestart ? 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,
sentinelPersisted,
restart
};
}
//#endregion
//#region src/gateway/server-methods/config.ts
const MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE = 3;
const HASHLESS_PATCH_LWW_PATH_PREFIXES = ["ui.prefs"];
let configSchemaResponseCache = null;
function requireConfigBaseHash(params, snapshot, respond, revisionProjector) {
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 !== revisionProjector.projectRawHash(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 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)) {
const path = formatConfigPatchPath(params.path ?? "", key);
if (!isMergePatchObjectKeyAllowed(key, params.path)) continue;
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 isConfigPatchObjectWithStringId(value) {
return isPlainObject(value) && typeof value.id === "string" && value.id.length > 0;
}
function assertNoDuplicateConfigPatchIds(params) {
const path = params.path ?? "";
if (Array.isArray(params.patch)) {
if (!Array.isArray(params.current) || params.replacePaths.has(path) || !isConfigPatchIdKeyedArray(params.current)) return;
const currentIds = /* @__PURE__ */ new Set();
for (const entry of params.current) {
if (currentIds.has(entry.id)) throw new Error(`Cannot ID-merge array at ${path || "<root>"}: current config contains duplicate ID ${entry.id}; use replacePaths for an explicit replacement.`);
currentIds.add(entry.id);
}
const ids = /* @__PURE__ */ new Set();
for (const entry of params.patch) {
if (!isConfigPatchObjectWithStringId(entry)) continue;
if (ids.has(entry.id)) throw new Error(`Ambiguous duplicate ID ${entry.id} in array at ${path || "<root>"}.`);
ids.add(entry.id);
}
const currentById = new Map(params.current.map((entry) => [entry.id, entry]));
for (const entry of params.patch) {
if (!isConfigPatchObjectWithStringId(entry)) continue;
const currentEntry = currentById.get(entry.id);
if (currentEntry) assertNoDuplicateConfigPatchIds({
patch: entry,
current: currentEntry,
replacePaths: params.replacePaths,
path: `${path}[]`
});
}
return;
}
if (!isRecord(params.patch) || !isRecord(params.current)) return;
for (const [key, child] of Object.entries(params.patch)) assertNoDuplicateConfigPatchIds({
patch: child,
current: params.current[key],
replacePaths: params.replacePaths,
path: formatConfigPatchPath(path, key)
});
}
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, revisionProjector) {
const result = await readConfigFileSnapshotForWrite();
if (!requireConfigBaseHash(params, result.snapshot, respond, revisionProjector)) 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 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, modelIdNormalizationPolicies) {
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 validatedSubmission = validateSubmittedConfigOrRespond({
candidate: snapshot.valid ? applyMergePatch(projectSourceOntoRuntimeShape(snapshot.resolved, snapshot.config), createMergePatch(snapshot.config, restored.result)) : restored.result,
sourceConfig: snapshot.sourceConfig,
modelIdNormalizationPolicies,
respond
});
if (!validatedSubmission) return null;
return {
config: validatedSubmission.config,
writeConfig: validatedSubmission.validationCandidate,
schema
};
}
function listExplicitAgentRosterIds(config) {
const roster = readAgentRosterProperty(config);
if (roster?.kind === "entries" && isRecord(roster.value)) return Object.keys(roster.value);
if (roster?.kind !== "list" || !Array.isArray(roster.value)) return [];
return roster.value.flatMap((entry) => isRecord(entry) && typeof entry.id === "string" ? [entry.id] : []);
}
function rejectDroppedAgentRosterEntries(params) {
const submittedIds = new Set(listExplicitAgentRosterIds(params.submittedConfig).map((agentId) => normalizeAgentId(agentId)));
const droppedIds = listExplicitAgentRosterIds(params.currentConfig).filter((agentId) => !submittedIds.has(normalizeAgentId(agentId))).toSorted();
if (droppedIds.length === 0) return false;
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `config.set would remove existing agent entries: ${droppedIds.join(", ")}. Use the agents.delete RPC or \`openclaw agents delete <id>\` for intentional deletion.`));
return true;
}
/** Shared normalize -> raw-validate -> plugin-validate pipeline for submitted configs; responds on failure. */
function validateSubmittedConfigOrRespond(params) {
const validationCandidate = normalizeSubmittedConfigModelRefs(stripBundledProviderRuntimeDefaults({
candidate: params.candidate,
sourceConfig: params.sourceConfig
}), params.modelIdNormalizationPolicies);
const respondInvalid = (issues) => {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(issues), { details: { issues } }));
};
const sourceValidated = validateConfigObjectRawWithPlugins(validationCandidate);
if (!sourceValidated.ok) {
respondInvalid(sourceValidated.issues);
return null;
}
const validated = validateConfigObjectWithPlugins(validationCandidate);
if (!validated.ok) {
respondInvalid(validated.issues);
return null;
}
return {
validationCandidate,
config: validated.config
};
}
function summarizeConfigValidationIssues(issues) {
const trimmed = issues.slice(0, MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE);
const lines = normalizeStringEntries(formatConfigIssueLines(trimmed, "", { 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 {
const snapshot = await prepareSecretsRuntimeSnapshot({
config: params.config,
includeAuthStoreRefs: false,
allowUnavailableSecretOwners: true
});
for (const owner of snapshot.degradedOwners ?? []) {
const reason = redactSecretDegradationReason(owner.reason);
if (!isRetryableSecretDegradationReason(reason)) throw new Error(reason);
}
return snapshot;
} 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 listPreparedSecretDegradations(snapshot) {
return (snapshot.degradedOwners ?? []).map((owner) => ({
ownerKind: owner.ownerKind,
ownerId: owner.ownerId,
state: owner.degradationState ?? "cold",
paths: [...owner.paths],
reason: redactSecretDegradationReason(owner.reason)
}));
}
function preparedSecretDegradationPayload(snapshot) {
const degradedSecretOwners = listPreparedSecretDegradations(snapshot);
return degradedSecretOwners.length > 0 ? { degradedSecretOwners } : {};
}
function clearConfigSchemaResponseCache() {
configSchemaResponseCache = null;
}
async function respondWithConfigRestartWrite(params) {
if (params.writeResult.application) {
const outcome = await params.writeResult.application;
if (outcome !== "applied") {
const message = outcome === "applied-restart-required" ? `${params.mode} persisted and updated the active Gateway, but a recovery restart is required; wait for the Gateway to restart, then run config.get to confirm the active revision` : outcome === "restart-pending" ? `${params.mode} persisted and was accepted for restart; wait for the Gateway to restart, then run config.get to confirm the active revision` : `${params.mode} persisted but was not applied to the active Gateway (${outcome}); run config.get, then use config.apply to reapply the saved config or restart the Gateway`;
params.respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, message));
params.writeResult.queueFollowUp();
return;
}
}
clearConfigSchemaResponseCache();
const { payload, sentinelPersisted, restart } = await resolveGatewayConfigRestartWriteResult({
requestParams: params.requestParams,
kind: params.kind,
mode: params.mode,
configPath: params.writeResult.path,
changedPaths: params.changedPaths,
previousConfig: params.previousConfig,
nextConfig: params.nextConfig,
actor: params.actor,
context: params.context
});
params.respond(true, {
ok: true,
path: params.writeResult.path,
...params.writeResult.hash ? { hash: params.context.configRevisionProjector.projectRawHash(params.writeResult.hash) } : {},
config: redactConfigObject(params.writeResult.config, params.uiHints),
...preparedSecretDegradationPayload(params.preparedSecretsSnapshot),
restart,
sentinel: {
persisted: sentinelPersisted,
payload
}
}, void 0);
params.writeResult.queueFollowUp();
}
function shouldDisconnectSharedAuthClientsForConfigWrite(params) {
return didSharedGatewayAuthChange(params.prevConfig, params.nextConfig) || didActiveSharedGatewayAuthChange({
fallbackPrev: params.prevConfig,
fallbackSource: params.prevSourceConfig,
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 pluginRegistryVersion = getActivePluginRegistryVersion();
if (configSchemaResponseCache && configSchemaResponseCache.pluginRegistryVersion === pluginRegistryVersion) return configSchemaResponseCache.response;
const response = loadGatewayRuntimeConfigSchema();
configSchemaResponseCache = {
pluginRegistryVersion,
response
};
return response;
}
async function commitGatewayConfigWriteOrRespond(params) {
try {
return await commitGatewayConfigWrite(params);
} catch (error) {
if (!(error instanceof ConfigMutationConflictError)) throw error;
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.retryable ? `${error.message}; re-run config.get and retry` : error.message));
return null;
}
}
function isHashlessPatchLwwPath(path) {
return HASHLESS_PATCH_LWW_PATH_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}.`));
}
function hasHashlessPatchLwwStructure(patch) {
return HASHLESS_PATCH_LWW_PATH_PREFIXES.every((prefix) => {
let node = patch;
for (const segment of prefix.split(".")) {
if (!isPlainObject(node)) return false;
if (!Object.hasOwn(node, segment)) return true;
node = node[segment];
if (!isPlainObject(node)) return false;
}
return true;
});
}
function diffConfigLeafPaths(prev, next, prefix = "") {
if (isPlainObject(prev) || isPlainObject(next)) {
const prevRecord = isPlainObject(prev) ? prev : {};
const nextRecord = isPlainObject(next) ? next : {};
const keys = [.../* @__PURE__ */ new Set([...Object.keys(prevRecord), ...Object.keys(nextRecord)])];
if (keys.length === 0) return isDeepStrictEqual(prev, next) ? [] : [prefix || "<root>"];
return keys.flatMap((key) => diffConfigLeafPaths(prevRecord[key], nextRecord[key], prefix ? `${prefix}.${key}` : key));
}
return diffConfigPaths(prev, next, prefix);
}
const configHandlers = {
"config.get": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateConfigGetParams, "config.get", respond)) return;
respond(true, await readConfigGetResponse({
getHotReloadStatus: context.getConfigReloaderHotReloadStatus,
loadUiHints: () => loadSchemaWithPlugins().uiHints,
revisionProjector: context.configRevisionProjector
}), 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 schema = loadSchemaWithPlugins();
const result = lookupConfigSchema(schema, 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 ${sanitizePathForLog(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, context.configRevisionProjector);
if (!writeSnapshot) return;
const { snapshot, writeOptions } = writeSnapshot;
const parsed = parseValidateConfigFromRawOrRespond(params, "config.set", snapshot, respond, writeOptions.basePluginMetadataSnapshot?.owners.modelIdNormalizationPolicies);
if (!parsed) return;
if (rejectDroppedAgentRosterEntries({
currentConfig: snapshot.config,
submittedConfig: parsed.config,
respond
})) return;
const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({
config: parsed.config,
respond
});
if (!preparedSecretsSnapshot) return;
const writeResult = await commitGatewayConfigWriteOrRespond({
snapshot,
writeOptions,
nextConfig: parsed.writeConfig,
context,
respond
});
if (!writeResult) return;
clearConfigSchemaResponseCache();
respond(true, {
ok: true,
path: writeResult.path,
...writeResult.hash ? { hash: context.configRevisionProjector.projectRawHash(writeResult.hash) } : {},
config: redactConfigObject(writeResult.config, parsed.schema.uiHints),
...preparedSecretDegradationPayload(preparedSecretsSnapshot)
}, void 0);
writeResult.queueFollowUp();
},
"config.patch": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateConfigPatchParams, "config.patch", respond)) return;
const hashlessPatch = resolveBaseHashParam(params) === null;
const writeSnapshot = hashlessPatch ? await readConfigFileSnapshotForWrite() : await readConfigWriteSnapshotOrRespond(params, respond, context.configRevisionProjector);
if (!writeSnapshot) return;
const { snapshot, writeOptions } = writeSnapshot;
const modelIdNormalizationPolicies = writeOptions.basePluginMetadataSnapshot?.owners.modelIdNormalizationPolicies;
if (!snapshot.valid) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `${summarizeConfigValidationIssues(snapshot.issues)}; fix (openclaw doctor) before patching`, { details: { issues: snapshot.issues } }));
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 normalizedPatch = normalizeSubmittedConfigModelRefs(parsedRes.parsed, modelIdNormalizationPolicies);
if (hashlessPatch && !hasHashlessPatchLwwStructure(normalizedPatch)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "config base hash required; re-run config.get and retry"));
return;
}
const replacePaths = readConfigPatchReplacePaths(params);
try {
assertNoDuplicateConfigPatchIds({
patch: normalizedPatch,
current: snapshot.config,
replacePaths
});
} catch (error) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, formatErrorMessage(error)));
return;
}
const sourceConfig = normalizeSubmittedConfigModelRefs(snapshot.sourceConfig, modelIdNormalizationPolicies);
const mergedSource = applyMergePatch(sourceConfig, normalizedPatch, {
mergeObjectArraysById: true,
replaceArrayPaths: replacePaths
});
const merged = applyMergePatch(snapshot.config, createMergePatch(sourceConfig, mergedSource));
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: normalizedPatch,
replacePaths,
respond
})) return;
const restoredChangedPaths = diffConfigLeafPaths(snapshot.config, restoredMerge.result);
if (hashlessPatch && !restoredChangedPaths.every(isHashlessPatchLwwPath)) {
const guardedPaths = restoredChangedPaths.filter((path) => !isHashlessPatchLwwPath(path));
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `config base hash required for ${guardedPaths.join(", ")}; re-run config.get and retry with baseHash`));
return;
}
const actor = resolveControlPlaneActor(client);
if (restoredChangedPaths.length === 0) {
respondConfigPatchNoop({
snapshot,
config: snapshot.config,
uiHints: schemaPatch.uiHints,
actor,
context,
respond
});
return;
}
const validatedSubmission = validateSubmittedConfigOrRespond({
candidate: restoredMerge.result,
sourceConfig: snapshot.sourceConfig,
modelIdNormalizationPolicies,
respond
});
if (!validatedSubmission) return;
const writeConfig = validatedSubmission.validationCandidate;
const validatedConfig = validatedSubmission.config;
const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({
config: validatedConfig,
respond
});
if (!preparedSecretsSnapshot) return;
const changedPaths = diffGatewayReloadPaths(snapshot.config, validatedConfig, listConfigReloadRefinementPrefixes());
if (changedPaths.length === 0) {
respondConfigPatchNoop({
snapshot,
config: validatedConfig,
uiHints: schemaPatch.uiHints,
actor,
context,
respond
});
return;
}
context?.logGateway?.info(`config.patch write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.patch`);
const writeResult = await commitGatewayConfigWriteOrRespond({
snapshot,
writeOptions,
nextConfig: writeConfig,
context,
disconnectSharedAuthClients: shouldDisconnectSharedAuthClientsForConfigWrite({
prevConfig: snapshot.config,
prevSourceConfig: snapshot.sourceConfig,
nextConfig: validatedConfig,
preparedSecretsSnapshot
}),
awaitRuntimeApplication: shouldAwaitGatewayConfigApplication({
changedPaths,
previousConfig: snapshot.config,
nextConfig: validatedConfig
}),
respond
});
if (!writeResult) return;
await respondWithConfigRestartWrite({
requestParams: params,
kind: "config-patch",
mode: "config.patch",
writeResult,
changedPaths,
previousConfig: snapshot.config,
nextConfig: validatedConfig,
actor,
context,
respond,
uiHints: schemaPatch.uiHints,
preparedSecretsSnapshot
});
},
"config.apply": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateConfigApplyParams, "config.apply", respond)) return;
const writeSnapshot = await readConfigWriteSnapshotOrRespond(params, respond, context.configRevisionProjector);
if (!writeSnapshot) return;
const { snapshot, writeOptions } = writeSnapshot;
const parsed = parseValidateConfigFromRawOrRespond(params, "config.apply", snapshot, respond, writeOptions.basePluginMetadataSnapshot?.owners.modelIdNormalizationPolicies);
if (!parsed) return;
const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({
config: parsed.config,
respond
});
if (!preparedSecretsSnapshot) return;
const changedPaths = diffGatewayReloadPaths(snapshot.config, parsed.config, listConfigReloadRefinementPrefixes());
const actor = resolveControlPlaneActor(client);
context?.logGateway?.info(`config.apply write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.apply`);
const disconnectSharedAuthClients = shouldDisconnectSharedAuthClientsForConfigWrite({
prevConfig: snapshot.config,
prevSourceConfig: snapshot.sourceConfig,
nextConfig: parsed.config,
preparedSecretsSnapshot
});
const writeResult = await commitGatewayConfigWriteOrRespond({
snapshot,
writeOptions,
nextConfig: parsed.writeConfig,
context,
disconnectSharedAuthClients,
awaitRuntimeApplication: shouldAwaitGatewayConfigApplication({
changedPaths,
previousConfig: snapshot.config,
nextConfig: parsed.config
}),
respond
});
if (!writeResult) return;
await respondWithConfigRestartWrite({
requestParams: params,
kind: "config-apply",
mode: "config.apply",
writeResult,
changedPaths,
previousConfig: snapshot.config,
nextConfig: parsed.config,
actor,
context,
respond,
uiHints: parsed.schema.uiHints,
preparedSecretsSnapshot
});
},
"config.openFile": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateConfigGetParams, "config.openFile", respond)) return;
const configPath = createConfigIO().configPath;
try {
await execOpenPath(resolveOpenPathCommand(configPath));
respond(true, {
ok: true,
path: configPath
}, void 0);
} catch (error) {
const errorMessage = formatOpenPathError(error);
const detailedError = isHeadlessOpenPathError(errorMessage) ? `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=${sanitizePathForLog(configPath)}: ${errorMessage}`);
respond(true, {
ok: false,
path: configPath,
error: detailedError
}, void 0);
}
}
};
//#endregion
export { configHandlers };