mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 01:43:55 +00:00
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
// Loads state-directory dotenv entries used by config and runtime startup.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import dotenv from "dotenv";
|
|
import {
|
|
isDangerousHostEnvOverrideVarName,
|
|
isDangerousHostEnvVarName,
|
|
normalizeEnvVarKey,
|
|
} from "../infra/host-env-security.js";
|
|
import { collectConfigServiceEnvVars } from "./config-env-vars.js";
|
|
import { ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV } from "./future-version-guard.js";
|
|
import { resolveStateDir } from "./paths.js";
|
|
import type { OpenClawConfig } from "./types.js";
|
|
|
|
function isBlockedServiceEnvVar(key: string): boolean {
|
|
return (
|
|
key.toUpperCase() === ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS_ENV ||
|
|
isDangerousHostEnvVarName(key) ||
|
|
isDangerousHostEnvOverrideVarName(key)
|
|
);
|
|
}
|
|
|
|
function unwrapMatchingLiteralQuotes(value: string): string {
|
|
if (value.length < 2) {
|
|
return value;
|
|
}
|
|
const first = value[0];
|
|
const last = value.at(-1);
|
|
if ((first === `"` || first === `'`) && first === last) {
|
|
return value.slice(1, -1);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/** Returns true when a dotenv value is only a shell reference, not an expanded secret. */
|
|
export function isUnresolvedShellReference(value: string): boolean {
|
|
const candidate = unwrapMatchingLiteralQuotes(value.trim());
|
|
// Match only values whose entire content is a shell variable reference:
|
|
// $VAR_NAME (simple reference, OpenClaw env-var style)
|
|
// ${VAR_NAME} (brace-form reference)
|
|
// $(command) (command substitution)
|
|
// A real credential that merely contains a $ (e.g. "abc$2!", "$100") is NOT matched.
|
|
return (
|
|
/^\$[A-Z_][A-Z0-9_]*$/.test(candidate) ||
|
|
/^\$\{[A-Z_][A-Z0-9_]*[^}]*\}$/.test(candidate) ||
|
|
/^\$\([^)]*\)$/.test(candidate)
|
|
);
|
|
}
|
|
|
|
type ParsedStateDirDotEnv = {
|
|
/** Keys whose values are persisted to the managed service environment. */
|
|
entries: Record<string, string>;
|
|
/**
|
|
* Keys that were dropped because their entire value was an unresolved shell
|
|
* reference ($VAR, ${VAR}, or $(cmd)). These are still OpenClaw-managed keys:
|
|
* a previously generated env file may carry a stale literal reference for them
|
|
* that must be removed on re-stage rather than preserved as an operator secret.
|
|
*/
|
|
skippedShellReferenceKeys: string[];
|
|
};
|
|
|
|
function parseStateDirDotEnvContent(content: string): ParsedStateDirDotEnv {
|
|
const parsed = dotenv.parse(content);
|
|
const entries: Record<string, string> = {};
|
|
const skippedShellReferenceKeys: string[] = [];
|
|
for (const [rawKey, value] of Object.entries(parsed)) {
|
|
if (!value?.trim()) {
|
|
continue;
|
|
}
|
|
const key = normalizeEnvVarKey(rawKey, { portable: true });
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
if (isBlockedServiceEnvVar(key)) {
|
|
continue;
|
|
}
|
|
// Skip values whose entire content is an unresolved shell variable reference
|
|
// ($VAR, ${VAR}, or $(cmd)). dotenv does not expand them, so persisting them
|
|
// into a single-quoted LaunchAgent/systemd env file would store the literal
|
|
// reference string rather than the intended credential value.
|
|
// Values that merely contain $ (e.g. a password like "abc$2!") are kept.
|
|
if (isUnresolvedShellReference(value)) {
|
|
skippedShellReferenceKeys.push(key);
|
|
continue;
|
|
}
|
|
entries[key] = value;
|
|
}
|
|
return { entries, skippedShellReferenceKeys };
|
|
}
|
|
|
|
/**
|
|
* Read and parse the state-dir `.env`, returning both the persisted entries and
|
|
* the keys that were skipped because they held unresolved shell references. The
|
|
* skipped keys are surfaced so generated service env files can remove stale
|
|
* literal references for keys OpenClaw previously managed.
|
|
*/
|
|
export function readStateDirDotEnvFromStateDir(stateDir: string): ParsedStateDirDotEnv {
|
|
const dotEnvPath = path.join(stateDir, ".env");
|
|
try {
|
|
return parseStateDirDotEnvContent(fs.readFileSync(dotEnvPath, "utf8"));
|
|
} catch {
|
|
return { entries: {}, skippedShellReferenceKeys: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read and parse `~/.openclaw/.env` (or `$OPENCLAW_STATE_DIR/.env`), returning
|
|
* a filtered record of key-value pairs suitable for a managed service
|
|
* environment source.
|
|
*/
|
|
export function readStateDirDotEnvVars(
|
|
env: Record<string, string | undefined>,
|
|
): Record<string, string> {
|
|
const stateDir = resolveStateDir(env as NodeJS.ProcessEnv);
|
|
return readStateDirDotEnvFromStateDir(stateDir).entries;
|
|
}
|
|
|
|
/** Split view of durable gateway service env sources before precedence is applied. */
|
|
export type DurableServiceEnvVarSources = {
|
|
stateDirDotEnvEnvironment: Record<string, string>;
|
|
configEnvironment: Record<string, string>;
|
|
durableEnvironment: Record<string, string>;
|
|
};
|
|
|
|
/** Collects durable service env vars from state-dir `.env` and config, preserving each source. */
|
|
export function collectDurableServiceEnvVarSources(params: {
|
|
env: Record<string, string | undefined>;
|
|
config?: OpenClawConfig;
|
|
}): DurableServiceEnvVarSources {
|
|
const stateDirDotEnvEnvironment = readStateDirDotEnvVars(params.env);
|
|
const configEnvironment = collectConfigServiceEnvVars(params.config);
|
|
return {
|
|
stateDirDotEnvEnvironment,
|
|
configEnvironment,
|
|
durableEnvironment: {
|
|
...stateDirDotEnvEnvironment,
|
|
...configEnvironment,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Durable service env sources survive beyond the invoking shell and are safe to
|
|
* persist into owner-only gateway service environment sources.
|
|
*
|
|
* Precedence:
|
|
* 1. state-dir `.env` file vars
|
|
* 2. config service env vars
|
|
*/
|
|
export function collectDurableServiceEnvVars(params: {
|
|
env: Record<string, string | undefined>;
|
|
config?: OpenClawConfig;
|
|
}): Record<string, string> {
|
|
return collectDurableServiceEnvVarSources(params).durableEnvironment;
|
|
}
|