openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
532 lines (531 loc) • 20.8 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import "./utils-P__uGsPB.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { s as coerceSecretRef, v as resolveSecretInputRef } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { f as secretRefKey } from "./ref-contract-D92DqQ-r.js";
import { c as normalizeProviderId } from "./model-ref-shared-Dz7QU0Lx.js";
import { r as listKnownSecretEnvVarNames } from "./provider-env-vars-B8XgJCSL.js";
import { t as isNonEmptyString } from "./shared-CeAc-MzA.js";
import { m as isExpectedResolvedSecretValue, p as hasConfiguredPlaintextSecretValue } from "./runtime-shared-CqbaocQu.js";
import { n as discoverConfigSecretTargets } from "./target-registry-query-qVhyD-Hq.js";
import "./target-registry-BHDUOLUU.js";
import "./config-Cs0XXL3x.js";
import { n as resolveSecretRefValue, r as resolveSecretRefValues } from "./resolve-224YoYfx.js";
import { a as isProviderScopedSecretResolutionError } from "./resolve-errors-C2XRFrol.js";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.js";
import { c as readSecretStoreValue, i as listSecretStoreEntries } from "./secret-store-CC1e6gjb.js";
import { c as resolveSharedMainAuthAgentDir } from "./path-resolve-oRkRBkQd.js";
import { E as listLegacyAuthProfileSources, T as listLegacyAuthProfileArchives, f as readPersistedSharedAuthProfileStoreRaw, u as readPersistedAuthProfileStoreRaw } from "./sqlite-MN_7y26V.js";
import "./legacy-source-diagnostic-BNaogmw1.js";
import { c as isNonSecretApiKeyMarker, u as isSecretRefHeaderValueMarker } from "./model-auth-markers-jBKQn38x.js";
import "./model-selection-di2kjKCB.js";
import { n as selectRefsForExecPolicy, t as getSkippedExecRefStaticError } from "./exec-resolution-policy-B4IeYZE3.js";
import { i as readJsonObjectIfExists, n as listSecretsDotEnvPaths, r as parseEnvAssignmentValue, t as listAgentModelsJsonPaths } from "./storage-scan-BMZL5YXA.js";
import { t as isLikelySensitiveModelProviderHeaderName } from "./model-provider-header-policy-DaO5ykFx.js";
import { n as listAuthProfileStoreTargets, r as iterateAuthProfileCredentials, t as createSecretsConfigIO } from "./config-io-DIBeaezB.js";
import fs from "node:fs";
import os from "node:os";
//#region src/secrets/audit-store.ts
function findSecretStorePlaintextResidueFindings(params) {
const entries = listSecretStoreEntries({
scope: { kind: "team" },
database: params.database
});
if (entries.length === 0 || params.assignments.length === 0) return [];
const namesByValue = /* @__PURE__ */ new Map();
for (const entry of entries) {
const result = readSecretStoreValue({
scope: { kind: "team" },
name: entry.name,
database: params.database
});
if (!result.ok) {
if (result.error.code === "SECRET_STORE_NOT_FOUND") continue;
if (result.error.code === "SECRET_STORE_INVALID_NAME") throw new Error(result.error.message);
throw new Error(result.error.message, { cause: result.error.cause });
}
const names = namesByValue.get(result.value);
if (names) names.push(entry.name);
else namesByValue.set(result.value, [entry.name]);
}
return params.assignments.flatMap((assignment) => (namesByValue.get(assignment.value) ?? []).map((name) => ({
code: "STORE_PLAINTEXT_RESIDUE",
severity: "warn",
file: assignment.file,
jsonPath: assignment.path,
message: `${assignment.path} duplicates team secret store entry "${name}"; replace the plaintext with a store SecretRef.`
})));
}
//#endregion
//#region src/secrets/audit.ts
const REF_RESOLVE_FALLBACK_CONCURRENCY = 8;
const MAX_AUDIT_MODELS_JSON_BYTES = 5242880;
function addFinding(collector, finding) {
collector.findings.push(finding);
}
function collectProviderRefPath(collector, providerId, configPath) {
const key = normalizeProviderId(providerId);
const existing = collector.configProviderRefPaths.get(key);
if (existing) {
existing.push(configPath);
return;
}
collector.configProviderRefPaths.set(key, [configPath]);
}
function trackAuthProviderState(collector, provider, mode) {
const key = normalizeProviderId(provider);
const existing = collector.authProviderState.get(key);
if (existing) {
existing.hasUsableStaticOrOAuth = true;
existing.modes.add(mode);
return;
}
collector.authProviderState.set(key, {
hasUsableStaticOrOAuth: true,
modes: /* @__PURE__ */ new Set([mode])
});
}
function collectEnvPlaintext(params) {
if (!fs.existsSync(params.envPath)) return;
params.collector.filesScanned.add(params.envPath);
const knownKeys = new Set(listKnownSecretEnvVarNames());
const lines = fs.readFileSync(params.envPath, "utf8").split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
if (!match) continue;
const key = match[1] ?? "";
if (!knownKeys.has(key)) continue;
if (!parseEnvAssignmentValue(match[2] ?? "")) continue;
addFinding(params.collector, {
code: "PLAINTEXT_FOUND",
severity: "warn",
file: params.envPath,
jsonPath: `$env.${key}`,
message: `Potential secret found in .env (${key}).`
});
}
}
function collectConfigSecrets(params) {
const defaults = params.config.secrets?.defaults;
for (const target of discoverConfigSecretTargets(params.config, { env: params.env })) {
if (!target.entry.includeInAudit) continue;
const { ref } = resolveSecretInputRef({
value: target.value,
refValue: target.refValue,
defaults
});
const hasPlaintext = hasConfiguredPlaintextSecretValue(target.value, target.entry.expectedResolvedValue);
const isNonSecretHeader = target.entry.id === "models.providers.*.headers.*" && !isLikelySensitiveModelProviderHeaderName(target.pathSegments.at(-1) ?? "");
const isModelMarker = target.entry.id === "models.providers.*.apiKey" && typeof target.value === "string" && isNonSecretApiKeyMarker(target.value);
if (hasPlaintext && !isNonSecretHeader && !isModelMarker && typeof target.value === "string") params.collector.configPlaintextAssignments.push({
file: params.configPath,
path: target.path,
value: target.value
});
if (ref) {
params.collector.refAssignments.push({
file: params.configPath,
path: target.path,
ref,
expected: target.entry.expectedResolvedValue,
provider: target.providerId
});
if (target.entry.trackProviderShadowing && target.providerId) collectProviderRefPath(params.collector, target.providerId, target.path);
continue;
}
if (isNonSecretHeader || isModelMarker || !hasPlaintext) continue;
addFinding(params.collector, {
code: "PLAINTEXT_FOUND",
severity: "warn",
file: params.configPath,
jsonPath: target.path,
message: `${target.path} is stored as plaintext.`,
provider: target.providerId
});
}
}
function collectAuthStoreSecrets(target, collector, defaults) {
const authStorePath = target.path;
if (!fs.existsSync(authStorePath)) return;
const parsed = target.kind === "shared" ? readPersistedSharedAuthProfileStoreRaw(target.env) : readPersistedAuthProfileStoreRaw(target.agentDir);
if (!isRecord(parsed) || !isRecord(parsed.profiles)) return;
collector.filesScanned.add(authStorePath);
for (const entry of iterateAuthProfileCredentials(parsed.profiles)) {
if (entry.kind === "api_key" || entry.kind === "token") {
const { ref } = resolveSecretInputRef({
value: entry.value,
refValue: entry.refValue,
defaults
});
const authoredValueRef = coerceSecretRef(entry.value, defaults);
if (ref) {
collector.refAssignments.push({
file: authStorePath,
path: `profiles.${entry.profileId}.${entry.valueField}`,
ref,
expected: "string",
provider: entry.provider
});
trackAuthProviderState(collector, entry.provider, entry.kind);
}
if (authoredValueRef) continue;
if (isNonEmptyString(entry.value)) {
addFinding(collector, {
code: "PLAINTEXT_FOUND",
severity: "warn",
file: authStorePath,
jsonPath: `profiles.${entry.profileId}.${entry.valueField}`,
message: entry.kind === "api_key" ? "Auth profile API key is stored as plaintext." : "Auth profile token is stored as plaintext.",
provider: entry.provider,
profileId: entry.profileId
});
trackAuthProviderState(collector, entry.provider, entry.kind);
}
continue;
}
if (entry.hasAccess || entry.hasRefresh) {
addFinding(collector, {
code: "LEGACY_RESIDUE",
severity: "info",
file: authStorePath,
jsonPath: `profiles.${entry.profileId}`,
message: "OAuth credentials are present (out of scope for static SecretRef migration).",
provider: entry.provider,
profileId: entry.profileId
});
trackAuthProviderState(collector, entry.provider, "oauth");
}
}
}
function collectModelsJsonSecrets(params) {
if (!fs.existsSync(params.modelsJsonPath)) return;
params.collector.filesScanned.add(params.modelsJsonPath);
const parsedResult = readJsonObjectIfExists(params.modelsJsonPath, {
requireRegularFile: true,
maxBytes: MAX_AUDIT_MODELS_JSON_BYTES
});
if (parsedResult.error) {
addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: params.modelsJsonPath,
jsonPath: "<root>",
message: `Invalid JSON in models.json: ${parsedResult.error}`
});
return;
}
const parsed = parsedResult.value;
if (!parsed || !isRecord(parsed.providers)) return;
for (const [providerId, providerValue] of Object.entries(parsed.providers)) {
if (!isRecord(providerValue)) continue;
const apiKey = providerValue.apiKey;
if (coerceSecretRef(apiKey)) addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: params.modelsJsonPath,
jsonPath: `providers.${providerId}.apiKey`,
message: "models.json contains an unresolved SecretRef object; regenerate models.json.",
provider: providerId
});
else if (isNonEmptyString(apiKey) && !isNonSecretApiKeyMarker(apiKey)) addFinding(params.collector, {
code: "PLAINTEXT_FOUND",
severity: "warn",
file: params.modelsJsonPath,
jsonPath: `providers.${providerId}.apiKey`,
message: "models.json provider apiKey is stored as plaintext.",
provider: providerId
});
const headers = isRecord(providerValue.headers) ? providerValue.headers : void 0;
if (!headers) continue;
for (const [headerKey, headerValue] of Object.entries(headers)) {
const headerPath = `providers.${providerId}.headers.${headerKey}`;
if (coerceSecretRef(headerValue)) {
addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: params.modelsJsonPath,
jsonPath: headerPath,
message: "models.json contains an unresolved SecretRef object for provider headers; regenerate models.json.",
provider: providerId
});
continue;
}
if (!isNonEmptyString(headerValue)) continue;
if (isSecretRefHeaderValueMarker(headerValue)) continue;
if (!isLikelySensitiveModelProviderHeaderName(headerKey)) continue;
addFinding(params.collector, {
code: "PLAINTEXT_FOUND",
severity: "warn",
file: params.modelsJsonPath,
jsonPath: headerPath,
message: "models.json provider header value is stored as plaintext.",
provider: providerId
});
}
}
}
function collectLegacyAuthSourceFindings(params) {
const seen = /* @__PURE__ */ new Set();
const targets = listAuthProfileStoreTargets(params.config, params.stateDir, params.env);
for (const target of targets) {
const agentDir = target.kind === "agent" ? target.agentDir : void 0;
for (const source of listLegacyAuthProfileSources({
agentDir,
env: params.env
})) {
if (seen.has(source.path)) continue;
seen.add(source.path);
addFinding(params.collector, {
code: "LEGACY_RESIDUE",
severity: source.kind === "auth-state" ? "info" : "warn",
file: source.path,
jsonPath: "<root>",
message: `Retired auth source ${source.kind} is present; run openclaw doctor --fix to migrate and archive it.`
});
}
}
const sharedMainDir = resolveSharedMainAuthAgentDir(params.env);
for (const archive of listLegacyAuthProfileArchives({
agentDirs: targets.flatMap((target) => target.kind === "agent" ? [target.agentDir] : []).concat(sharedMainDir),
env: params.env
})) {
if (seen.has(archive.path)) continue;
seen.add(archive.path);
addFinding(params.collector, {
code: "LEGACY_RESIDUE",
severity: "warn",
file: archive.path,
jsonPath: "<root>",
message: `Archived auth source ${archive.kind} may contain plaintext credentials; retain it only as long as recovery requires.`
});
}
}
async function collectUnresolvedRefFindings(params) {
const cache = {};
const refsByProvider = /* @__PURE__ */ new Map();
const skippedRefKeys = /* @__PURE__ */ new Set();
let refsChecked = 0;
let skippedExecRefs = 0;
for (const assignment of params.collector.refAssignments) {
const providerKey = `${assignment.ref.source}:${assignment.ref.provider}`;
let refsForProvider = refsByProvider.get(providerKey);
if (!refsForProvider) {
refsForProvider = /* @__PURE__ */ new Map();
refsByProvider.set(providerKey, refsForProvider);
}
refsForProvider.set(secretRefKey(assignment.ref), assignment.ref);
}
const resolvedByRefKey = /* @__PURE__ */ new Map();
const errorsByRefKey = /* @__PURE__ */ new Map();
for (const refsForProvider of refsByProvider.values()) {
const refs = [...refsForProvider.values()];
const selectedRefs = selectRefsForExecPolicy({
refs,
allowExec: params.allowExec
});
if (selectedRefs.skippedExecRefs.length > 0) {
skippedExecRefs += selectedRefs.skippedExecRefs.length;
for (const ref of selectedRefs.skippedExecRefs) {
skippedRefKeys.add(secretRefKey(ref));
const staticError = getSkippedExecRefStaticError({
ref,
config: params.config
});
if (staticError) errorsByRefKey.set(secretRefKey(ref), new Error(staticError));
}
}
if (selectedRefs.refsToResolve.length === 0) continue;
refsChecked += selectedRefs.refsToResolve.length;
const provider = refs[0]?.provider;
try {
const resolved = await resolveSecretRefValues(selectedRefs.refsToResolve, {
config: params.config,
env: params.env,
cache
});
for (const [key, value] of resolved.entries()) resolvedByRefKey.set(key, value);
continue;
} catch (err) {
if (provider && isProviderScopedSecretResolutionError(err)) {
for (const ref of selectedRefs.refsToResolve) errorsByRefKey.set(secretRefKey(ref), err);
continue;
}
}
const tasks = selectedRefs.refsToResolve.map((ref) => async () => ({
key: secretRefKey(ref),
resolved: await resolveSecretRefValue(ref, {
config: params.config,
env: params.env,
cache
})
}));
const fallback = await runTasksWithConcurrency({
tasks,
limit: Math.min(REF_RESOLVE_FALLBACK_CONCURRENCY, selectedRefs.refsToResolve.length),
errorMode: "continue",
onTaskError: (error, index) => {
const ref = selectedRefs.refsToResolve[index];
if (!ref) return;
errorsByRefKey.set(secretRefKey(ref), error);
}
});
for (const result of fallback.results) {
if (!result) continue;
resolvedByRefKey.set(result.key, result.resolved);
}
}
for (const assignment of params.collector.refAssignments) {
const key = secretRefKey(assignment.ref);
if (skippedRefKeys.has(key) && !errorsByRefKey.has(key)) continue;
const resolveErr = errorsByRefKey.get(key);
if (resolveErr) {
addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: assignment.file,
jsonPath: assignment.path,
message: `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (${formatErrorMessage(resolveErr)}).`,
provider: assignment.provider
});
continue;
}
if (!resolvedByRefKey.has(key)) {
addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: assignment.file,
jsonPath: assignment.path,
message: `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is missing).`,
provider: assignment.provider
});
continue;
}
const resolved = resolvedByRefKey.get(key);
if (!isExpectedResolvedSecretValue(resolved, assignment.expected)) addFinding(params.collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: assignment.file,
jsonPath: assignment.path,
message: assignment.expected === "string" ? `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is not a non-empty string).` : `Failed to resolve ${assignment.ref.source}:${assignment.ref.provider}:${assignment.ref.id} (resolved value is not a string/object).`,
provider: assignment.provider
});
}
return {
refsChecked,
skippedExecRefs
};
}
function collectShadowingFindings(collector) {
for (const [provider, paths] of collector.configProviderRefPaths.entries()) {
const authState = collector.authProviderState.get(provider);
if (!authState?.hasUsableStaticOrOAuth) continue;
const modeText = [...authState.modes].join("/");
for (const configPath of paths) addFinding(collector, {
code: "REF_SHADOWED",
severity: "warn",
file: "openclaw.json",
jsonPath: configPath,
message: `Auth profile credentials (${modeText}) take precedence for provider "${provider}", so this config ref may never be used.`,
provider
});
}
}
function summarizeFindings(findings) {
return {
plaintextCount: findings.filter((entry) => entry.code === "PLAINTEXT_FOUND").length,
unresolvedRefCount: findings.filter((entry) => entry.code === "REF_UNRESOLVED").length,
shadowedRefCount: findings.filter((entry) => entry.code === "REF_SHADOWED").length,
storeResidueCount: findings.filter((entry) => entry.code === "STORE_PLAINTEXT_RESIDUE").length,
legacyResidueCount: findings.filter((entry) => entry.code === "LEGACY_RESIDUE").length
};
}
/** Runs a secrets audit over config/auth stores and returns structured findings. */
async function runSecretsAudit(params = {}) {
const env = params.env ?? process.env;
const snapshot = await createSecretsConfigIO({ env }).readConfigFileSnapshot();
const configPath = resolveUserPath(snapshot.path);
const defaults = snapshot.valid ? snapshot.config.secrets?.defaults : void 0;
const collector = {
findings: [],
refAssignments: [],
configProviderRefPaths: /* @__PURE__ */ new Map(),
authProviderState: /* @__PURE__ */ new Map(),
configPlaintextAssignments: [],
filesScanned: /* @__PURE__ */ new Set([configPath])
};
const stateDir = resolveStateDir(env, os.homedir);
const envPaths = listSecretsDotEnvPaths({
configPath,
stateDir
});
const config = snapshot.valid ? snapshot.config : {};
let resolution = {
refsChecked: 0,
skippedExecRefs: 0,
resolvabilityComplete: true
};
if (snapshot.valid) {
collectConfigSecrets({
config,
configPath,
collector,
env
});
for (const target of listAuthProfileStoreTargets(config, stateDir, env)) collectAuthStoreSecrets(target, collector, defaults);
for (const modelsJsonPath of listAgentModelsJsonPaths(config, stateDir, env)) collectModelsJsonSecrets({
modelsJsonPath,
collector
});
const unresolvedRefResult = await collectUnresolvedRefFindings({
collector,
config,
env,
allowExec: Boolean(params.allowExec)
});
resolution = {
refsChecked: unresolvedRefResult.refsChecked,
skippedExecRefs: unresolvedRefResult.skippedExecRefs,
resolvabilityComplete: unresolvedRefResult.skippedExecRefs === 0
};
collectShadowingFindings(collector);
collector.findings.push(...findSecretStorePlaintextResidueFindings({
assignments: collector.configPlaintextAssignments,
database: { env }
}));
} else addFinding(collector, {
code: "REF_UNRESOLVED",
severity: "error",
file: configPath,
jsonPath: "<root>",
message: "Config is invalid; cannot validate secret references reliably."
});
for (const envPath of envPaths) collectEnvPlaintext({
envPath,
collector
});
collectLegacyAuthSourceFindings({
config,
stateDir,
env,
collector
});
const summary = summarizeFindings(collector.findings);
return {
version: 1,
status: summary.unresolvedRefCount > 0 ? "unresolved" : collector.findings.length > 0 ? "findings" : "clean",
resolution,
filesScanned: [...collector.filesScanned].toSorted(),
summary,
findings: collector.findings
};
}
/** Maps audit results to CLI exit codes. */
function resolveSecretsAuditExitCode(report, check) {
if (report.summary.unresolvedRefCount > 0) return 2;
if (check && report.findings.length > 0) return 1;
return 0;
}
//#endregion
export { resolveSecretsAuditExitCode, runSecretsAudit };