Skip to content

Commit 15d4487

Browse files
committed
fix(secrets): avoid exec target gateway failures
1 parent 6830aa3 commit 15d4487

2 files changed

Lines changed: 105 additions & 1 deletion

File tree

src/secrets/runtime-command-secrets.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ discoverConfigSecretTargetsByIds(forcedFallbackConfig, new Set([firecrawlPath]))
6060

6161
function activateMinimalSecretsRuntimeSnapshot(params: {
6262
config: OpenClawConfig;
63+
resolvedConfig?: OpenClawConfig;
6364
env: Record<string, string | undefined>;
6465
}) {
6566
const snapshot = {
6667
sourceConfig: structuredClone(params.config),
67-
config: structuredClone(params.config),
68+
config: structuredClone(params.resolvedConfig ?? params.config),
6869
authStores: [],
6970
warnings: [],
7071
webTools: createEmptyRuntimeWebToolsMetadata(),
@@ -157,4 +158,58 @@ describe("runtime command secrets", () => {
157158
expect(resolved.diagnostics).toEqual([]);
158159
expect(resolved.inactiveRefPaths).toEqual([]);
159160
});
161+
162+
it("returns resolved snapshot assignments when exec targets remain unresolved", async () => {
163+
const sourceConfig = {
164+
models: {
165+
providers: {
166+
google: {
167+
enabled: true,
168+
apiKey: { source: "exec", provider: "default", id: "models/google/api-key" },
169+
},
170+
openai: {
171+
enabled: true,
172+
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
173+
},
174+
},
175+
},
176+
secrets: {
177+
providers: {
178+
default: { source: "env" },
179+
},
180+
},
181+
} as unknown as OpenClawConfig;
182+
activateMinimalSecretsRuntimeSnapshot({
183+
config: sourceConfig,
184+
resolvedConfig: {
185+
...sourceConfig,
186+
models: {
187+
providers: {
188+
...sourceConfig.models?.providers,
189+
openai: {
190+
...sourceConfig.models?.providers?.openai,
191+
apiKey: "gateway-openai-key",
192+
},
193+
},
194+
},
195+
} as OpenClawConfig,
196+
env: {
197+
HOME: process.env.HOME,
198+
},
199+
});
200+
201+
const resolved = await resolveCommandSecretsFromActiveRuntimeSnapshot({
202+
commandName: "reply",
203+
targetIds: new Set(["models.providers.*.apiKey"]),
204+
});
205+
206+
expect(resolved.assignments).toMatchObject([
207+
{
208+
path: "models.providers.openai.apiKey",
209+
value: "gateway-openai-key",
210+
},
211+
]);
212+
expect(resolved.diagnostics).toEqual([]);
213+
expect(resolved.inactiveRefPaths).toEqual([]);
214+
});
160215
});

src/secrets/runtime-command-secrets.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,33 @@ function mirrorResolvedProviderCredentialToDirectPaths(params: {
344344
}
345345
}
346346

347+
function collectUnresolvedExecSecretRefPaths(params: {
348+
sourceConfig: OpenClawConfig;
349+
targetIds: ReadonlySet<string>;
350+
unresolvedPaths: ReadonlySet<string>;
351+
allowedPaths?: ReadonlySet<string>;
352+
}): Set<string> {
353+
const defaults = params.sourceConfig.secrets?.defaults;
354+
const execPaths = new Set<string>();
355+
for (const target of discoverConfigSecretTargetsByIds(params.sourceConfig, params.targetIds)) {
356+
if (params.allowedPaths && !params.allowedPaths.has(target.path)) {
357+
continue;
358+
}
359+
if (!params.unresolvedPaths.has(target.path)) {
360+
continue;
361+
}
362+
const { ref } = resolveSecretInputRef({
363+
value: target.value,
364+
refValue: target.refValue,
365+
defaults,
366+
});
367+
if (ref?.source === "exec") {
368+
execPaths.add(target.path);
369+
}
370+
}
371+
return execPaths;
372+
}
373+
347374
async function resolveForcedActiveCommandSecretTargets(params: {
348375
sourceConfig: OpenClawConfig;
349376
resolvedConfig: OpenClawConfig;
@@ -575,6 +602,28 @@ async function resolveCommandSecretsFromSnapshot(params: {
575602
inactiveRefPaths,
576603
};
577604
}
605+
const unresolvedPaths = new Set(analyzed.unresolved.map((entry) => entry.path));
606+
const unresolvedExecPaths = collectUnresolvedExecSecretRefPaths({
607+
sourceConfig,
608+
targetIds: params.targetIds,
609+
unresolvedPaths,
610+
allowedPaths: params.allowedPaths,
611+
});
612+
if (unresolvedExecPaths.size > 0) {
613+
const nonExecUnresolved = analyzed.unresolved.filter(
614+
(entry) => !unresolvedExecPaths.has(entry.path),
615+
);
616+
if (nonExecUnresolved.length > 0) {
617+
throw new Error(
618+
`${params.commandName}: ${nonExecUnresolved[0]?.path ?? "target"} is unresolved in the active runtime snapshot.`,
619+
);
620+
}
621+
return {
622+
assignments: analyzed.assignments,
623+
diagnostics: analyzed.diagnostics,
624+
inactiveRefPaths,
625+
};
626+
}
578627
const resolved = collectCommandSecretAssignmentsFromSnapshot({
579628
sourceConfig,
580629
resolvedConfig,

0 commit comments

Comments
 (0)