Skip to content

Commit b258c3f

Browse files
committed
fix(secretrefs): preserve exec resolver env
1 parent d04a897 commit b258c3f

5 files changed

Lines changed: 142 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
1414

1515
### Fixes
1616

17+
- 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.
1718
- 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.
1819
- 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.
1920
- 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.

docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ services:
22
openclaw-gateway:
33
image: ${OPENCLAW_IMAGE:-openclaw:local}
44
build: .
5+
env_file:
6+
- path: .env
7+
required: false
58
environment:
69
HOME: /home/node
710
TERM: xterm-256color
@@ -71,6 +74,9 @@ services:
7174
openclaw-cli:
7275
image: ${OPENCLAW_IMAGE:-openclaw:local}
7376
network_mode: "service:openclaw-gateway"
77+
env_file:
78+
- path: .env
79+
required: false
7480
cap_drop:
7581
- NET_RAW
7682
- NET_ADMIN

src/commands/daemon-install-helpers.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,74 @@ describe("buildGatewayInstallPlan", () => {
310310
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
311311
});
312312

313+
it("includes passEnv values for configured exec SecretRef providers", async () => {
314+
mockNodeGatewayPlanFixture({
315+
serviceEnvironment: {
316+
OPENCLAW_PORT: "3000",
317+
},
318+
});
319+
320+
const plan = await buildGatewayInstallPlan({
321+
env: isolatedPlanEnv({
322+
OP_CONNECT_TOKEN: "op-connect-token",
323+
}),
324+
port: 3000,
325+
runtime: "node",
326+
config: {
327+
secrets: {
328+
providers: {
329+
onepassword: {
330+
source: "exec",
331+
command: "/usr/bin/op",
332+
args: ["read", "op://Private/Discord/password"],
333+
passEnv: ["OP_CONNECT_TOKEN"],
334+
allowInsecurePath: true,
335+
},
336+
},
337+
},
338+
channels: {
339+
discord: {
340+
token: { source: "exec", provider: "onepassword", id: "value" },
341+
},
342+
},
343+
},
344+
});
345+
346+
expect(plan.environment.OP_CONNECT_TOKEN).toBe("op-connect-token");
347+
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
348+
});
349+
350+
it("does not include passEnv values for unused exec SecretRef providers", async () => {
351+
mockNodeGatewayPlanFixture({
352+
serviceEnvironment: {
353+
OPENCLAW_PORT: "3000",
354+
},
355+
});
356+
357+
const plan = await buildGatewayInstallPlan({
358+
env: isolatedPlanEnv({
359+
OP_CONNECT_TOKEN: "op-connect-token",
360+
}),
361+
port: 3000,
362+
runtime: "node",
363+
config: {
364+
secrets: {
365+
providers: {
366+
onepassword: {
367+
source: "exec",
368+
command: "/usr/bin/op",
369+
passEnv: ["OP_CONNECT_TOKEN"],
370+
allowInsecurePath: true,
371+
},
372+
},
373+
},
374+
},
375+
});
376+
377+
expect(plan.environment.OP_CONNECT_TOKEN).toBeUndefined();
378+
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
379+
});
380+
313381
it("does not embed gateway auth SecretRef values into the service environment", async () => {
314382
mockNodeGatewayPlanFixture({
315383
serviceEnvironment: {

src/commands/daemon-install-helpers.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,61 @@ function collectConfigSecretRefServiceEnvVars(params: {
170170
return entries;
171171
}
172172

173+
function collectExecSecretRefPassEnvServiceEnvVars(params: {
174+
env: Record<string, string | undefined>;
175+
config?: OpenClawConfig;
176+
durableEnvironment: Record<string, string | undefined>;
177+
warn?: DaemonInstallWarnFn;
178+
}): Record<string, string> {
179+
if (!params.config) {
180+
return {};
181+
}
182+
const entries: Record<string, string> = {};
183+
for (const target of discoverConfigSecretTargets(params.config)) {
184+
if (!target.entry.includeInPlan) {
185+
continue;
186+
}
187+
const { ref } = resolveSecretInputRef({
188+
value: target.value,
189+
refValue: target.refValue,
190+
defaults: params.config.secrets?.defaults,
191+
});
192+
if (!ref || ref.source !== "exec") {
193+
continue;
194+
}
195+
const provider = params.config.secrets?.providers?.[ref.provider];
196+
if (!provider || provider.source !== "exec") {
197+
continue;
198+
}
199+
for (const rawKey of provider.passEnv ?? []) {
200+
const key = normalizeEnvVarKey(rawKey, { portable: true });
201+
if (!key) {
202+
params.warn?.(
203+
`Exec SecretRef passEnv id "${rawKey}" is not portable and was not added to the service environment`,
204+
"Config SecretRef",
205+
);
206+
continue;
207+
}
208+
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
209+
params.warn?.(
210+
`Exec SecretRef passEnv ref "${key}" blocked by host-env security policy`,
211+
"Config SecretRef",
212+
);
213+
continue;
214+
}
215+
if (Object.hasOwn(params.durableEnvironment, key)) {
216+
continue;
217+
}
218+
const value = params.env[key]?.trim();
219+
if (!value) {
220+
continue;
221+
}
222+
entries[key] = value;
223+
}
224+
}
225+
return entries;
226+
}
227+
173228
function mergeServicePath(
174229
nextPath: string | undefined,
175230
existingPath: string | undefined,
@@ -338,6 +393,12 @@ async function buildGatewayInstallEnvironment(params: {
338393
durableEnvironment,
339394
warn: params.warn,
340395
});
396+
const execSecretRefPassEnvEnvironment = collectExecSecretRefPassEnvServiceEnvVars({
397+
env: params.env,
398+
config: params.config,
399+
durableEnvironment,
400+
warn: params.warn,
401+
});
341402
const authProfileEnvironment = await collectAuthProfileServiceEnvVars({
342403
env: params.env,
343404
authStore: params.authStore,
@@ -350,6 +411,7 @@ async function buildGatewayInstallEnvironment(params: {
350411
),
351412
...durableEnvironment,
352413
...configSecretRefEnvironment,
414+
...execSecretRefPassEnvEnvironment,
353415
...authProfileEnvironment,
354416
};
355417
const managedServiceEnvKeys = formatManagedServiceEnvKeys(durableEnvironment, {

src/docker-setup.e2e.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,11 @@ describe("scripts/docker/setup.sh", () => {
610610
);
611611
});
612612

613+
it("keeps docker-compose optional env files aligned across services", async () => {
614+
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
615+
expect(compose.match(/env_file:\n {6}- path: \.env\n {8}required: false/g)).toHaveLength(2);
616+
});
617+
613618
it("keeps docker-compose timezone env defaults aligned across services", async () => {
614619
const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8");
615620
expect(compose.match(/TZ: \$\{OPENCLAW_TZ:-UTC\}/g)).toHaveLength(2);

0 commit comments

Comments
 (0)