mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
refactor(config): harden catchall hint mapping and array fallback
This commit is contained in:
@@ -717,6 +717,47 @@ describe("redactConfigSnapshot", () => {
|
||||
expect(restored.skills.entries.web_search.env.BRAVE_REGION).toBe("us");
|
||||
});
|
||||
|
||||
it("contract-covers dynamic catchall/record paths for redact+restore", () => {
|
||||
const hints = mapSensitivePaths(OpenClawSchema, "", {});
|
||||
const snapshot = makeSnapshot({
|
||||
env: {
|
||||
GROQ_API_KEY: "gsk-contract-123",
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
skills: {
|
||||
entries: {
|
||||
web_search: {
|
||||
env: {
|
||||
GEMINI_API_KEY: "gemini-contract-456",
|
||||
BRAVE_REGION: "us",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
broadcast: {
|
||||
apiToken: ["broadcast-secret-1", "broadcast-secret-2"],
|
||||
channels: ["ops", "eng"],
|
||||
},
|
||||
});
|
||||
|
||||
const redacted = redactConfigSnapshot(snapshot, hints);
|
||||
const config = redacted.config as {
|
||||
env: Record<string, string>;
|
||||
skills: { entries: Record<string, { env: Record<string, string> }> };
|
||||
broadcast: Record<string, string[]>;
|
||||
};
|
||||
|
||||
expect(config.env.GROQ_API_KEY).toBe(REDACTED_SENTINEL);
|
||||
expect(config.env.NODE_ENV).toBe("production");
|
||||
expect(config.skills.entries.web_search.env.GEMINI_API_KEY).toBe(REDACTED_SENTINEL);
|
||||
expect(config.skills.entries.web_search.env.BRAVE_REGION).toBe("us");
|
||||
expect(config.broadcast.apiToken).toEqual([REDACTED_SENTINEL, REDACTED_SENTINEL]);
|
||||
expect(config.broadcast.channels).toEqual(["ops", "eng"]);
|
||||
|
||||
const restored = restoreRedactedValues(redacted.config, snapshot.config, hints);
|
||||
expect(restored).toEqual(snapshot.config);
|
||||
});
|
||||
|
||||
it("uses wildcard hints for array items", () => {
|
||||
const hints: ConfigUiHints = {
|
||||
"channels.slack.accounts[].botToken": { sensitive: true },
|
||||
|
||||
@@ -17,15 +17,6 @@ function isEnvVarPlaceholder(value: string): boolean {
|
||||
return ENV_VAR_PLACEHOLDER_PATTERN.test(value.trim());
|
||||
}
|
||||
|
||||
function isExtensionPath(path: string): boolean {
|
||||
return (
|
||||
path === "plugins" ||
|
||||
path.startsWith("plugins.") ||
|
||||
path === "channels" ||
|
||||
path.startsWith("channels.")
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitlyNonSensitivePath(hints: ConfigUiHints | undefined, paths: string[]): boolean {
|
||||
if (!hints) {
|
||||
return false;
|
||||
@@ -130,9 +121,8 @@ function redactObjectWithLookup(
|
||||
if (Array.isArray(obj)) {
|
||||
const path = `${prefix}[]`;
|
||||
if (!lookup.has(path)) {
|
||||
if (!isExtensionPath(prefix)) {
|
||||
return obj;
|
||||
}
|
||||
// Keep behavior symmetric with object fallback: if hints miss the path,
|
||||
// still run pattern-based guessing for non-extension arrays.
|
||||
return redactObjectGuessing(obj, prefix, values, hints);
|
||||
}
|
||||
return obj.map((item) => {
|
||||
@@ -507,9 +497,8 @@ function restoreRedactedValuesWithLookup(
|
||||
// sensitive string array in the config...
|
||||
const { incoming: incomingArray, path } = arrayContext;
|
||||
if (!lookup.has(path)) {
|
||||
if (!isExtensionPath(prefix)) {
|
||||
return incomingArray;
|
||||
}
|
||||
// Keep behavior symmetric with object fallback: if hints miss the path,
|
||||
// still run pattern-based guessing for non-extension arrays.
|
||||
return restoreRedactedValuesGuessing(incomingArray, original, prefix, hints);
|
||||
}
|
||||
return mapRedactedArray({
|
||||
|
||||
@@ -98,6 +98,30 @@ describe("mapSensitivePaths", () => {
|
||||
expect(result["merged.nested"]?.sensitive).toBe(undefined);
|
||||
});
|
||||
|
||||
it("maps sensitive fields nested under object catchall schemas", () => {
|
||||
const schema = z.object({
|
||||
custom: z.object({}).catchall(
|
||||
z.object({
|
||||
apiKey: z.string().register(sensitive),
|
||||
label: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const result = mapSensitivePaths(schema, "", {});
|
||||
expect(result["custom.*.apiKey"]?.sensitive).toBe(true);
|
||||
expect(result["custom.*.label"]?.sensitive).toBe(undefined);
|
||||
});
|
||||
|
||||
it("does not mark plain catchall values sensitive by default", () => {
|
||||
const schema = z.object({
|
||||
env: z.object({}).catchall(z.string()),
|
||||
});
|
||||
|
||||
const result = mapSensitivePaths(schema, "", {});
|
||||
expect(result["env.*"]?.sensitive).toBe(undefined);
|
||||
});
|
||||
|
||||
it("main schema yields correct hints (samples)", () => {
|
||||
const schema = OpenClawSchema.toJSONSchema({
|
||||
target: "draft-07",
|
||||
|
||||
@@ -209,6 +209,11 @@ export function mapSensitivePaths(
|
||||
const nextPath = path ? `${path}.${key}` : key;
|
||||
next = mapSensitivePaths(shape[key], nextPath, next);
|
||||
}
|
||||
const catchallSchema = currentSchema._def.catchall as z.ZodType | undefined;
|
||||
if (catchallSchema && !(catchallSchema instanceof z.ZodNever)) {
|
||||
const nextPath = path ? `${path}.*` : "*";
|
||||
next = mapSensitivePaths(catchallSchema, nextPath, next);
|
||||
}
|
||||
} else if (currentSchema instanceof z.ZodArray) {
|
||||
const nextPath = path ? `${path}[]` : "[]";
|
||||
next = mapSensitivePaths(currentSchema.element as z.ZodType, nextPath, next);
|
||||
|
||||
Reference in New Issue
Block a user