mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 09:52:56 +00:00
* fix(config): skip state-dir dotenv values that are unresolved shell references
readStateDirDotEnvVarsFromStateDir accepted any non-empty value from the
state-dir .env file and passed it into the managed service env. When a value
contains an unresolved shell variable reference such as "${SUPERMEMORY_KEY}"
or "$MY_VAR", dotenv preserves the literal string. The value then reaches
the LaunchAgent/systemd wrapper as a single-quoted literal, so the credential
is never resolved.
Add containsUnresolvedShellReference() and skip any value matching
$IDENTIFIER, ${...}, or $(...) in parseStateDirDotEnvContent(). Real credential
values (e.g. "sm_abc123") are unaffected.
Fixes #88274
* fix(config): narrow shell-reference detector to whole-value patterns only
The previous /$[\w{(]/ regex matched any value containing $ followed by
a word character, which would incorrectly drop real credentials that merely
contain a dollar sign (e.g. a password like abc$2!xyz).
Replace with isUnresolvedShellReference() that only matches values whose
ENTIRE content is a recognised reference form:
- $VAR_NAME (simple reference)
- ${VAR_NAME} (brace-form reference)
- $(command) (command substitution)
Add a regression test that verifies dollar-bearing real secrets are kept.
* fix(config): use letter/underscore-anchored pattern to avoid matching dollar-numbers
$100, $2, etc. are NOT shell variable references — shell variable names must
begin with a letter or underscore. The previous /^$[\w_]/ would match them.
Change to /^$[A-Za-z_]\w*$/ so only genuine named-variable references like
$MY_VAR are rejected. Dollar-number sequences are now preserved.
* fix(daemon): drop stale systemd env-file refs for skipped state-dir dotenv keys
When a state-dir .env value is an unresolved shell reference ($VAR/${VAR}/$(cmd))
the parser skips it from the managed environment. A prior install could have
written that literal reference into gateway.systemd.env; because the skipped key
no longer appeared in the incoming env or the managed-key removal sets, the stale
literal survived re-stage and could override fresh inline Environment= values.
Surface the skipped shell-reference keys from the state-dir dotenv parser and add
them to the systemd env-file managed-key removal set so re-staging strips the
obsolete literal while preserving operator-only secrets that were never managed
via state-dir .env. launchd regenerates its env file wholesale, so it is
unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(config): skip quoted shell parameter dotenv refs
* fix(config): preserve lowercase dollar-prefixed dotenv literals
* fix(daemon): clear stale unresolved systemd env refs
* fix(daemon): avoid re-staging unresolved file env refs
* fix(daemon): drop unresolved file env refs inline
* fix(daemon): drop inline-and-file unresolved env refs
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
151 lines
5.2 KiB
TypeScript
151 lines
5.2 KiB
TypeScript
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 { resolveStateDir } from "./paths.js";
|
|
import type { OpenClawConfig } from "./types.js";
|
|
|
|
function isBlockedServiceEnvVar(key: string): boolean {
|
|
return 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;
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
export function readStateDirDotEnvVarsFromStateDir(stateDir: string): Record<string, string> {
|
|
return readStateDirDotEnvFromStateDir(stateDir).entries;
|
|
}
|
|
|
|
/**
|
|
* 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 readStateDirDotEnvVarsFromStateDir(stateDir);
|
|
}
|
|
|
|
export type DurableServiceEnvVarSources = {
|
|
stateDirDotEnvEnvironment: Record<string, string>;
|
|
configEnvironment: Record<string, string>;
|
|
durableEnvironment: Record<string, string>;
|
|
};
|
|
|
|
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;
|
|
}
|