openclaw/src/infra/dotenv.ts

107 lines
2.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import dotenv from "dotenv";
import { resolveConfigDir } from "../utils.js";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
normalizeEnvVarKey,
} from "./host-env-security.js";
const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
"ALL_PROXY",
"HTTP_PROXY",
"HTTPS_PROXY",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NO_PROXY",
"OPENCLAW_AGENT_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_HOME",
"OPENCLAW_OAUTH_DIR",
"OPENCLAW_PROFILE",
"OPENCLAW_STATE_DIR",
"PI_CODING_AGENT_DIR",
]);
const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_BASE_URL"];
function shouldBlockRuntimeDotEnvKey(key: string): boolean {
return isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key);
}
function shouldBlockWorkspaceDotEnvKey(key: string): boolean {
const upper = key.toUpperCase();
return (
shouldBlockRuntimeDotEnvKey(upper) ||
BLOCKED_WORKSPACE_DOTENV_KEYS.has(upper) ||
BLOCKED_WORKSPACE_DOTENV_SUFFIXES.some((suffix) => upper.endsWith(suffix))
);
}
function loadDotEnvFile(params: {
filePath: string;
shouldBlockKey: (key: string) => boolean;
quiet?: boolean;
}) {
let content: string;
try {
content = fs.readFileSync(params.filePath, "utf8");
} catch (error) {
if (!params.quiet) {
const code =
error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
if (code !== "ENOENT") {
console.warn(`[dotenv] Failed to read ${params.filePath}: ${String(error)}`);
}
}
return;
}
let parsed: Record<string, string>;
try {
parsed = dotenv.parse(content);
} catch (error) {
if (!params.quiet) {
console.warn(`[dotenv] Failed to parse ${params.filePath}: ${String(error)}`);
}
return;
}
for (const [rawKey, value] of Object.entries(parsed)) {
const key = normalizeEnvVarKey(rawKey, { portable: true });
if (!key || params.shouldBlockKey(key)) {
continue;
}
if (process.env[key] !== undefined) {
continue;
}
process.env[key] = value;
}
}
export function loadRuntimeDotEnvFile(filePath: string, opts?: { quiet?: boolean }) {
loadDotEnvFile({
filePath,
shouldBlockKey: shouldBlockRuntimeDotEnvKey,
quiet: opts?.quiet ?? true,
});
}
export function loadWorkspaceDotEnvFile(filePath: string, opts?: { quiet?: boolean }) {
loadDotEnvFile({
filePath,
shouldBlockKey: shouldBlockWorkspaceDotEnvKey,
quiet: opts?.quiet ?? true,
});
}
export function loadDotEnv(opts?: { quiet?: boolean }) {
const quiet = opts?.quiet ?? true;
const cwdEnvPath = path.join(process.cwd(), ".env");
loadWorkspaceDotEnvFile(cwdEnvPath, { quiet });
// Then load global fallback: ~/.openclaw/.env (or OPENCLAW_STATE_DIR/.env),
// without overriding any env vars already present.
const globalEnvPath = path.join(resolveConfigDir(process.env), ".env");
loadRuntimeDotEnvFile(globalEnvPath, { quiet });
}