diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index d54d941a2f..c65531a4d3 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -114,6 +114,11 @@ l'application s'arrête et le signale au lieu d'écrire une valeur modifiée en réussi. Le fichier concerné est indiqué et rien n'est déplacé sur le disque. Vous pouvez toujours modifier ce fichier manuellement ; seule la réécriture automatique est refusée. +Les dates et heures TOML empêchent également la réécriture automatique : la fusion +les convertirait en chaînes entre guillemets, y compris dans les tableaux et les +tables en ligne. Les dates déjà écrites entre guillemets restent prises en charge. +Pour conserver une date typée sans guillemets, modifiez manuellement la configuration. + **Pi, Kimi Code, Gajae Code, MiniMax Code et l'intégration DSH gérée fonctionnent uniquement avec une adresse de bouclage.** Les quatre premiers n'ont aucun champ de configuration pour l'en-tête `x-opencodex-api-key` qu'exige une liaison hors bouclage. DSH possède une table d'en-têtes générique, mais rc.6 ne documente pas diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index da1273ad9a..06a98ea33e 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -159,6 +159,11 @@ changed value and calling it success. You will see the file named and nothing on disk will have moved. Editing that file by hand still works; it is only our automatic rewrite that declines. +TOML dates and times also refuse managed rewrites: the merge step would turn these +typed values into quoted strings. This includes values inside arrays and inline +tables. Quoted date strings remain supported; an unquoted date must be preserved +by editing the configuration manually. + **Pi, Kimi Code, Gajae Code, MiniMax Code, Prime Agent and the managed DSH integration only work against a loopback bind.** The first four have no config field for the `x-opencodex-api-key` header a non-loopback bind requires. DSH has a generic headers map, but rc.6 does not document that dedicated admission diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index 636e86af79..fea4b37dd4 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -136,6 +136,11 @@ değişen bir değer yazıp buna başarı demek yerine durur ve bunu söyler. Do adlandırıldığını ve diskte hiçbir şeyin taşınmadığını görürsünüz. Bu dosyayı elle düzenlemek hala çalışır; yalnızca otomatik yeniden yazmamız reddeder. +TOML tarih ve saat değerleri de otomatik yeniden yazmayı engeller: birleştirme adımı, +diziler ve satır içi tablolar dahil bu türlenmiş değerleri tırnaklı metne dönüştürür. +Zaten tırnak içinde yazılmış tarihler desteklenir. Tırnaksız tarih türünü korumak +için yapılandırmayı elle düzenleyin. + **Pi, Kimi Code, Gajae Code, MiniMax Code ve yönetilen DSH entegrasyonu yalnızca geri döngü (loopback) bağlantısına karşı çalışır.** İlk dördünün yapılandırmasında geri döngü olmayan bir bağlantının gerektirdiği `x-opencodex-api-key` başlığı için alan yoktur. DSH genel bir headers haritası sunar, ancak rc.6 diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md index 426cdae162..54751d5620 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -60,6 +60,8 @@ opencodex 從自己的環境讀取這些變數。如果你的 gateway 以 profil **如果某個值無法忠實重寫,開關會拒絕執行。** 往返覆蓋這些格式在實務上會用到的值種類;當它做不到時——例如使用 `inf` 或 `nan` 的 TOML 檔案,我們可用的 parser 無法準確讀回——套用會停止並說明,而不是寫入被改動的值然後宣稱成功。你會看到檔案被指名,磁碟上沒有任何東西被移動。手動編輯那個檔案仍然有效;只有我們的自動重寫會拒絕。 +TOML 日期與時間值也會阻止自動重寫:合併步驟會將這些帶有型別的值轉成加引號的字串,陣列和行內表格中的值也一樣。原本就加引號的日期字串仍受支援;若要保留不加引號的日期型別,請手動編輯設定。 + **Pi、Kimi Code、Gajae Code、MiniMax Code 與受管理 DSH 整合只能對 loopback bind 運作。** 前四者的設定沒有非 loopback bind 所需的 `x-opencodex-api-key` header 欄位。DSH 雖然提供通用 headers map,但 rc.6 並未把這個專用准入 header 記錄為受支援的整合契約,因此受管理 writer 會選擇安全拒絕,而不自行猜測。請改用 SSH tunnel,或由本機 forwarder 加上該 header 後再以 loopback 存取。 **產生的 OMP 整合也刻意只支援 loopback。** OMP 確實支援 provider 層級的 headers,但這個最初的整合不會發出遠端 `x-opencodex-api-key` 憑證連線。手動的遠端 OMP 設定目前不在受管理的整合範圍內。 diff --git a/src/integrations/config-io.ts b/src/integrations/config-io.ts index 4f2a834824..9cb5f97baa 100644 --- a/src/integrations/config-io.ts +++ b/src/integrations/config-io.ts @@ -162,7 +162,22 @@ export function parseConfig(text: string | null, format: ConfigFormat): unknown * evidence is gone. */ if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED; - return Bun.TOML.parse(text); + const document = Bun.TOML.parse(text); + // TOML date/time scalars are Temporal objects with toJSON methods. + // The merge layer JSON-clones documents, which silently turns these + // into strings. Refuse before either status or a writer can admit a + // lossy rewrite, including dates nested in arrays and inline tables. + const pending: unknown[] = [document]; + while (pending.length > 0) { + const value = pending.pop(); + if (value === null || typeof value !== "object") continue; + if (!Array.isArray(value)) { + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) return PARSE_FAILED; + } + for (const child of Object.values(value)) pending.push(child); + } + return document; } } } catch { diff --git a/tests/clients/integrations-state.test.ts b/tests/clients/integrations-state.test.ts index 54ab80de12..872e9b3824 100644 --- a/tests/clients/integrations-state.test.ts +++ b/tests/clients/integrations-state.test.ts @@ -401,6 +401,25 @@ describe("classifier unit behavior", () => { expect(parseConfig("{{{", "json")).toBe(PARSE_FAILED); }); + test("parseConfig refuses typed TOML dates before a JSON clone can turn them into strings", () => { + for (const literal of [ + "2026-09-05T10:00:00Z", + "2026-09-05T10:00:00-07:00", + "2026-09-05T10:00:00.123456", + "2026-09-05", + "10:00:00.123456", + ]) { + for (const text of [ + `expires = ${literal}\n`, + `[user]\nexpires = ${literal}\n`, + `items = [{ expires = ${literal} }]\n`, + ]) { + expect(parseConfig(text, "toml")).toBe(PARSE_FAILED); + } + expect(parseConfig(`expires = "${literal}"\n`, "toml")).toEqual({ expires: literal }); + } + }); + test("parseConfig refuses json number literals a rewrite would change", () => { // Overflow to Infinity — a rewrite would bake in null. expect(parseConfig("{\"a\": 1e999}", "json")).toBe(PARSE_FAILED); diff --git a/tests/clients/integrations-writer.test.ts b/tests/clients/integrations-writer.test.ts index 0bf81fdb54..de2f164710 100644 --- a/tests/clients/integrations-writer.test.ts +++ b/tests/clients/integrations-writer.test.ts @@ -141,6 +141,24 @@ function reverseJsonObjectKeys(value: unknown): unknown { } describe("apply", () => { + test("refuses Kimi TOML date rewrites without changing the file or ownership store", () => { + const spec = INTEGRATION_CLIENTS.kimi; + mkdirSync(spec.detectDir(TEST_ENV, home), { recursive: true }); + const configPath = spec.configPath(TEST_ENV, home); + mkdirSync(dirname(configPath), { recursive: true }); + const original = "[user]\nexpires = 2026-09-05T10:00:00Z\n"; + writeFileSync(configPath, original); + const request = input({ clientId: "kimi" }); + + expect(readIntegrationState(request).state).toBe("unsafe"); + const result = applyIntegration(request); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toBe("unsafe"); + expect(readFileSync(configPath, "utf8")).toBe(original); + expect(store.listOperations()).toHaveLength(0); + expect(store.readRecords().kimi).toBeUndefined(); + }); + test("refuses a client that is not installed, and writes nothing", () => { const result = applyIntegration(input()); expect(result.ok).toBe(false);