fix(exec): strip invalid approval policy enums during config normalization (#59112)

* fix(exec): strip invalid security/ask enum values during config normalization

* fix(exec): narrow invalid approvals config cleanup

---------

Co-authored-by: scoootscooob <zhentongfan@gmail.com>
This commit is contained in:
wangchunyue
2026-04-02 16:40:10 +08:00
committed by GitHub
parent d90c8db491
commit a597938be8
2 changed files with 136 additions and 6 deletions

View File

@@ -354,3 +354,107 @@ describe("normalizeExecApprovals handles string allowlist entries (#9790)", () =
}
});
});
describe("normalizeExecApprovals strips invalid security/ask enum values (#59006)", () => {
it("drops invalid defaults.security values like 'none'", () => {
const file = {
version: 1,
defaults: { security: "none" },
agents: {},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.defaults?.security).toBeUndefined();
});
it("drops invalid defaults.ask values like 'never'", () => {
const file = {
version: 1,
defaults: { ask: "never" },
agents: {},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.defaults?.ask).toBeUndefined();
});
it("drops invalid defaults.askFallback values", () => {
const file = {
version: 1,
defaults: { askFallback: "none" },
agents: {},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.defaults?.askFallback).toBeUndefined();
});
it("preserves valid defaults.security and defaults.ask values", () => {
const file: ExecApprovalsFile = {
version: 1,
defaults: { security: "full", ask: "off", askFallback: "deny" },
agents: {},
};
const normalized = normalizeExecApprovals(file);
expect(normalized.defaults?.security).toBe("full");
expect(normalized.defaults?.ask).toBe("off");
expect(normalized.defaults?.askFallback).toBe("deny");
});
it("drops invalid agent-level security/ask values", () => {
const file = {
version: 1,
agents: {
main: { security: "none", ask: "never", askFallback: "open" },
},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.agents?.main?.security).toBeUndefined();
expect(normalized.agents?.main?.ask).toBeUndefined();
expect(normalized.agents?.main?.askFallback).toBeUndefined();
});
it("drops invalid wildcard agent security/ask values", () => {
const file = {
version: 1,
agents: {
"*": { security: "none", ask: "off" },
},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.agents?.["*"]?.security).toBeUndefined();
expect(normalized.agents?.["*"]?.ask).toBe("off");
});
it("resolves to built-in defaults when invalid values are stripped", () => {
const file = {
version: 1,
defaults: { security: "none", ask: "never" },
agents: {
"*": { security: "none", ask: "off" },
},
} as unknown as ExecApprovalsFile;
const resolved = resolveExecApprovalsFromFile({ file });
// Invalid "none" in defaults is stripped, so fallback to DEFAULT_SECURITY ("deny")
expect(resolved.defaults.security).toBe("deny");
// Invalid "never" in defaults is stripped, so fallback to DEFAULT_ASK ("on-miss")
expect(resolved.defaults.ask).toBe("on-miss");
// Wildcard agent "none" is stripped, so agent inherits resolved defaults
expect(resolved.agent.security).toBe("deny");
// Wildcard agent ask="off" is valid and preserved
expect(resolved.agent.ask).toBe("off");
});
it("strips non-string policy values (e.g. numbers, booleans) without throwing", () => {
const file = {
version: 1,
defaults: { security: 1, ask: true, askFallback: ["deny"] },
agents: {
main: { security: 42, ask: false },
},
} as unknown as ExecApprovalsFile;
const normalized = normalizeExecApprovals(file);
expect(normalized.defaults?.security).toBeUndefined();
expect(normalized.defaults?.ask).toBeUndefined();
expect(normalized.defaults?.askFallback).toBeUndefined();
expect(normalized.agents?.main?.security).toBeUndefined();
expect(normalized.agents?.main?.ask).toBeUndefined();
});
});

View File

@@ -28,6 +28,11 @@ export function normalizeExecTarget(value?: string | null): ExecTarget | null {
return normalizeExecHost(normalized);
}
/** Coerce a raw JSON field to string, returning undefined for non-string types. */
function toStringOrUndefined(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
export function normalizeExecSecurity(value?: string | null): ExecSecurity | null {
const normalized = value?.trim().toLowerCase();
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") {
@@ -291,6 +296,17 @@ function stripAllowlistCommandText(
return changed ? next : allowlist;
}
function sanitizeExecApprovalPolicy(
policy: ExecApprovalsDefaults | ExecApprovalsAgent | undefined,
): ExecApprovalsDefaults {
return {
security: normalizeExecSecurity(toStringOrUndefined(policy?.security)) ?? undefined,
ask: normalizeExecAsk(toStringOrUndefined(policy?.ask)) ?? undefined,
askFallback: normalizeExecSecurity(toStringOrUndefined(policy?.askFallback)) ?? undefined,
autoAllowSkills: policy?.autoAllowSkills,
};
}
export function normalizeExecApprovals(file: ExecApprovalsFile): ExecApprovalsFile {
const socketPath = file.socket?.path?.trim();
const token = file.socket?.token?.trim();
@@ -305,10 +321,23 @@ export function normalizeExecApprovals(file: ExecApprovalsFile): ExecApprovalsFi
const coerced = coerceAllowlistEntries(agent.allowlist);
const withIds = ensureAllowlistIds(coerced);
const allowlist = stripAllowlistCommandText(withIds);
if (allowlist !== agent.allowlist) {
agents[key] = { ...agent, allowlist };
const sanitizedPolicy = sanitizeExecApprovalPolicy(agent);
const agentChanged =
allowlist !== agent.allowlist ||
sanitizedPolicy.security !== agent.security ||
sanitizedPolicy.ask !== agent.ask ||
sanitizedPolicy.askFallback !== agent.askFallback;
if (agentChanged) {
agents[key] = {
...agent,
allowlist,
security: sanitizedPolicy.security,
ask: sanitizedPolicy.ask,
askFallback: sanitizedPolicy.askFallback,
};
}
}
const sanitizedDefaults = sanitizeExecApprovalPolicy(file.defaults);
const normalized: ExecApprovalsFile = {
version: 1,
socket: {
@@ -316,10 +345,7 @@ export function normalizeExecApprovals(file: ExecApprovalsFile): ExecApprovalsFi
token: token && token.length > 0 ? token : undefined,
},
defaults: {
security: file.defaults?.security,
ask: file.defaults?.ask,
askFallback: file.defaults?.askFallback,
autoAllowSkills: file.defaults?.autoAllowSkills,
...sanitizedDefaults,
},
agents,
};