From 08c7d3784d0cfa96c61467b8c7a581ea661378e3 Mon Sep 17 00:00:00 2001 From: Hako <25837994+devswha@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:29:36 +0900 Subject: [PATCH 1/2] fix(integrations): refuse lossy TOML date rewrites (cherry picked from commit f6db9cae8e8854c6df06087288a074d767f9787d) --- .../content/docs/fr/guides/integrations.md | 5 +++++ .../src/content/docs/guides/integrations.md | 5 +++++ .../content/docs/tr/guides/integrations.md | 5 +++++ .../content/docs/zh-tw/guides/integrations.md | 2 ++ src/integrations/config-io.ts | 17 ++++++++++++++++- tests/clients/integrations-state.test.ts | 19 +++++++++++++++++++ tests/clients/integrations-writer.test.ts | 18 ++++++++++++++++++ 7 files changed, 70 insertions(+), 1 deletion(-) 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); From e0b5f74b4db8551cf200aca4a9c05b2750375d48 Mon Sep 17 00:00:00 2001 From: t Date: Sun, 6 Sep 2026 01:38:48 +0900 Subject: [PATCH 2/2] docs(integrations): record TOML admission and carry evidence Co-authored-by: Hako <25837994+devswha@users.noreply.github.com> --- .../260906_d_integrations_delivery/011_toml_refresh.md | 8 ++++++++ structure/09_client-integrations.md | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 devlog/_plan/260906_d_integrations_delivery/011_toml_refresh.md diff --git a/devlog/_plan/260906_d_integrations_delivery/011_toml_refresh.md b/devlog/_plan/260906_d_integrations_delivery/011_toml_refresh.md new file mode 100644 index 0000000000..5c2850fa4d --- /dev/null +++ b/devlog/_plan/260906_d_integrations_delivery/011_toml_refresh.md @@ -0,0 +1,8 @@ +# TOML cycle P refresh + +Parent: cb75f49c9401e10f8bd37f4817cdef32b0a5cbe1, documentation PR #3681. +Source: f6db9cae8e8854c6df06087288a074d767f9787d by Hako. + +Read-only git comparison from source parent to the current parent returned no changes in config-io.ts and the two affected client regression files. The 010 diff remains applicable. The shared parser admits both status and writers; no caller-specific exception or new option is required. + +Implementation scope stays as 010. Main will cherry-pick the original commit and add the structure contract. An inherited independent reviewer audits the candidate; no local application tests or typecheck are permitted. Hosted CI supplies runtime verification; docs build may run in a fresh macmini-cf scratch checkout with no real credentials or service changes. diff --git a/structure/09_client-integrations.md b/structure/09_client-integrations.md index 2c2e42b419..0b1f7cc181 100644 --- a/structure/09_client-integrations.md +++ b/structure/09_client-integrations.md @@ -36,6 +36,10 @@ Status and mutation must use the same classifier. A special case added only to a would be misleading because refresh or disable could still reject the same file; a special case added only to a writer would let a mutation bypass the state users saw. +TOML temporal scalars cannot survive the JSON-cloned merge representation with their types +intact. The common parser refuses documents containing them before either status or mutation +proceeds, including nested arrays and inline tables. Quoted date strings remain supported. + ## Fast model selectors The serving proxy resolves `fastRowAvailable` on every management model row, including its