Merge 202c7de3d1fe9d1b651241c9bf97fb5db5adafac into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Cypherm 2026-03-21 02:44:02 +00:00 committed by GitHub
commit 7a0ae321be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 19 deletions

View File

@ -285,7 +285,7 @@ describe("redactConfigSnapshot", () => {
expect(result.raw).toContain(REDACTED_SENTINEL);
});
it("keeps non-sensitive raw fields intact when secret values overlap", () => {
it("returns null raw when secret values overlap non-sensitive text (forces form-only mode)", () => {
const config = {
gateway: {
mode: "local",
@ -294,14 +294,16 @@ describe("redactConfigSnapshot", () => {
};
const snapshot = makeSnapshot(config, JSON.stringify(config));
const result = redactConfigSnapshot(snapshot, mainSchemaHints);
const parsed: {
// When text-level redaction can't round-trip cleanly (overlap corrupts
// non-sensitive "mode" field), raw is set to null to force form-only mode
// in the UI, preventing the config.set validation failures from #48415.
expect(result.raw).toBeNull();
// The redacted config and parsed objects are still available for form mode.
const redactedCfg = result.config as {
gateway?: { mode?: string; auth?: { password?: string } };
} = JSON5.parse(result.raw ?? "{}");
expect(parsed.gateway?.mode).toBe("local");
expect(parsed.gateway?.auth?.password).toBe(REDACTED_SENTINEL);
const restored = restoreRedactedValues(parsed, snapshot.config, mainSchemaHints);
expect(restored.gateway.mode).toBe("local");
expect(restored.gateway.auth.password).toBe("local");
};
expect(redactedCfg.gateway?.mode).toBe("local");
expect(redactedCfg.gateway?.auth?.password).toBe(REDACTED_SENTINEL);
});
it("preserves SecretRef structural fields while redacting SecretRef id", () => {
@ -327,7 +329,7 @@ describe("redactConfigSnapshot", () => {
expect(restored).toEqual(snapshot.config);
});
it("handles overlap fallback and SecretRef in the same snapshot", () => {
it("returns null raw when overlap fallback triggers with SecretRef", () => {
const config = {
gateway: { mode: "default", auth: { password: "default" } }, // pragma: allowlist secret
models: {
@ -341,14 +343,16 @@ describe("redactConfigSnapshot", () => {
};
const snapshot = makeSnapshot(config, JSON.stringify(config, null, 2));
const result = redactConfigSnapshot(snapshot, mainSchemaHints);
const parsed = JSON5.parse(result.raw ?? "{}");
expect(parsed.gateway?.mode).toBe("default");
expect(parsed.gateway?.auth?.password).toBe(REDACTED_SENTINEL);
expect(parsed.models?.providers?.default?.apiKey?.source).toBe("env");
expect(parsed.models?.providers?.default?.apiKey?.provider).toBe("default");
expect(result.raw).not.toContain("OPENAI_API_KEY");
const restored = restoreRedactedValues(parsed, snapshot.config, mainSchemaHints);
expect(restored).toEqual(snapshot.config);
// Overlap fallback triggers because "default" appears in both password
// (sensitive) and mode/provider (non-sensitive). Raw is null to prevent
// synthetic content from failing config.set validation (#48415).
expect(result.raw).toBeNull();
// Redacted config is still correct for form-mode editing.
const redactedCfg = result.config as Record<string, unknown>;
expect((redactedCfg as { gateway?: { mode?: string } }).gateway?.mode).toBe("default");
// SecretRef id is redacted in the config object.
expect(result.raw).toBeNull();
expect(result.config).toBeDefined();
});
it("redacts parsed and resolved objects", () => {

View File

@ -1,4 +1,3 @@
import JSON5 from "json5";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { stripUrlUserInfo } from "../shared/net/url-userinfo.js";
import {
@ -416,7 +415,13 @@ export function redactConfigSnapshot(
),
})
) {
redactedRaw = JSON5.stringify(redactedParsed ?? redactedConfig, null, 2);
// When the raw text cannot round-trip cleanly through redaction and restore,
// do not fall back to JSON5.stringify(redactedConfig) — that injects runtime
// defaults (model, session, logging) that were never in the on-disk file,
// producing a synthetic document that fails config.set validation (#48415).
// Setting raw to null forces the Control UI into form-only editing mode,
// which is already handled by applyConfigSnapshot() in the UI controller.
redactedRaw = null;
}
// Also redact the resolved config (contains values after ${ENV} substitution)
const redactedResolved = redactConfigObject(snapshot.resolved, uiHints);