mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
fix(secretrefs): preserve exec resolver env
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Docker/Gateway: pass Docker setup `.env` values into gateway and CLI containers and preserve exec SecretRef `passEnv` keys in managed service plans, so 1Password Connect-backed Discord tokens keep resolving after doctor or plugin repair. Thanks @vincentkoc.
|
||||
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
|
||||
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
|
||||
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
|
||||
|
||||
@@ -2,6 +2,9 @@ services:
|
||||
openclaw-gateway:
|
||||
image: ${OPENCLAW_IMAGE:-openclaw:local}
|
||||
build: .
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
environment:
|
||||
HOME: /home/node
|
||||
TERM: xterm-256color
|
||||
@@ -71,6 +74,9 @@ services:
|
||||
openclaw-cli:
|
||||
image: ${OPENCLAW_IMAGE:-openclaw:local}
|
||||
network_mode: "service:openclaw-gateway"
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
- NET_ADMIN
|
||||
|
||||
@@ -310,6 +310,74 @@ describe("buildGatewayInstallPlan", () => {
|
||||
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes passEnv values for configured exec SecretRef providers", async () => {
|
||||
mockNodeGatewayPlanFixture({
|
||||
serviceEnvironment: {
|
||||
OPENCLAW_PORT: "3000",
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await buildGatewayInstallPlan({
|
||||
env: isolatedPlanEnv({
|
||||
OP_CONNECT_TOKEN: "op-connect-token",
|
||||
}),
|
||||
port: 3000,
|
||||
runtime: "node",
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
onepassword: {
|
||||
source: "exec",
|
||||
command: "/usr/bin/op",
|
||||
args: ["read", "op://Private/Discord/password"],
|
||||
passEnv: ["OP_CONNECT_TOKEN"],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "exec", provider: "onepassword", id: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan.environment.OP_CONNECT_TOKEN).toBe("op-connect-token");
|
||||
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not include passEnv values for unused exec SecretRef providers", async () => {
|
||||
mockNodeGatewayPlanFixture({
|
||||
serviceEnvironment: {
|
||||
OPENCLAW_PORT: "3000",
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await buildGatewayInstallPlan({
|
||||
env: isolatedPlanEnv({
|
||||
OP_CONNECT_TOKEN: "op-connect-token",
|
||||
}),
|
||||
port: 3000,
|
||||
runtime: "node",
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
onepassword: {
|
||||
source: "exec",
|
||||
command: "/usr/bin/op",
|
||||
passEnv: ["OP_CONNECT_TOKEN"],
|
||||
allowInsecurePath: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(plan.environment.OP_CONNECT_TOKEN).toBeUndefined();
|
||||
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not embed gateway auth SecretRef values into the service environment", async () => {
|
||||
mockNodeGatewayPlanFixture({
|
||||
serviceEnvironment: {
|
||||
|
||||
@@ -170,6 +170,61 @@ function collectConfigSecretRefServiceEnvVars(params: {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectExecSecretRefPassEnvServiceEnvVars(params: {
|
||||
env: Record<string, string | undefined>;
|
||||
config?: OpenClawConfig;
|
||||
durableEnvironment: Record<string, string | undefined>;
|
||||
warn?: DaemonInstallWarnFn;
|
||||
}): Record<string, string> {
|
||||
if (!params.config) {
|
||||
return {};
|
||||
}
|
||||
const entries: Record<string, string> = {};
|
||||
for (const target of discoverConfigSecretTargets(params.config)) {
|
||||
if (!target.entry.includeInPlan) {
|
||||
continue;
|
||||
}
|
||||
const { ref } = resolveSecretInputRef({
|
||||
value: target.value,
|
||||
refValue: target.refValue,
|
||||
defaults: params.config.secrets?.defaults,
|
||||
});
|
||||
if (!ref || ref.source !== "exec") {
|
||||
continue;
|
||||
}
|
||||
const provider = params.config.secrets?.providers?.[ref.provider];
|
||||
if (!provider || provider.source !== "exec") {
|
||||
continue;
|
||||
}
|
||||
for (const rawKey of provider.passEnv ?? []) {
|
||||
const key = normalizeEnvVarKey(rawKey, { portable: true });
|
||||
if (!key) {
|
||||
params.warn?.(
|
||||
`Exec SecretRef passEnv id "${rawKey}" is not portable and was not added to the service environment`,
|
||||
"Config SecretRef",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
|
||||
params.warn?.(
|
||||
`Exec SecretRef passEnv ref "${key}" blocked by host-env security policy`,
|
||||
"Config SecretRef",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (Object.hasOwn(params.durableEnvironment, key)) {
|
||||
continue;
|
||||
}
|
||||
const value = params.env[key]?.trim();
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
entries[key] = value;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function mergeServicePath(
|
||||
nextPath: string | undefined,
|
||||
existingPath: string | undefined,
|
||||
@@ -338,6 +393,12 @@ async function buildGatewayInstallEnvironment(params: {
|
||||
durableEnvironment,
|
||||
warn: params.warn,
|
||||
});
|
||||
const execSecretRefPassEnvEnvironment = collectExecSecretRefPassEnvServiceEnvVars({
|
||||
env: params.env,
|
||||
config: params.config,
|
||||
durableEnvironment,
|
||||
warn: params.warn,
|
||||
});
|
||||
const authProfileEnvironment = await collectAuthProfileServiceEnvVars({
|
||||
env: params.env,
|
||||
authStore: params.authStore,
|
||||
@@ -350,6 +411,7 @@ async function buildGatewayInstallEnvironment(params: {
|
||||
),
|
||||
...durableEnvironment,
|
||||
...configSecretRefEnvironment,
|
||||
...execSecretRefPassEnvEnvironment,
|
||||
...authProfileEnvironment,
|
||||
};
|
||||
const managedServiceEnvKeys = formatManagedServiceEnvKeys(durableEnvironment, {
|
||||
|
||||
@@ -610,6 +610,11 @@ describe("scripts/docker/setup.sh", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps docker-compose optional env files aligned across services", async () => {
|
||||
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
|
||||
expect(compose.match(/env_file:\n {6}- path: \.env\n {8}required: false/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps docker-compose timezone env defaults aligned across services", async () => {
|
||||
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
|
||||
expect(compose.match(/TZ: \$\{OPENCLAW_TZ:-UTC\}/g)).toHaveLength(2);
|
||||
|
||||
Reference in New Issue
Block a user