diff --git a/src/integrations/serialize.ts b/src/integrations/serialize.ts index 7ef402916..1f5c06508 100644 --- a/src/integrations/serialize.ts +++ b/src/integrations/serialize.ts @@ -177,7 +177,17 @@ export function quoteTomlKey(key: string): string { function tomlScalar(value: unknown): string { if (typeof value === "string") return tomlString(value); if (typeof value === "boolean") return value ? "true" : "false"; - if (typeof value === "number" && Number.isFinite(value)) return String(value); + if (typeof value === "number" && Number.isFinite(value)) { + // Bun.TOML.parse returns TOML integers as JavaScript numbers. Values outside + // the safe range may already have been rounded, so writing them back would + // silently alter a user-owned config rather than merely reformatting it. + if (Number.isInteger(value) && !Number.isSafeInteger(value)) { + throw new UnserializableValueError( + "TOML cannot safely rewrite an integer outside JavaScript's safe range", + ); + } + return String(value); + } /* * Arrays of ANY scalar, not just strings. The string-only check was written * against our own builder output; a user's config legitimately holds diff --git a/tests/integrations-invariants.test.ts b/tests/integrations-invariants.test.ts index 4178b908b..7c61e5087 100644 --- a/tests/integrations-invariants.test.ts +++ b/tests/integrations-invariants.test.ts @@ -459,6 +459,20 @@ describe("a real user document is not rejected for being richer than ours", () = }); describe("we refuse rather than corrupt or crash", () => { + test("a TOML file with an unsafe integer array is refused without being rewritten", () => { + const configPath = installClient("kimi"); + const seed = '[providers.mine]\napi = "http://keep-me"\nports = [9007199254740993]\n'; + writeFileSync(configPath, seed); + + const result = applyIntegration({ + clientId: "kimi", models: MODELS, config: CONFIG, port: 10100, + env: TEST_ENV, home, store, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("unsafe"); + expect(readFileSync(configPath, "utf8")).toBe(seed); + }); + test("a TOML file with special floats is refused, not silently rewritten", () => { /* * Bun's TOML parser mangles these before we ever see the document: `inf` diff --git a/tests/integrations-serialize.test.ts b/tests/integrations-serialize.test.ts index ae5d07191..c295ec771 100644 --- a/tests/integrations-serialize.test.ts +++ b/tests/integrations-serialize.test.ts @@ -116,6 +116,11 @@ describe("renderToml", () => { expect(renderToml({ k: ["a", 1, true] })).toContain('k = ["a", 1, true]'); }); + test("refuses integers outside JavaScript's safe range", () => { + expect(() => renderToml({ k: [Number.MAX_SAFE_INTEGER + 1] })) + .toThrow(/outside JavaScript's safe range/); + }); + test("still refuses what TOML cannot express inline", () => { // TOML has no null; that is a real limit of the format, not of our renderer. expect(() => renderToml({ k: null })).toThrow(/TOML cannot represent/);