mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(config): redact dynamic catchall secret keys
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305.
|
||||
- Tests/Vitest: tier local parallel worker defaults by host memory, keep gateway serial by default on non-high-memory hosts, and document a low-profile fallback command for memory-constrained land/gate runs to prevent local OOMs. (#24719) Thanks @ngutman.
|
||||
- Agents/Context pruning: extend `cache-ttl` eligibility to Moonshot/Kimi and ZAI/GLM providers (including OpenRouter model refs), so `contextPruning.mode: "cache-ttl"` is no longer silently skipped for those sessions. (#24497) Thanks @lailoo.
|
||||
- Tools/web_search: add `provider: "kimi"` (Moonshot) support with key/config schema wiring and a corrected two-step `$web_search` tool flow that echoes tool results before final synthesis, including citation extraction from search results. (#16616, #18822) Thanks @adshine.
|
||||
|
||||
@@ -656,7 +656,7 @@ describe("redactConfigSnapshot", () => {
|
||||
expectGatewayAuthFieldValue(result, "token", "not-actually-secret-value");
|
||||
});
|
||||
|
||||
it("does not redact paths absent from uiHints (schema is single source of truth)", () => {
|
||||
it("redacts sensitive-looking paths even when absent from uiHints (defense in depth)", () => {
|
||||
const hints: ConfigUiHints = {
|
||||
"some.other.path": { sensitive: true },
|
||||
};
|
||||
@@ -664,7 +664,57 @@ describe("redactConfigSnapshot", () => {
|
||||
gateway: { auth: { password: "not-in-hints-value" } },
|
||||
});
|
||||
const result = redactConfigSnapshot(snapshot, hints);
|
||||
expectGatewayAuthFieldValue(result, "password", "not-in-hints-value");
|
||||
expectGatewayAuthFieldValue(result, "password", REDACTED_SENTINEL);
|
||||
});
|
||||
|
||||
it("redacts and restores dynamic env catchall secrets when uiHints miss the path", () => {
|
||||
const hints: ConfigUiHints = {
|
||||
"some.other.path": { sensitive: true },
|
||||
};
|
||||
const snapshot = makeSnapshot({
|
||||
env: {
|
||||
GROQ_API_KEY: "gsk-secret-123",
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
});
|
||||
const redacted = redactConfigSnapshot(snapshot, hints);
|
||||
const env = redacted.config.env as Record<string, string>;
|
||||
expect(env.GROQ_API_KEY).toBe(REDACTED_SENTINEL);
|
||||
expect(env.NODE_ENV).toBe("production");
|
||||
|
||||
const restored = restoreRedactedValues(redacted.config, snapshot.config, hints);
|
||||
expect(restored.env.GROQ_API_KEY).toBe("gsk-secret-123");
|
||||
expect(restored.env.NODE_ENV).toBe("production");
|
||||
});
|
||||
|
||||
it("redacts and restores skills entry env secrets in dynamic record paths", () => {
|
||||
const hints: ConfigUiHints = {
|
||||
"some.other.path": { sensitive: true },
|
||||
};
|
||||
const snapshot = makeSnapshot({
|
||||
skills: {
|
||||
entries: {
|
||||
web_search: {
|
||||
env: {
|
||||
GEMINI_API_KEY: "gemini-secret-456",
|
||||
BRAVE_REGION: "us",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const redacted = redactConfigSnapshot(snapshot, hints);
|
||||
const entry = (
|
||||
redacted.config.skills as {
|
||||
entries: Record<string, { env: Record<string, string> }>;
|
||||
}
|
||||
).entries.web_search;
|
||||
expect(entry.env.GEMINI_API_KEY).toBe(REDACTED_SENTINEL);
|
||||
expect(entry.env.BRAVE_REGION).toBe("us");
|
||||
|
||||
const restored = restoreRedactedValues(redacted.config, snapshot.config, hints);
|
||||
expect(restored.skills.entries.web_search.env.GEMINI_API_KEY).toBe("gemini-secret-456");
|
||||
expect(restored.skills.entries.web_search.env.BRAVE_REGION).toBe("us");
|
||||
});
|
||||
|
||||
it("uses wildcard hints for array items", () => {
|
||||
|
||||
@@ -164,7 +164,10 @@ function redactObjectWithLookup(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched && isExtensionPath(path)) {
|
||||
if (!matched) {
|
||||
// Fall back to pattern-based guessing for paths not covered by schema
|
||||
// hints. This catches dynamic keys inside catchall objects (for example
|
||||
// env.GROQ_API_KEY) and extension/plugin config alike.
|
||||
const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
|
||||
if (
|
||||
typeof value === "string" &&
|
||||
@@ -542,7 +545,7 @@ function restoreRedactedValuesWithLookup(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched && isExtensionPath(path)) {
|
||||
if (!matched) {
|
||||
const markedNonSensitive = isExplicitlyNonSensitivePath(hints, [path, wildcardPath]);
|
||||
if (!markedNonSensitive && isSensitivePath(path) && value === REDACTED_SENTINEL) {
|
||||
result[key] = restoreOriginalValueOrThrow({ key, path, original: orig });
|
||||
|
||||
Reference in New Issue
Block a user