mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 04:08:38 +00:00
SecretRef: harden custom/provider secret persistence and reuse (#42554)
* Models: gate custom provider keys by usable secret semantics * Config: project runtime writes onto source snapshot * Models: prevent stale apiKey preservation for marker-managed providers * Runner: strip SecretRef marker headers from resolved models * Secrets: scan active agent models.json path in audit * Config: guard runtime-source projection for unrelated configs * Extensions: fix onboarding type errors in CI * Tests: align setup helper account-enabled expectation * Secrets audit: harden models.json file reads * fix: harden SecretRef custom/provider secret persistence (#42554) (thanks @joshavant)
This commit is contained in:
committed by
Peter Steinberger
parent
20237358d9
commit
36d2ae2a22
@@ -5,6 +5,7 @@ export {
|
||||
createConfigIO,
|
||||
getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
projectConfigOntoRuntimeSourceSnapshot,
|
||||
loadConfig,
|
||||
readBestEffortConfig,
|
||||
parseConfigJson5,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
loadConfig,
|
||||
projectConfigOntoRuntimeSourceSnapshot,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
setRuntimeConfigSnapshot,
|
||||
writeConfigFile,
|
||||
@@ -61,6 +62,46 @@ describe("runtime config snapshot writes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips source projection for non-runtime-derived configs", async () => {
|
||||
await withTempHome("openclaw-config-runtime-projection-shape-", async () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
...createSourceConfig(),
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
...createRuntimeConfig(),
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
},
|
||||
},
|
||||
};
|
||||
const independentConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "sk-independent-config", // pragma: allowlist secret
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
const projected = projectConfigOntoRuntimeSourceSnapshot(independentConfig);
|
||||
expect(projected).toBe(independentConfig);
|
||||
} finally {
|
||||
resetRuntimeConfigState();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("clears runtime source snapshot when runtime snapshot is cleared", async () => {
|
||||
const sourceConfig = createSourceConfig();
|
||||
const runtimeConfig = createRuntimeConfig();
|
||||
|
||||
@@ -1374,6 +1374,58 @@ export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
|
||||
return runtimeConfigSourceSnapshot;
|
||||
}
|
||||
|
||||
function isCompatibleTopLevelRuntimeProjectionShape(params: {
|
||||
runtimeSnapshot: OpenClawConfig;
|
||||
candidate: OpenClawConfig;
|
||||
}): boolean {
|
||||
const runtime = params.runtimeSnapshot as Record<string, unknown>;
|
||||
const candidate = params.candidate as Record<string, unknown>;
|
||||
for (const key of Object.keys(runtime)) {
|
||||
if (!Object.hasOwn(candidate, key)) {
|
||||
return false;
|
||||
}
|
||||
const runtimeValue = runtime[key];
|
||||
const candidateValue = candidate[key];
|
||||
const runtimeType = Array.isArray(runtimeValue)
|
||||
? "array"
|
||||
: runtimeValue === null
|
||||
? "null"
|
||||
: typeof runtimeValue;
|
||||
const candidateType = Array.isArray(candidateValue)
|
||||
? "array"
|
||||
: candidateValue === null
|
||||
? "null"
|
||||
: typeof candidateValue;
|
||||
if (runtimeType !== candidateType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function projectConfigOntoRuntimeSourceSnapshot(config: OpenClawConfig): OpenClawConfig {
|
||||
if (!runtimeConfigSnapshot || !runtimeConfigSourceSnapshot) {
|
||||
return config;
|
||||
}
|
||||
if (config === runtimeConfigSnapshot) {
|
||||
return runtimeConfigSourceSnapshot;
|
||||
}
|
||||
// This projection expects callers to pass config objects derived from the
|
||||
// active runtime snapshot (for example shallow/deep clones with targeted edits).
|
||||
// For structurally unrelated configs, skip projection to avoid accidental
|
||||
// merge-patch deletions or reintroducing resolved values into source refs.
|
||||
if (
|
||||
!isCompatibleTopLevelRuntimeProjectionShape({
|
||||
runtimeSnapshot: runtimeConfigSnapshot,
|
||||
candidate: config,
|
||||
})
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
const runtimePatch = createMergePatch(runtimeConfigSnapshot, config);
|
||||
return coerceConfig(applyMergePatch(runtimeConfigSourceSnapshot, runtimePatch));
|
||||
}
|
||||
|
||||
export function setRuntimeConfigSnapshotRefreshHandler(
|
||||
refreshHandler: RuntimeConfigSnapshotRefreshHandler | null,
|
||||
): void {
|
||||
|
||||
Reference in New Issue
Block a user