UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

376 lines (375 loc) • 21.3 kB
import "./src-SskUVPJG.mjs"; import { t as expectDefined } from "./expect-lbe3Hgrh.mjs"; import { X as resolveConfigEnvVars, Y as containsEnvVarReference, Z as scanEnvTemplateTokens } from "./redact-BEIE6NCT.mjs"; import { t as isPlainObject } from "./plain-object-BFvGATnd.mjs"; import { isDeepStrictEqual } from "node:util"; //#region src/config/env-preserve-authored.ts function containsAuthoredEnvTemplate(value, matches) { if (typeof value === "string") return matches(value); return (Array.isArray(value) ? value : isPlainObject(value) ? Object.values(value) : []).some((item) => containsAuthoredEnvTemplate(item, matches)); } function containsAuthoredUnescapedEnvTemplate(value) { return containsAuthoredEnvTemplate(value, containsEnvVarReference); } function containsAuthoredEscapedEnvTemplate(value) { return containsAuthoredEnvTemplate(value, (text) => scanEnvTemplateTokens(text).some((ref) => ref.kind === "escaped")); } /** * Keyed by bare variable name: `${VAR}` and `${VAR:-x}` are one identity, so changing * an authored `$${VAR}` literal into either active reference is rejected. */ function countAuthoredEnvRefsByPath(value, kind) { const countsByName = /* @__PURE__ */ new Map(); const visit = (item, path) => { if (typeof item === "string") { for (const ref of scanEnvTemplateTokens(item)) if (ref.kind === kind) { const pathCounts = countsByName.get(ref.name) ?? /* @__PURE__ */ new Map(); const pathKey = JSON.stringify(path); pathCounts.set(pathKey, (pathCounts.get(pathKey) ?? 0) + 1); countsByName.set(ref.name, pathCounts); } return; } if (Array.isArray(item)) { item.forEach((child, index) => visit(child, [...path, String(index)])); return; } if (isPlainObject(item)) Object.entries(item).forEach(([key, child]) => visit(child, [...path, key])); }; visit(value, []); return countsByName; } function countResolvedActiveEnvRefsByPath(incoming, parsed, resolved) { const countsByName = /* @__PURE__ */ new Map(); const visit = (incomingItem, parsedItem, resolvedItem, path) => { if (typeof incomingItem === "string" && typeof parsedItem === "string") { if (!isDeepStrictEqual(incomingItem, resolvedItem)) return; for (const ref of scanEnvTemplateTokens(parsedItem)) if (ref.kind === "substitution") { const pathCounts = countsByName.get(ref.name) ?? /* @__PURE__ */ new Map(); const pathKey = JSON.stringify(path); pathCounts.set(pathKey, (pathCounts.get(pathKey) ?? 0) + 1); countsByName.set(ref.name, pathCounts); } return; } if (Array.isArray(incomingItem) && Array.isArray(parsedItem) && Array.isArray(resolvedItem)) { parsedItem.forEach((child, index) => visit(incomingItem[index], child, resolvedItem[index], [...path, String(index)])); return; } if (isPlainObject(incomingItem) && isPlainObject(parsedItem) && isPlainObject(resolvedItem)) Object.entries(parsedItem).forEach(([key, child]) => visit(incomingItem[key], child, resolvedItem[key], [...path, key])); }; visit(incoming, parsed, resolved, []); return countsByName; } function containsUnaccountedActiveEscapedEnvRef(incoming, escapedParsed, matchedIncoming, matchedParsed, matchedResolved, explicitSetPaths) { const escapedCounts = countAuthoredEnvRefsByPath(escapedParsed, "escaped"); const incomingActiveCounts = countAuthoredEnvRefsByPath(incoming, "substitution"); const incomingEscapedCounts = countAuthoredEnvRefsByPath(incoming, "escaped"); const matchedActiveCounts = countResolvedActiveEnvRefsByPath(matchedIncoming, matchedParsed, matchedResolved); const matchedEscapedCounts = countAuthoredEnvRefsByPath(matchedParsed, "escaped"); return [...escapedCounts].some(([name, escapedPathCounts]) => { const isExplicitActivation = (path) => { if (!escapedPathCounts.has(path) || !explicitSetPaths?.length) return false; const segments = JSON.parse(path); return explicitSetPaths.some((supplied) => supplied.every((segment, index) => segments[index] === segment)); }; return [...incomingActiveCounts.get(name) ?? /* @__PURE__ */ new Map()].some(([path, count]) => !isExplicitActivation(path) && count > (matchedActiveCounts.get(name)?.get(path) ?? 0)) || [...escapedPathCounts.keys()].some((path) => { const incomingActiveCount = incomingActiveCounts.get(name)?.get(path) ?? 0; return !isExplicitActivation(path) && incomingActiveCount > 0 && (incomingEscapedCounts.get(name)?.get(path) ?? 0) < (matchedEscapedCounts.get(name)?.get(path) ?? 0); }); }); } function preservesAuthoredEscapedEnvRefs(incoming, parsed) { const parsedEscapedCounts = countAuthoredEnvRefsByPath(parsed, "escaped"); const incomingEscapedCounts = countAuthoredEnvRefsByPath(incoming, "escaped"); return [...parsedEscapedCounts].every(([name, parsedPathCounts]) => [...parsedPathCounts].every(([path, count]) => (incomingEscapedCounts.get(name)?.get(path) ?? 0) >= count)); } //#endregion //#region src/config/env-preserve.ts var EnvRefArrayMutationError = class extends Error { constructor() { super("Config write would reorder or modify an array containing environment references."); this.name = "EnvRefArrayMutationError"; } }; /** * Check if a string contains any `${VAR}` env var references, escaped or not. * * Escaped `$${VAR}` counts: it still changes under substitution, so the authored text * must be restored on write-back the same way an active reference is. */ function hasEnvVarRef(value) { return scanEnvTemplateTokens(value).length > 0; } function getArrayIdentityPathValue(value, path) { let current = value; for (const segment of path) { if (!isPlainObject(current)) return; current = current[segment]; } return current; } function resolveStableArrayIdentityMatch(params) { const parsedItem = params.parsed[params.parsedIndex]; if (!isPlainObject(parsedItem)) return { kind: "none" }; const identityKey = ["id", "agentId"].find((key) => typeof parsedItem[key] === "string" && !hasEnvVarRef(parsedItem[key])); if (!identityKey) return { kind: "none" }; const identityValue = parsedItem[identityKey]; const matchesIdentity = (item) => isPlainObject(item) && isDeepStrictEqual(item[identityKey], identityValue); if (params.parsed.filter(matchesIdentity).length !== 1) return { kind: "none" }; const incomingMatches = params.incoming.flatMap((item, index) => matchesIdentity(item) ? [index] : []); return incomingMatches.length === 1 ? { kind: "match", incomingIndex: expectDefined(incomingMatches[0], "env preserve identity match") } : { kind: "invalid" }; } function collectLiteralArrayIdentityPaths(value, path = []) { if (typeof value === "string") return hasEnvVarRef(value) ? [] : [path]; if (!isPlainObject(value)) return []; return Object.entries(value).flatMap(([key, child]) => collectLiteralArrayIdentityPaths(child, [...path, key])); } function hasStableSameIndexLiteralShape(params) { if (params.incoming.length !== params.parsed.length) return false; const parsedItem = params.parsed[params.parsedIndex]; const literalPaths = collectLiteralArrayIdentityPaths(parsedItem); if (literalPaths.length === 0 || literalPaths.some((identityPath) => { const identityValue = getArrayIdentityPathValue(parsedItem, identityPath); return !isDeepStrictEqual(getArrayIdentityPathValue(params.incoming[params.parsedIndex], identityPath), identityValue); })) return false; return literalPaths.some((identityPath) => { const identityValue = getArrayIdentityPathValue(parsedItem, identityPath); const authoredCount = params.parsed.filter((item) => isDeepStrictEqual(getArrayIdentityPathValue(item, identityPath), identityValue)).length; const incomingCount = params.incoming.filter((item) => isDeepStrictEqual(getArrayIdentityPathValue(item, identityPath), identityValue)).length; return authoredCount === 1 && incomingCount === 1; }); } function matchesArrayElementAtSameIndex(incoming, parsed, resolved) { return isDeepStrictEqual(incoming, parsed) || isDeepStrictEqual(incoming, resolved); } function matchesRetainedArrayItem(params) { if (matchesArrayElementAtSameIndex(params.incoming[params.incomingIndex], params.parsed[params.parsedIndex], params.resolved[params.parsedIndex])) return true; const stableIdentity = resolveStableArrayIdentityMatch({ incoming: params.incoming, parsed: params.parsed, parsedIndex: params.parsedIndex }); return stableIdentity.kind === "match" && stableIdentity.incomingIndex === params.incomingIndex; } function hasStableSameIndexNeighbors(params) { return params.incoming.length === params.parsed.length && params.parsed.every((item, index) => index === params.parsedIndex || matchesArrayElementAtSameIndex(params.incoming[index], item, params.resolved[index])); } function canMatchEditedArrayItemAtSameIndex(params) { return params.incoming.length === 1 && params.parsed.length === 1 || hasStableSameIndexLiteralShape(params) || hasStableSameIndexNeighbors(params); } function matchUniqueRetainedArrayItems(params) { if (params.incoming.length >= params.parsed.length) return; const earliestParsedIndexes = []; let nextParsedIndex = 0; for (let incomingIndex = 0; incomingIndex < params.incoming.length; incomingIndex += 1) { const parsedIndex = params.parsed.findIndex((_parsedItem, index) => index >= nextParsedIndex && matchesRetainedArrayItem({ ...params, incomingIndex, parsedIndex: index })); if (parsedIndex < 0) return; earliestParsedIndexes.push(parsedIndex); nextParsedIndex = parsedIndex + 1; } const latestParsedIndexes = Array.from({ length: params.incoming.length }, () => 0); nextParsedIndex = params.parsed.length - 1; for (let incomingIndex = params.incoming.length - 1; incomingIndex >= 0; incomingIndex -= 1) { let parsedIndex = nextParsedIndex; while (parsedIndex >= 0 && !matchesRetainedArrayItem({ ...params, incomingIndex, parsedIndex })) parsedIndex -= 1; if (parsedIndex < 0) return; latestParsedIndexes[incomingIndex] = parsedIndex; nextParsedIndex = parsedIndex - 1; } if (!isDeepStrictEqual(earliestParsedIndexes, latestParsedIndexes)) return; return new Map(earliestParsedIndexes.map((parsedIndex, incomingIndex) => [parsedIndex, incomingIndex])); } function matchAuthoredTemplateArrayItems(params) { const templateIndexes = params.parsed.flatMap((item, index) => containsAuthoredUnescapedEnvTemplate(item) ? [index] : []); if (params.incoming.length === params.parsed.length && params.incoming.every((item, index) => matchesArrayElementAtSameIndex(item, params.parsed[index], params.resolved[index]))) return new Map(templateIndexes.map((index) => [index, index])); const retainedDeletionMatches = matchUniqueRetainedArrayItems(params); if (retainedDeletionMatches) return new Map(templateIndexes.flatMap((parsedIndex) => { const incomingIndex = retainedDeletionMatches.get(parsedIndex); return incomingIndex === void 0 ? [] : [[parsedIndex, incomingIndex]]; })); const matches = /* @__PURE__ */ new Map(); const usedIncomingIndexes = /* @__PURE__ */ new Set(); const addMatch = (parsedIndex, incomingIndex) => { if (usedIncomingIndexes.has(incomingIndex)) throw new EnvRefArrayMutationError(); matches.set(parsedIndex, incomingIndex); usedIncomingIndexes.add(incomingIndex); }; for (const parsedIndex of templateIndexes) { const parsedItem = params.parsed[parsedIndex]; const stableIdentity = resolveStableArrayIdentityMatch({ incoming: params.incoming, parsed: params.parsed, parsedIndex }); if (stableIdentity.kind !== "none") { if (stableIdentity.kind === "invalid") throw new EnvRefArrayMutationError(); addMatch(parsedIndex, stableIdentity.incomingIndex); continue; } if (parsedIndex < params.incoming.length && matchesArrayElementAtSameIndex(params.incoming[parsedIndex], parsedItem, params.resolved[parsedIndex])) { const precedingItemsRemainAligned = params.parsed.slice(0, parsedIndex).every((item, index) => matchesArrayElementAtSameIndex(params.incoming[index], item, params.resolved[index])); const duplicateAuthoredMatch = params.parsed.some((item, index) => index !== parsedIndex && matchesArrayElementAtSameIndex(params.incoming[parsedIndex], item, params.resolved[index])); const duplicateIncomingMatch = params.incoming.some((item, index) => index !== parsedIndex && matchesArrayElementAtSameIndex(item, parsedItem, params.resolved[parsedIndex])); if (!(params.incoming.length === params.parsed.length || precedingItemsRemainAligned) || duplicateAuthoredMatch || duplicateIncomingMatch) throw new EnvRefArrayMutationError(); addMatch(parsedIndex, parsedIndex); continue; } if (isPlainObject(parsedItem) || Array.isArray(parsedItem)) { if (!canMatchEditedArrayItemAtSameIndex({ ...params, parsedIndex })) throw new EnvRefArrayMutationError(); addMatch(parsedIndex, parsedIndex); continue; } if (params.incoming.some((item, incomingIndex) => incomingIndex !== parsedIndex && matchesArrayElementAtSameIndex(item, parsedItem, params.resolved[parsedIndex]))) throw new EnvRefArrayMutationError(); if (parsedIndex < params.incoming.length) addMatch(parsedIndex, parsedIndex); } return matches; } function matchAuthoredEscapedTemplateArrayItems(params) { const escapedTemplateIndexes = params.parsed.flatMap((item, index) => containsAuthoredEscapedEnvTemplate(item) && !containsAuthoredUnescapedEnvTemplate(item) ? [index] : []); if (params.incoming.length === params.parsed.length && params.incoming.every((item, index) => matchesArrayElementAtSameIndex(item, params.parsed[index], params.resolved[index]))) return new Map(escapedTemplateIndexes.map((index) => [index, index])); const retainedDeletionMatches = matchUniqueRetainedArrayItems(params); if (retainedDeletionMatches) return new Map(escapedTemplateIndexes.flatMap((parsedIndex) => { const incomingIndex = retainedDeletionMatches.get(parsedIndex); if (incomingIndex === void 0) return []; if (params.usedIncomingIndexes.has(incomingIndex)) throw new EnvRefArrayMutationError(); return [[parsedIndex, incomingIndex]]; })); const matches = /* @__PURE__ */ new Map(); const usedIncomingIndexes = new Set(params.usedIncomingIndexes); const addMatch = (parsedIndex, incomingIndex) => { if (usedIncomingIndexes.has(incomingIndex)) throw new EnvRefArrayMutationError(); matches.set(parsedIndex, incomingIndex); usedIncomingIndexes.add(incomingIndex); }; for (const parsedIndex of escapedTemplateIndexes) { const parsedItem = params.parsed[parsedIndex]; const stableIdentity = resolveStableArrayIdentityMatch({ incoming: params.incoming, parsed: params.parsed, parsedIndex }); if (stableIdentity.kind === "match") { addMatch(parsedIndex, stableIdentity.incomingIndex); continue; } const resolvedItem = params.resolved[parsedIndex]; const incomingMatches = params.incoming.flatMap((item, incomingIndex) => !usedIncomingIndexes.has(incomingIndex) && isDeepStrictEqual(item, resolvedItem) ? [incomingIndex] : []); const authoredMatches = escapedTemplateIndexes.filter((index) => isDeepStrictEqual(params.resolved[index], resolvedItem)); const authoredRepresentationsAreIdentical = authoredMatches.every((index) => isDeepStrictEqual(params.parsed[index], parsedItem)); if (incomingMatches.length > 0 && incomingMatches.length <= authoredMatches.length && authoredRepresentationsAreIdentical) { const sameIndexMatch = incomingMatches.includes(parsedIndex) ? parsedIndex : incomingMatches[0]; addMatch(parsedIndex, expectDefined(sameIndexMatch, "env preserve same index match")); continue; } if (incomingMatches.length > 0) throw new EnvRefArrayMutationError(); if (isPlainObject(parsedItem) || Array.isArray(parsedItem)) { if (stableIdentity.kind === "none" && parsedIndex < params.incoming.length && !usedIncomingIndexes.has(parsedIndex) && canMatchEditedArrayItemAtSameIndex({ ...params, parsedIndex })) { addMatch(parsedIndex, parsedIndex); continue; } } } return matches; } function resolveEnvVarRefsForComparison(value, env) { if (typeof value === "string") return hasEnvVarRef(value) ? resolveConfigEnvVars(value, env, { onMissing: () => {} }) : value; if (Array.isArray(value)) return value.map((item) => resolveEnvVarRefsForComparison(item, env)); if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveEnvVarRefsForComparison(item, env)])); return value; } /** * Deep-walk the incoming config and restore `${VAR}` references from the * pre-substitution parsed config wherever the resolved value matches. * * @param incoming - The resolved config about to be written * @param parsed - The pre-substitution parsed config (from the current file on disk) * @param env - Environment variables for verification * @returns A new config object with env var references restored where appropriate */ function restoreEnvVarRefs(incoming, parsed, env = process.env) { return restoreEnvVarRefsFromResolved(incoming, parsed, resolveEnvVarRefsForComparison(parsed, env)); } /** Restore only references owned by the matching authored/resolved planning read. */ function restoreEnvVarRefsFromResolved(incoming, parsed, resolved, explicitSetPaths) { if (parsed === null || parsed === void 0) return incoming; if (typeof incoming === "string" && typeof parsed === "string") { if (hasEnvVarRef(incoming) && explicitSetPaths?.some((path) => path.length === 0)) return incoming; return hasEnvVarRef(parsed) && resolved === incoming ? parsed : incoming; } const childExplicitPaths = (key) => explicitSetPaths?.flatMap((path) => path.length === 0 ? [path] : path[0] === key ? [path.slice(1)] : []); if (Array.isArray(incoming) && Array.isArray(parsed) && Array.isArray(resolved)) { if (!containsAuthoredUnescapedEnvTemplate(parsed) && !containsAuthoredEscapedEnvTemplate(parsed)) return incoming.map((item, index) => index < parsed.length ? restoreEnvVarRefsFromResolved(item, parsed[index], resolved[index], childExplicitPaths(String(index))) : item); const unescapedMatches = matchAuthoredTemplateArrayItems({ incoming, parsed, resolved }); const escapedMatches = matchAuthoredEscapedTemplateArrayItems({ incoming, parsed, resolved, usedIncomingIndexes: new Set(unescapedMatches.values()) }); const matches = new Map([...unescapedMatches, ...escapedMatches]); const next = [...incoming]; const matchedIncomingIndexes = new Set(matches.values()); for (const [parsedIndex, incomingIndex] of matches) next[incomingIndex] = restoreEnvVarRefsFromResolved(incoming[incomingIndex], parsed[parsedIndex], resolved[parsedIndex], childExplicitPaths(String(incomingIndex))); for (let index = 0; index < incoming.length && index < parsed.length; index += 1) if (!matchedIncomingIndexes.has(index) && !containsAuthoredUnescapedEnvTemplate(parsed[index]) && !containsAuthoredEscapedEnvTemplate(parsed[index])) next[index] = restoreEnvVarRefsFromResolved(incoming[index], parsed[index], resolved[index], childExplicitPaths(String(index))); const matchedParsedIndexByIncoming = new Map([...matches].map(([parsedIndex, incomingIndex]) => [incomingIndex, parsedIndex])); for (const [escapedParsedIndex, escapedParsedItem] of parsed.entries()) { if (!containsAuthoredEscapedEnvTemplate(escapedParsedItem)) continue; const matchedIncomingIndex = matches.get(escapedParsedIndex); if (matchedIncomingIndex !== void 0 && preservesAuthoredEscapedEnvRefs(next[matchedIncomingIndex], escapedParsedItem)) continue; const stableIdentity = resolveStableArrayIdentityMatch({ incoming, parsed, parsedIndex: escapedParsedIndex }); if (next.some((item, incomingIndex) => { const matchedParsedIndex = matchedParsedIndexByIncoming.get(incomingIndex); return containsUnaccountedActiveEscapedEnvRef(item, escapedParsedItem, incoming[incomingIndex], matchedParsedIndex === void 0 ? void 0 : parsed[matchedParsedIndex], matchedParsedIndex === void 0 ? void 0 : resolved[matchedParsedIndex], matchedParsedIndex === escapedParsedIndex && stableIdentity.kind === "match" && stableIdentity.incomingIndex === incomingIndex ? childExplicitPaths(String(incomingIndex)) : void 0); })) throw new EnvRefArrayMutationError(); } return next; } if (isPlainObject(incoming) && isPlainObject(parsed) && isPlainObject(resolved)) { const result = {}; for (const [key, value] of Object.entries(incoming)) if (Object.hasOwn(parsed, key)) result[key] = restoreEnvVarRefsFromResolved(value, parsed[key], resolved[key], childExplicitPaths(key)); else result[key] = value; return result; } return incoming; } function resolveWriteEnvSnapshotForPath(params) { if (params.expectedConfigPath === void 0 || params.expectedConfigPath === params.actualConfigPath) return params.envSnapshotForRestore; } //#endregion //#region src/config/io.types.ts const configWriteCommittedSnapshot = Symbol("configWriteCommittedSnapshot"); const configWritePostCommitRollback = Symbol("configWritePostCommitRollback"); var ConfigRuntimeRefreshError = class extends Error { constructor(message, options) { super(message, options); this.name = "ConfigRuntimeRefreshError"; } }; //#endregion export { restoreEnvVarRefs as a, resolveWriteEnvSnapshotForPath as i, configWriteCommittedSnapshot as n, restoreEnvVarRefsFromResolved as o, configWritePostCommitRollback as r, ConfigRuntimeRefreshError as t };