diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 0cc2f6deea2..960e6146bcf 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" dependencies = [ "gimli", ] @@ -28,25 +28,19 @@ dependencies = [ [[package]] name = "alloc-no-stdlib" -version = "2.0.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" [[package]] name = "alloc-stdlib" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.6" @@ -62,15 +56,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -158,13 +143,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -199,7 +184,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -250,14 +235,14 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.8.9", + "miniz_oxide 0.9.1", "object", "rustc-demangle", "windows-link 0.2.1", @@ -304,11 +289,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -344,9 +329,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.4" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -355,9 +340,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -411,7 +396,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "cairo-sys-rs", "glib", "libc", @@ -432,11 +417,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.12" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -459,24 +444,25 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] name = "cargo_toml" -version = "0.22.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" dependencies = [ + "semver", "serde", - "toml 0.9.5", + "toml 1.1.8+spec-1.1.0", ] [[package]] name = "cc" -version = "1.4.7" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "shlex", @@ -490,13 +476,13 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfb" -version = "0.7.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" dependencies = [ - "byteorder", "fnv", "uuid", + "web-time", ] [[package]] @@ -534,22 +520,21 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", @@ -574,6 +559,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -596,8 +591,8 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.13.2", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -609,8 +604,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.13.2", + "core-foundation 0.10.1", "libc", ] @@ -640,9 +635,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] @@ -674,9 +669,9 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ "cssparser-macros", "dtoa-short", @@ -687,29 +682,19 @@ dependencies = [ [[package]] name = "cssparser-macros" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] name = "darling" @@ -731,7 +716,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -742,7 +727,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -758,9 +743,9 @@ dependencies = [ [[package]] name = "defmt" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -768,45 +753,34 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "defmt-parser" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" dependencies = [ - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" dependencies = [ "serde_core", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -850,9 +824,9 @@ dependencies = [ [[package]] name = "dirs" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" dependencies = [ "dirs-sys 0.5.0", ] @@ -886,7 +860,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -900,7 +874,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -928,9 +902,9 @@ dependencies = [ [[package]] name = "dom_query" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" dependencies = [ "bit-set", "cssparser", @@ -965,21 +939,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - [[package]] name = "dunce" version = "1.0.5" @@ -994,16 +953,16 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.8" +version = "3.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.5", + "toml 1.1.8+spec-1.1.0", "vswhom", - "winreg 0.55.0", + "winreg 0.56.0", ] [[package]] @@ -1014,13 +973,12 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.41" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", "core_detect", - "multiversion", "multiversion_no_op", "rustversion", "scopeguard", @@ -1056,17 +1014,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" dependencies = [ "serde", + "serde_core", "typeid", ] @@ -1137,9 +1096,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "flate2" @@ -1182,7 +1141,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1253,7 +1212,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1434,9 +1393,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" [[package]] name = "gio" @@ -1476,7 +1435,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "futures-channel", "futures-core", "futures-executor", @@ -1633,9 +1592,9 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", @@ -1682,13 +1641,12 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", - "futures-channel", "futures-core", "http", "http-body", @@ -1702,9 +1660,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.9" +version = "0.27.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" dependencies = [ "http", "hyper", @@ -1718,25 +1676,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.5", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1765,19 +1726,19 @@ dependencies = [ [[package]] name = "ico" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" dependencies = [ "byteorder", - "png 0.17.16", + "png", ] [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" dependencies = [ "displaydoc", "potential_utf", @@ -1789,9 +1750,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", @@ -1802,9 +1763,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1816,15 +1777,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" dependencies = [ "displaydoc", "icu_collections", @@ -1837,15 +1798,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", @@ -1893,7 +1854,7 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png 0.18.1", + "png", ] [[package]] @@ -1921,9 +1882,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" dependencies = [ "cfb", ] @@ -1984,9 +1945,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" dependencies = [ "defmt", "jiff-core", @@ -2011,9 +1972,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" dependencies = [ "jiff-core", "proc-macro2", @@ -2023,9 +1984,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" [[package]] name = "jiff-tzdb-platform" @@ -2052,6 +2013,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2082,9 +2073,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -2093,21 +2084,21 @@ dependencies = [ [[package]] name = "json-patch" -version = "3.0.1" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" dependencies = [ "jsonptr", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.21", ] [[package]] name = "jsonptr" -version = "0.6.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" dependencies = [ "serde", "serde_json", @@ -2115,13 +2106,12 @@ dependencies = [ [[package]] name = "keyboard-types" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "serde", - "unicode-segmentation", ] [[package]] @@ -2150,9 +2140,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.189" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libdbus-sys" @@ -2175,9 +2165,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] @@ -2217,9 +2207,9 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "markup5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", @@ -2249,9 +2239,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minisign-verify" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" +checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce" [[package]] name = "miniz_oxide" @@ -2275,9 +2265,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi", @@ -2296,9 +2286,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.3" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" dependencies = [ "crossbeam-channel", "dpi", @@ -2309,33 +2299,12 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png 0.18.1", + "png", "serde", - "thiserror 2.0.20", + "thiserror 2.0.21", "windows-sys 0.61.2", ] -[[package]] -name = "multiversion" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" -dependencies = [ - "multiversion-macros", -] - -[[package]] -name = "multiversion-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" -dependencies = [ - "proc-macro2", - "quote", - "rustversion", - "syn 3.0.6", -] - [[package]] name = "multiversion_no_op" version = "1.0.0" @@ -2348,7 +2317,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2357,6 +2326,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -2372,6 +2347,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2403,7 +2390,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.119", @@ -2411,9 +2398,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2425,11 +2412,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", "objc2", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", ] [[package]] @@ -2438,7 +2426,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "objc2", "objc2-foundation", ] @@ -2459,7 +2447,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "dispatch2", "objc2", ] @@ -2470,7 +2458,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "dispatch2", "objc2", "objc2-core-foundation", @@ -2503,7 +2491,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -2530,7 +2518,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -2543,7 +2531,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", ] @@ -2554,7 +2542,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "objc2", "objc2-app-kit", "objc2-foundation", @@ -2566,7 +2554,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2578,7 +2566,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", "objc2", "objc2-cloud-kit", @@ -2609,7 +2597,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", "objc2", "objc2-app-kit", @@ -2619,9 +2607,9 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" dependencies = [ "memchr", ] @@ -2634,9 +2622,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.4.4" +version = "5.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" dependencies = [ "dunce", "is-wsl", @@ -2645,7 +2633,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.97.0" +version = "2.98.0" dependencies = [ "base64 0.22.1", "dbus", @@ -2668,6 +2656,12 @@ dependencies = [ "winreg 0.55.0", ] +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + [[package]] name = "option-ext" version = "0.2.0" @@ -2705,7 +2699,7 @@ dependencies = [ "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -2846,37 +2840,24 @@ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plist" -version = "1.8.0" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "indexmap 2.14.2", "quick-xml", "serde", "time", ] -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - [[package]] name = "png" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", @@ -2923,9 +2904,9 @@ dependencies = [ [[package]] name = "powerfmt" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" [[package]] name = "precomputed-hash" @@ -2955,11 +2936,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.4", + "toml_edit 0.25.17+spec-1.1.0", ] [[package]] @@ -3003,9 +2984,9 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] @@ -3024,7 +3005,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.5", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -3032,9 +3013,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "bytes", "getrandom 0.4.3", @@ -3046,7 +3027,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.20", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -3054,9 +3035,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ "cfg_aliases", "libc", @@ -3089,9 +3070,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -3125,7 +3106,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", ] [[package]] @@ -3146,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" dependencies = [ "libredox", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -3166,7 +3147,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3207,7 +3188,6 @@ dependencies = [ "base64 0.22.1", "bytes", "futures-core", - "futures-util", "http", "http-body", "http-body-util", @@ -3227,14 +3207,12 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams 0.4.2", "web-sys", "webpki-roots", ] @@ -3253,15 +3231,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3269,7 +3252,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams 0.5.0", + "wasm-streams", "web-sys", ] @@ -3310,11 +3293,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -3335,6 +3318,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -3345,11 +3340,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -3377,6 +3399,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3406,9 +3437,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" dependencies = [ "dyn-clone", "ref-cast", @@ -3434,13 +3465,36 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" -version = "0.36.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "cssparser", "derive_more", "log", @@ -3455,11 +3509,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -3474,12 +3529,13 @@ dependencies = [ [[package]] name = "serde-untagged" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ "erased-serde", "serde", + "serde_core", "typeid", ] @@ -3500,7 +3556,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3535,7 +3591,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3549,11 +3605,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3570,9 +3626,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" dependencies = [ "base64 0.23.1", "bs58", @@ -3582,7 +3638,7 @@ dependencies = [ "indexmap 2.14.2", "jiff", "schemars 0.9.0", - "schemars 1.2.2", + "schemars 1.2.3", "serde_core", "serde_json", "serde_with_macros", @@ -3591,14 +3647,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" dependencies = [ "darling", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3673,9 +3729,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e" dependencies = [ "libc", "signal-hook-registry", @@ -3697,6 +3753,16 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -3705,9 +3771,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -3717,9 +3783,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.1" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "socket2" @@ -3795,6 +3861,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_vcruntime" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" + [[package]] name = "string_cache" version = "0.9.0" @@ -3833,9 +3905,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "swift-rs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" dependencies = [ "base64 0.21.7", "serde", @@ -3865,9 +3937,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -3891,7 +3963,28 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3909,13 +4002,13 @@ dependencies = [ [[package]] name = "tao" -version = "0.35.3" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -3925,24 +4018,24 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", "ndk", + "ndk-context", "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", "objc2-ui-kit", - "once_cell", "parking_lot", "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", "url", - "windows", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] @@ -3977,14 +4070,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.6" +version = "2.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" +checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" dependencies = [ "anyhow", "bytes", "cookie", - "dirs 6.0.0", + "dirs 7.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3993,7 +4086,7 @@ dependencies = [ "heck 0.5.0", "http", "image", - "jni", + "jni 0.21.1", "libc", "log", "mime", @@ -4017,25 +4110,25 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tray-icon", "url", "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.62.2", ] [[package]] name = "tauri-build" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" dependencies = [ "anyhow", "cargo_toml", - "dirs 6.0.0", + "dirs 7.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4043,6 +4136,8 @@ dependencies = [ "semver", "serde", "serde_json", + "static_vcruntime", + "strsim", "tauri-utils", "tauri-winres", "walkdir", @@ -4050,16 +4145,16 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "brotli", "ico", "json-patch", "plist", - "png 0.17.16", + "png", "proc-macro2", "quote", "semver", @@ -4068,7 +4163,7 @@ dependencies = [ "sha2", "syn 2.0.119", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", "url", "uuid", @@ -4077,9 +4172,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4091,9 +4186,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.6.3" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +checksum = "67fb21a77e8f94e8beb3325e583102488659365af0a3948b7ea244c5b779c291" dependencies = [ "anyhow", "glob", @@ -4116,7 +4211,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.20", + "thiserror 2.0.21", ] [[package]] @@ -4135,9 +4230,9 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.20", + "thiserror 2.0.21", "url", - "windows", + "windows 0.61.3", "zbus", ] @@ -4168,7 +4263,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", ] @@ -4181,7 +4276,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "thiserror 2.0.20", + "thiserror 2.0.21", "tracing", "windows-sys 0.60.2", "zbus", @@ -4189,21 +4284,23 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b" dependencies = [ - "base64 0.22.1", - "dirs 6.0.0", + "base64 0.23.1", + "dirs 7.0.0", "flate2", "futures-util", "http", "infer", + "libc", "log", "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.12.24", + "reqwest 0.13.5", + "rustls", "semver", "serde", "serde_json", @@ -4211,25 +4308,25 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.20", + "thiserror 2.0.21", "time", "tokio", "url", - "windows-sys 0.60.2", + "windows-sys 0.61.2", "zip", ] [[package]] name = "tauri-runtime" -version = "2.11.3" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" dependencies = [ "cookie", "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -4237,26 +4334,25 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.20", + "thiserror 2.0.21", "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.62.2", ] [[package]] name = "tauri-runtime-wry" -version = "2.11.4" +version = "2.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", - "once_cell", "percent-encoding", "raw-window-handle", "softbuffer", @@ -4266,15 +4362,15 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.62.2", "wry", ] [[package]] name = "tauri-utils" -version = "2.9.3" +version = "2.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" dependencies = [ "anyhow", "brotli", @@ -4300,8 +4396,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.20", - "toml 0.9.5", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4310,13 +4406,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796" dependencies = [ "dunce", "embed-resource", - "toml 0.9.5", + "toml 1.1.8+spec-1.1.0", ] [[package]] @@ -4352,11 +4448,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.21", ] [[package]] @@ -4372,13 +4468,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4413,9 +4509,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", "zerovec", @@ -4444,9 +4540,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -4479,17 +4575,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.5" +version = "1.1.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" dependencies = [ "indexmap 2.14.2", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow 1.0.4", ] [[package]] @@ -4503,11 +4599,11 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -4536,30 +4632,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.4" +version = "0.25.17+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" dependencies = [ "indexmap 2.14.2", - "toml_datetime 0.7.0", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", - "winnow 0.7.15", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -4582,7 +4678,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -4639,12 +4735,12 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" dependencies = [ "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "libappindicator", "muda", "objc2", @@ -4653,9 +4749,9 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png 0.18.1", + "png", "serde", - "thiserror 2.0.20", + "thiserror 2.0.21", "windows-sys 0.61.2", ] @@ -4665,6 +4761,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typeid" version = "1.0.3" @@ -4688,52 +4790,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-segmentation" @@ -4762,13 +4823,13 @@ dependencies = [ [[package]] name = "urlpattern" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" dependencies = [ + "icu_properties", "regex", "serde", - "unic-ucd-ident", "url", ] @@ -4834,9 +4895,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -4858,9 +4919,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -4871,19 +4932,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.78" +version = "0.4.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4891,39 +4953,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.128" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -4939,9 +4988,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.105" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -5013,27 +5062,34 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] [[package]] name = "webview2-com" -version = "0.38.2" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -5049,13 +5105,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.38.2" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" dependencies = [ - "thiserror 2.0.20", - "windows", - "windows-core 0.61.2", + "thiserror 2.0.21", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -5091,16 +5147,17 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window-vibrancy" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" dependencies = [ "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "windows-sys 0.59.0", + "windows-sys 0.61.2", "windows-version", ] @@ -5110,11 +5167,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5126,6 +5195,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -5160,7 +5238,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -5207,6 +5296,27 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5345,6 +5455,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -5501,15 +5620,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.4" @@ -5538,6 +5648,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -5552,15 +5672,15 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wry" -version = "0.55.1" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "block2", "cookie", "crossbeam-channel", - "dirs 6.0.0", + "dirs 7.0.0", "dom_query", "dpi", "dunce", @@ -5568,7 +5688,7 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", "ndk", "objc2", @@ -5577,19 +5697,18 @@ dependencies = [ "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", - "once_cell", "percent-encoding", "raw-window-handle", "sha2", "soup3", "tao-macros", - "thiserror 2.0.20", + "thiserror 2.0.21", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", "windows-version", "x11-dl", ] @@ -5617,9 +5736,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +checksum = "963d6fab92d5504b971dc8bd90ca743c2575e590bc8071ea5bc571598667ae08" dependencies = [ "libc", "rustix", @@ -5627,9 +5746,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5638,13 +5757,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -5689,10 +5808,10 @@ version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "zbus_names", "zvariant", "zvariant_utils", @@ -5700,9 +5819,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.4" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" dependencies = [ "serde", "winnow 1.0.4", @@ -5735,15 +5854,15 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zerotrie" @@ -5758,9 +5877,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ "yoke", "zerofrom", @@ -5769,25 +5888,28 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "zip" -version = "4.6.1" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb" dependencies = [ - "arbitrary", "crc32fast", + "getrandom 0.4.3", "indexmap 2.14.2", "memchr", + "nix", + "typed-path", + "windows-sys 0.61.2", ] [[package]] @@ -5804,9 +5926,9 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" -version = "5.15.0" +version = "5.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" dependencies = [ "endi", "enumflags2", @@ -5819,14 +5941,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.15.0" +version = "5.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "zvariant_utils", ] @@ -5839,6 +5961,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 3.0.6", + "syn 3.0.9", "winnow 1.0.4", ] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index a0ecc4f7a0a..9707db5345a 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "opencodex-desktop" -version = "2.97.0" +version = "2.98.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" edition = "2021" -rust-version = "1.88" +rust-version = "1.90" [lib] name = "opencodex_desktop_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "=2.6.3", features = [] } +tauri-build = { version = "=2.7.4", features = [] } [dependencies] base64 = "=0.22.1" @@ -22,14 +22,14 @@ serde = { version = "=1.0.229", features = ["derive"] } serde_json = "=1.0.151" sha2 = "=0.10.9" uuid = { version = "=1.18.1", features = ["v4"] } -tauri = { version = "=2.11.6", features = ["tray-icon", "image-png", "macos-private-api"] } -tauri-utils = "=2.9.3" +tauri = { version = "=2.12.5", features = ["tray-icon", "image-png", "macos-private-api"] } +tauri-utils = "=2.10.2" tauri-plugin-autostart = "=2.5.0" tauri-plugin-opener = "=2.5.3" tauri-plugin-process = "=2.3.0" tauri-plugin-shell = "=2.2.1" tauri-plugin-single-instance = "=2.4.0" -tauri-plugin-updater = "=2.9.0" +tauri-plugin-updater = "=2.14.1" tokio = { version = "=1.45.1", features = ["sync", "time"] } # Linux only, and already in this graph: `tao` enables its own `dbus` feature by default, so diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 7a63a3e2b67..544a26fe31b 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.97.0", + "version": "2.98.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 8bdfd730b12..1ef8e55d72c 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -134,6 +134,16 @@ configuration de référence et de repli. Sur l'interface de bouclage, ce fichie vous pouvez fusionner manuellement si l'injection automatique a été supprimée ; hors bouclage, il contient la forme avec fournisseur dédié. Le mode de fournisseur externe laisse ce profil intact. +Quand ce fichier est régénéré, les clés racine simples qu'OpenCodex ne gère pas sont conservées : un réglage +que vous ajoutez, comme `stream_idle_timeout_ms` ou `stream_max_retries`, survit à l'injection suivante. +Chaque affectation conservée est copiée telle quelle, y compris les valeurs sur plusieurs lignes et les entiers +64 bits, et placée avant la première table. Les clés à l'intérieur des tables et les clés racine pointées comme +`a.b = 1` ne sont pas conservées. En cas de conflit, OpenCodex l'emporte toujours : les clés racine qu'il gère +(`model_provider`, `model_catalog_json`, `openai_base_url`) sont réécrites pour le mode de routage actif, et +une clé racine portant le nom d'une table écrite par le profil, par exemple un `features = { ... }` en ligne +quand le profil écrit `[features]`, est supprimée. Si le fichier change pendant qu'une injection attend le +verrou d'écriture, l'injection s'arrête avec une erreur réessayable et laisse votre modification en place. + :::caution Les clés racine telles que `openai_base_url`, `model_provider` et `model_catalog_json` **doivent** précéder le premier en-tête `[table]`. L'injecteur garantit ce placement, supprime ses propres copies obsolètes ou en double diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md index a123bf34797..f014dd0ecd6 100644 --- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md @@ -32,6 +32,8 @@ ocx start --socks5-off ### `ocx stop` +Un Link Child non supervisé restaure Codex natif et Grok avant de libérer son listener lors d’un signal, en revérifiant la connexion à chaque écriture. Service/Desktop, sibling, transfert de redémarrage et reçu stop du parent préservent les intégrations. L’arrêt refuse le redémarrage avec `409 runtime_stopping`. Une terminaison forcée peut éviter le signal ; la restauration par `ocx stop` reste nécessaire. + Arrête le proxy actif à partir de son PID, supprime le fichier de PID et rétablit le fonctionnement natif de Codex. Si un service d’arrière-plan géré est installé, `ocx stop` l’arrête d’abord afin qu’il ne puisse pas relancer le proxy. Le bouton **Stop** du tableau de bord Web exécute la même opération (`POST /api/stop`) sur tous les backends, sauf le Planificateur de tâches Windows : le wrapper peut y relancer le proxy après la fin de la tâche, donc le tableau de bord refuse avec `respawnable_service`, ne modifie rien et vous demande d'exécuter `ocx stop`. Sur un client hub connecté, `ocx stop`, y compris via l’API, conserve le routage Codex, le catalogue et la configuration Grok appartenant à la connexion ; `ocx start` les dessert de nouveau. L’arrêt explicite d’un Remote Link Child rétablit au contraire, si la restauration partagée réussit, le routage natif de Codex, le catalogue et les métadonnées de reprise sauvegardées, et supprime la configuration Grok gérée. La connexion Home enregistrée reste conservée ; `ocx disconnect` la supprime. diff --git a/docs-site/src/content/docs/fr/reference/configuration/agents.md b/docs-site/src/content/docs/fr/reference/configuration/agents.md index e73e248598f..6053240fb55 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -34,6 +34,8 @@ L’API de gestion expose `GET`/`PUT /api/v2`, `/api/injection-model`, `/api/eff La page Codex Auth permet également de modifier l’indicateur natif `default_mode_request_user_input` de Codex (`GET`/`PUT /api/codex-auth/features/default-mode-request-user-input`). Son activation ajoute `[features] default_mode_request_user_input = true` au fichier `$CODEX_HOME/config.toml` de Codex au moyen de la commande officielle `codex features enable|disable`. Cette modification préserve le format du fichier et est retirée lorsque l’option est désactivée. Codex peut ainsi suspendre une session en mode Default et vous poser des questions avec l’outil `request_user_input`. Cet indicateur est encore en développement en amont et ne s’applique qu’aux nouvelles sessions ; l’activation échoue explicitement si la version installée de Codex ne le reconnaît pas encore. +Pendant qu’une mise à jour de Codex CLI détient l’installation, cette commande renvoie HTTP 503 avec `codex_cli_selection_busy` et `retryable: true` avant toute exécution. Réessayez après la mise à jour ; les erreurs d’écriture de la configuration restent des réponses 502. + ## Liste des modèles et consignes La liste v2 effective comprend, parmi les modèles configurés, les cinq premiers qui sont visibles dans le sélecteur, classés par priorité, compatibles avec v2 et présents dans le catalogue injecté. Un modèle est admissible en v2 si son choix imposé en amont vaut explicitement `"v2"`, vaut `null` ou est absent. Un véritable choix `"v1"` l’exclut. Les entrées exclues restent dans la configuration afin de pouvoir redevenir admissibles ultérieurement. diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 5e656662799..0b75d49d6a4 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -412,6 +412,16 @@ reference/fallback config. On loopback it contains the root keys you can merge m injection was removed; on non-loopback it contains the dedicated provider form. External-provider mode leaves this profile untouched. +Rebuilding that file keeps the plain root keys OpenCodex does not manage, so a setting you add such as +`stream_idle_timeout_ms` or `stream_max_retries` survives the next injection. Each kept assignment is +copied exactly as written, including values that span several lines and 64-bit integers, and is placed +above the first table. Keys inside tables and dotted root keys such as `a.b = 1` are not kept. +OpenCodex wins every collision: the root keys it owns (`model_provider`, `model_catalog_json`, +`openai_base_url`) are rewritten for the routing mode in use, and a root key named like a table the +profile writes, such as an inline `features = { ... }` when the profile writes `[features]`, is +dropped. If the file changes while an injection waits for the write lock, the injection stops with a +retryable error and leaves your edit in place. + :::caution Root keys such as `openai_base_url`, `model_provider`, and `model_catalog_json` **must** sit before the first `[table]` header. The injector guarantees that placement, removes its own stale/duplicate @@ -760,6 +770,12 @@ Codex. Native custom calls and converted function calls use the same completion patch previews are held while their executable form is unresolved. JavaScript that merely contains patch text and unrelated native custom payloads stay unchanged. +When a routed model double-wraps an executable freeform body, opencodex removes one extra +JSON object containing only an input string. This repair applies to local exec and apply_patch +bodies, leaves other tool grammars and ambiguous objects unchanged, and does not decode recursively. +A body holds its streamed preview only while it can still turn out to be such a wrapper; +JavaScript that merely starts with an object or block keeps streaming as it arrives. + A routed model can also mistakenly send a shell-argument object such as `{"cmd":"git status --short"}` to code-mode `exec`. For a verified code-mode catalog, opencodex converts an unambiguous shell object into `tools.exec_command(...)` JavaScript diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index e98c4098d95..b2d078d3a0b 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -654,6 +654,8 @@ If Kiro ends with plain text instead of its private final-answer tool, one bound still runs, and only the resulting final answer is displayed. Progress accompanying a real tool call remains visible. A normal private final answer needs no completion retry. +When Kiro's safety filter stops a turn (`CONTENT_FILTERED`, for example the Claude `BIO` classifier), the request ends as a refusal that clients do not retry: Claude clients see `stop_reason: "refusal"` and Codex sees a failed response carrying Kiro's category and explanation. If the filter stops the bounded retry, no final answer is displayed; the progress held from the first attempt and any partial text from the retry are shown as commentary before the refusal. + When Kiro emits credit metering, request logs preserve the reported spend as `usage.providerCredits`, including in the persisted usage ledger. These are Kiro credits; token counts may still be estimated, and the credit value does not replace USD cost estimates. @@ -1837,6 +1839,16 @@ Google Antigravity, OpenCode Go, OpenRouter, DeepSeek, ClinePass, Z.AI, MiniMax, Moonshot, Venice, Synthetic, DeepInfra, Neuralwatt, Command Code, and any a6api-backed or Abliteration custom provider. +**Kimi Coding Plan quota.** Account login and API-key providers on the canonical +`https://api.kimi.com/coding/v1` endpoint read `/usages`. Plans that report +`usages.limit_month_total` (or `monthTotal`) show a monthly utilization bar, with +`limit_month_code` (or `monthCode`) as the fallback when total usage is unavailable. +The `used_ratio` / `usedRatio` fraction must be in `[0, 1]` before conversion to a consumed +percentage; invalid ratios are ignored. Empty or unusable `usages` containers do not hide +usable monthly data in another supported envelope. Any reported reset time is retained. +Existing 5-hour, weekly, and total subscription-credit +windows remain separate, and an absent monthly window is not shown as 0%. + **Abliteration credits.** A provider of any name whose `baseUrl` is `https://api.abliteration.ai` or `https://api.abliteration.ai/v1` reads `GET https://api.abliteration.ai/v1/credits` with its active key as a Bearer token and does not diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index 908fdd1bbc6..57d16985e00 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -94,6 +94,8 @@ env_key = "OPENCODEX_API_AUTH_TOKEN" OpenCodex がルーティングを所有している場合、どちらのモードも参照/フォールバック設定として `$CODEX_HOME/opencodex.config.toml` を書き込みます。ループバックでは、自動挿入が削除された場合に手動でマージできるルート キーが含まれています。非ループバックでは、専用のプロバイダー フォームが含まれます。外部プロバイダー モードでは、このプロファイルは変更されません。 +このファイルを再生成しても、OpenCodex が管理しない単純なルート キーは保持されます。そのため、`stream_idle_timeout_ms` や `stream_max_retries` のように追加した設定は次の挿入後も残ります。保持される代入は、複数行にわたる値や 64 ビット整数も含めて書かれたとおりにコピーされ、最初のテーブルより前に置かれます。テーブル内のキーと、`a.b = 1` のようなドット付きルート キーは保持されません。衝突した場合は常に OpenCodex が優先されます。OpenCodex が所有するルート キー(`model_provider`、`model_catalog_json`、`openai_base_url`)は使用中のルーティング モードに合わせて書き直され、プロファイルが書き込むテーブルと同じ名前のルート キー(プロファイルが `[features]` を書き込むときのインライン `features = { ... }` など)は削除されます。挿入が書き込みロックを待っている間にファイルが変更された場合、挿入は再試行可能なエラーで停止し、編集内容はそのまま残ります。 + :::caution `openai_base_url`、`model_provider`、`model_catalog_json` などのルート キーは、最初の `[table]` ヘッダーの前に**なければなりません**。インジェクターはその配置を保証し、それ自身の古い/重複したコピーを削除し、ユーザー所有のルート `openai_base_url` を決して上書きしません。存在する場合、sync はカタログを更新しますが、ルーティングが挿入されなかったことを報告します。 ::: diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index 73020dc2fc0..06103b47bc8 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -28,6 +28,8 @@ ocx start --socks5-off ### `ocx stop` +監督プロセスのない Link Child は終了シグナル時、各書き込みで接続を再確認し、リスナーを閉じる前に Codex と管理対象 Grok を復元します。サービス・Desktop、sibling、再起動の引き継ぎ、親 stop 記録がある場合は設定を保持します。終了中の再起動は `409 runtime_stopping` です。強制終了はシグナル処理を省略する場合があり、親 `ocx stop` の復元も必要です。 + 実行中のプロキシを (PID によって) 停止し、PID ファイルを削除します。スタンドアロンの所有者ではネイティブ Codex を復元します。マネージド バックグラウンド サービスがインストールされている場合、`ocx stop` はそれを最初に停止するため、プロキシを再起動できません。Web ダッシュボードの **停止** ボタンは同じ処理 (`POST /api/stop`) を実行しますが、Windows タスク スケジューラだけは例外です。タスク終了後もラッパーがプロキシを再起動しうるため、ダッシュボードは `respawnable_service` で拒否し、何も変更せずに `ocx stop` の実行を促します。 接続済みの hub クライアントでは、`ocx stop`(成功した API 停止を含む)は接続が所有する Codex のルーティング、モデルカタログ、Grok 設定を保持します。`ocx start` はルーティングを再注入せず、その設定で再び処理します。Codex を自身のアカウントへ戻すには `ocx disconnect`、既にネイティブルーティングへ戻した後の再適用には `ocx sync --restart-codex` を使います。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 717a24f1da0..9de4a3d9e7f 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -196,6 +196,8 @@ env_key = "OPENCODEX_API_AUTH_TOKEN" OpenCodex가 라우팅을 소유할 때는 두 모드 모두 `$CODEX_HOME/opencodex.config.toml`을 참고용/폴백 설정으로 작성합니다. loopback에서는 자동 주입이 사라졌을 때 수동으로 합칠 수 있는 root key가 들어가고, non-loopback에서는 전용 provider 형식이 들어갑니다. 외부 provider 모드는 이 프로필을 건드리지 않습니다. +이 파일을 다시 만들 때 OpenCodex가 관리하지 않는 일반 root key는 그대로 남습니다. 그래서 `stream_idle_timeout_ms`나 `stream_max_retries`처럼 직접 추가한 설정은 다음 주입 뒤에도 유지됩니다. 남는 대입문은 여러 줄에 걸친 값과 64비트 정수까지 작성한 그대로 복사되고, 첫 번째 테이블보다 앞에 놓입니다. 테이블 안의 키와 `a.b = 1` 같은 점 표기 root key는 남지 않습니다. 충돌하면 항상 OpenCodex 쪽이 이깁니다. OpenCodex가 소유한 root key(`model_provider`, `model_catalog_json`, `openai_base_url`)는 현재 라우팅 모드에 맞게 다시 쓰이고, 프로필이 쓰는 테이블과 이름이 같은 root key(프로필이 `[features]`를 쓸 때의 인라인 `features = { ... }` 등)는 버려집니다. 주입이 쓰기 잠금을 기다리는 동안 파일이 바뀌면 주입은 재시도 가능한 오류로 멈추고 수정한 내용은 그대로 둡니다. + :::caution `openai_base_url`, `model_provider`, `model_catalog_json` 같은 root key는 첫 번째 `[table]` 헤더보다 **반드시** 앞에 있어야 합니다. 인젝터는 그 위치를 보장하고, 자신이 남긴 오래되었거나 중복된 복사본은 지웁니다. 사용자가 소유한 root `openai_base_url`은 덮어쓰지 않습니다. 그런 값이 있으면 sync는 카탈로그만 갱신하고 라우팅은 주입하지 않았다고 알립니다. ::: diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 90512b785e1..40ecde6889f 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -44,6 +44,8 @@ ocx start --socks5-off ### `ocx stop` +감독 프로세스 없이 실행되는 Link Child는 종료 신호 처리 시 각 쓰기 직전에 연결을 재확인하고 리스너를 닫기 전에 Codex 기본 설정과 관리 Grok 설정을 복원합니다. 서비스·Desktop 감독, sibling, 재시작·재활용 인계, 부모 stop 영수증이 있으면 설정을 보존합니다. 종료 중 재시작 요청은 `409 runtime_stopping`입니다. 강제 종료는 신호 처리를 건너뛸 수 있어 부모 `ocx stop`의 복원도 필요합니다. + 실행 중인 프록시를 PID 기준으로 중지하고 PID 파일을 삭제합니다. 독립 실행 소유자라면 기본 Codex를 복원합니다. 관리형 백그라운드 서비스가 설치되어 있으면 `ocx stop`이 먼저 그 서비스를 중지하므로 프록시가 다시 올라올 수 없습니다. 웹 대시보드의 **Stop** 버튼도 같은 동작(`POST /api/stop`)을 하지만, Windows 작업 스케줄러는 예외입니다. 작업이 끝나도 래퍼가 프록시를 다시 띄울 수 있어서, 대시보드는 `respawnable_service`로 거절하고 아무것도 바꾸지 않은 채 `ocx stop` 실행을 안내합니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 58e544a041b..f95272a73cf 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -604,6 +604,8 @@ real client tool call keeps the turn open. A completion-tool answer is always em `final_answer`, even when it exactly repeats prior commentary, because phase correctness is more important than cosmetic de-duplication. Tool-free requests retain normal text completion behavior. +The continuation's instruction tells the model that its ordinary text has not reached the user, and asks it to call `codex_kiro_final_answer` with the complete final answer if the task is complete, submitting its previous reply unchanged when that reply already was the answer. A model that answered a trivial request in plain text therefore keeps its answer, instead of being told to continue a task it had already finished. + ### Reasoning effort The GPT-5.6 family uses `additionalModelRequestFields.reasoning.effort`; `claude-opus-5` diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 11b7f0269cf..8d84eb48a54 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -990,6 +990,8 @@ Alternatively, supply the key yourself, for example by exporting `OPENCODEX_API_ Use `--` before a single-word prompt so it cannot select an unknown Codex subcommand. +Automatic credentials also require a fresh ownership proof from this installation's running listener. For a standalone proxy, the proof must come from the exact HTTP origin in the managed Codex provider configuration; a running proxy on a different port or loopback address does not authorize that destination. A direct client proves its own machine listener and keeps its saved server URL checks, because the server it dials is a different installation. For a direct client the proof shows that the answering listener belongs to this installation; it does not prove which kind of listener it is. Without a matching proof, the launcher withholds the automatic token and leaves routing unchanged. + The machine configuration namespace must also prevent other users from creating missing system files. An unprotected Windows ProgramData/OpenAI/Codex namespace withholds automatic credentials; the launcher does not create it. macOS accepts deny-only and read-only allow ACLs, but refuses any allow entry granting write, append, delete, attribute/security changes or ownership changes, even after a deny entry. Unknown tags and inspection errors refuse. A fresh service token is loaded only when argv is within the allowlist below, the environment has no nonempty HTTP(S)/ALL proxy or `CODEX_*`/`OPENAI_*`/`AZURE_OPENAI_*` overrides (canonical `CODEX_HOME` is allowed), and the home/config/system namespaces pass ownership checks. The configuration must contain only audited keys and the exact managed loopback provider, with no additional credential consumers or configuration layers. Every unsupported argument, including `-c`/`--config`, `-p`/`--profile`, unknown flags and `resume`/`fork`, withholds the token with a value-free diagnostic; explicit user credentials and original arguments are preserved. diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 9dc04b30b64..b557c2dfcb5 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -82,6 +82,8 @@ ocx start --socks5-off ### `ocx stop [--json]` +An unsupervised Link Child handling a shutdown signal restores native Codex and managed Grok before releasing its listener, rechecking the served connection before each write. Service/Desktop supervision, sibling ownership, restart/recycle handoff and pending parent stop receipts preserve integrations. Shutdown rejects bound restart with `409 runtime_stopping`. Forced termination can bypass signal handlers; parent `ocx stop` restoration remains necessary. To route Codex through Home again after such a stop, start the Child (`ocx start`, or `ocx service start` for an installed service) and then run `ocx sync --restart-codex`. Starting alone does not reapply routing. A sync run while the Child is stopped still reapplies routing, which points Codex at a listener that is not running, so start the Child first. + Stop the running proxy (by PID), remove the PID file, and restore native Codex for standalone owners. If a managed background service is installed, `ocx stop` also stops it first so it cannot respawn the proxy. On a connected hub client, `ocx stop` leaves Codex routing, its diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index ea036536740..3ba3ee04605 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -115,6 +115,8 @@ Default-mode session and ask you questions with the `request_user_input` tool. T flag is under development upstream and only applies to new sessions; the toggle fails loudly when the installed Codex build does not know the flag yet. +While a Codex CLI update owns the installation, the toggle returns retryable HTTP 503 (`codex_cli_selection_busy`) before running the feature command. Retry after the update finishes; config-write failures still return 502. + ## Roster and guidance The effective v2 roster is the configured, picker-visible, priority-sorted first five models that diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 884eec3e4da..035244e70cb 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -137,6 +137,16 @@ reference/fallback-конфиг. На loopback в нём лежат root key, к если автоматическое внедрение убрали; на не-loopback — форма с выделенным провайдером. Режим external-provider этот профиль не трогает. +При пересоздании файла простые root key, которыми OpenCodex не управляет, сохраняются, поэтому +добавленная вами настройка вроде `stream_idle_timeout_ms` или `stream_max_retries` переживает следующее +внедрение. Каждое сохранённое присваивание копируется как есть, включая многострочные значения и 64-битные +целые, и ставится перед первой таблицей. Ключи внутри таблиц и root key с точками вроде `a.b = 1` не +сохраняются. При конфликте всегда побеждает OpenCodex: его root key (`model_provider`, +`model_catalog_json`, `openai_base_url`) переписываются под текущий режим маршрутизации, а root key с +именем таблицы, которую пишет профиль (например, inline `features = { ... }`, когда профиль пишет +`[features]`), отбрасывается. Если файл меняется, пока внедрение ждёт блокировку записи, внедрение +останавливается с ошибкой, которую можно повторить, и ваша правка остаётся на месте. + :::caution Root key вроде `openai_base_url`, `model_provider` и `model_catalog_json` **обязаны** располагаться до первого заголовка `[table]`. Injector гарантирует это размещение, удаляет собственные diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index 2ecf0d288cc..390aae87e90 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -50,6 +50,8 @@ ocx start --socks5-off ### `ocx stop` +Link Child без супервизора при сигнале восстанавливает нативный Codex и управляемый Grok до закрытия listener, проверяя подключение перед каждой записью. Сервис/Desktop, sibling, передача перезапуска и квитанция stop родителя сохраняют настройки. Во время завершения перезапуск возвращает `409 runtime_stopping`. Принудительное завершение может обойти сигнал; восстановление родителем `ocx stop` остаётся необходимым. + Остановить работающий прокси (по PID) и удалить PID-file; для автономного владельца также восстановить native Codex. Если установлена managed background service, `ocx stop` сначала останавливает и её, чтобы она не перезапустила прокси обратно. Кнопка **Stop** в веб-дашборде выполняет то же действие (`POST /api/stop`) на всех бэкендах, кроме планировщика заданий Windows: там обёртка может перезапустить прокси после завершения задачи, поэтому дашборд отказывает с `respawnable_service`, ничего не меняет и просит выполнить `ocx stop`. diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index fce593de37c..30862e73896 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -150,6 +150,19 @@ döngüde otomatik enjeksiyon kaldırılmışsa manuel olarak birleştirebilece kök anahtarları içerir; geri döngü olmayanda ise özel sağlayıcı formunu içerir. Harici sağlayıcı modu bu profile dokunmaz. +Bu dosya yeniden oluşturulurken OpenCodex'in yönetmediği düz kök anahtarlar +korunur; böylece `stream_idle_timeout_ms` veya `stream_max_retries` gibi +eklediğiniz bir ayar sonraki enjeksiyondan sonra da kalır. Korunan her atama, +birden çok satıra yayılan değerler ve 64 bit tamsayılar dahil yazıldığı gibi +kopyalanır ve ilk tablonun önüne yerleştirilir. Tablo içindeki anahtarlar ve +`a.b = 1` gibi noktalı kök anahtarlar korunmaz. Çakışmada her zaman OpenCodex +kazanır: sahip olduğu kök anahtarlar (`model_provider`, `model_catalog_json`, +`openai_base_url`) kullanılan yönlendirme moduna göre yeniden yazılır ve +profilin yazdığı bir tabloyla aynı adı taşıyan kök anahtar (profil `[features]` +yazarken satır içi `features = { ... }` gibi) atılır. Enjeksiyon yazma kilidini +beklerken dosya değişirse enjeksiyon yeniden denenebilir bir hatayla durur ve +düzenlemeniz yerinde kalır. + :::caution `openai_base_url`, `model_provider` ve `model_catalog_json` gibi kök anahtarlar ilk `[tablo]` başlığından önce **bulunmalıdır**. Enjektör bu yerleşimi garanti diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md index 4a95d62b8a2..8c7e61c2750 100644 --- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md @@ -50,6 +50,8 @@ ocx start --socks5-off ### `ocx stop` +Gözetimsiz Link Child kapanma sinyalinde her yazmadan önce bağlantıyı doğrular ve dinleyiciyi bırakmadan önce yerel Codex ile yönetilen Grok ayarlarını geri yükler. Servis/Desktop, sibling, yeniden başlatma devri ve üst stop makbuzu ayarları korur. Kapanma sırasında yeniden başlatma `409 runtime_stopping` döndürür. Zorla sonlandırma sinyali atlayabilir; üst `ocx stop` geri yüklemesi yine gereklidir. + Çalışan proxy'yi (PID'ye göre) durdurun, PID dosyasını kaldırın ve yerel Codex'i geri yükleyin. Yönetilen bir arka plan servisi kuruluysa `ocx stop` proxy'yi yeniden oluşturamaması için önce onu da durdurur. Web kontrol panelinin **Durdur** düğmesi aynı eylemi (`POST /api/stop`) Windows Görev Zamanlayıcı dışındaki tüm arka uçlarda çalıştırır: orada görev bittikten sonra sarmalayıcı proxy'yi yeniden başlatabilir, bu yüzden panel `respawnable_service` ile reddeder, hiçbir şeyi değiştirmez ve `ocx stop` çalıştırmanızı ister. diff --git a/docs-site/src/content/docs/tr/reference/configuration/agents.md b/docs-site/src/content/docs/tr/reference/configuration/agents.md index b51ba27d89f..e57200c9cd1 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -64,6 +64,8 @@ kaldırılır), bu da Codex'in bir Varsayılan mod oturumunu duraklatmasına ve akışta geliştirilme aşamasındadır ve yalnızca yeni oturumlara uygulanır; yüklü Codex derlemesi bayrağı henüz tanımadığında anahtar yüksek sesle başarısız olur. +Codex CLI güncellemesi kurulumu kullanırken özellik komutu çalıştırılmadan önce `codex_cli_selection_busy` ve `retryable: true` ile HTTP 503 döner. Güncelleme bitince yeniden deneyin; yapılandırma yazma hataları 502 döndürmeye devam eder. + ## Kadro ve rehberlik Geçerli v2 kadrosu, yapılandırılmış, seçicide görünen, önceliğe göre sıralanmış, diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index af2df25fe35..05d5718e975 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -124,6 +124,13 @@ env_key = "OPENCODEX_API_AUTH_TOKEN" 在 loopback 情况下,它包含可在自动注入被移除后手动合并的 root keys;在非 loopback 情况下, 它包含专用 provider 形式。外部 provider 模式会保持这个 profile 不变。 +重新生成这个文件时,会保留 OpenCodex 不管理的普通 root keys,所以你添加的 `stream_idle_timeout_ms` 或 +`stream_max_retries` 这类设置在下一次注入后仍然存在。保留的赋值按原样复制,包括跨多行的值和 64 位整数, +并放在第一个表之前。表内的键以及 `a.b = 1` 这样的点分 root key 不会保留。发生冲突时总是以 OpenCodex +为准:它拥有的 root keys(`model_provider`、`model_catalog_json`、`openai_base_url`)会按当前路由模式 +重写;与 profile 写入的表同名的 root key(例如 profile 写入 `[features]` 时的内联 `features = { ... }`) +会被丢弃。如果在注入等待写锁期间文件发生变化,注入会以可重试的错误停止,并保留你的修改。 + :::caution 像 `openai_base_url`、`model_provider` 和 `model_catalog_json` 这样的 root keys **必须** 位于第一个 `[table]` 头之前。注入器会保证这一点,移除自己留下的陈旧/重复副本,并且绝不会覆盖用户拥有的 root diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index d547e59f933..5fafbccc46f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -28,6 +28,8 @@ ocx start --socks5-off ### `ocx stop` +无人监督的 Link Child 处理退出信号时,会在每次写入前核对连接,并在关闭监听器前恢复 Codex 原生配置及受管理的 Grok。服务/Desktop、sibling、重启交接和父进程 stop 收据会保留配置。退出中的重启请求返回 `409 runtime_stopping`。强制终止可能跳过信号处理,仍需父进程 `ocx stop` 的恢复路径。 + 停止正在运行的代理(按 PID),移除 PID 文件;独立运行的所有者还会恢复原生 Codex。如果安装了受管后台服务,`ocx stop` 还会先停止该服务,这样它就无法重新拉起代理。Web 仪表盘的 **Stop** 按钮在多数后端执行同样的操作(`POST /api/stop`),但 Windows 任务计划程序除外:任务结束后包装器仍可能重新拉起代理,只有运行在代理之外的 stop 才能在恢复客户端配置前确认这个重启窗口,因此仪表盘会以 `respawnable_service` 拒绝、不做任何更改,并提示改用 `ocx stop`。 对于已连接的 hub 客户端,`ocx stop`(包括成功的 API 停止)会保留连接所拥有的 Codex 路由、模型目录和 Grok 配置。`ocx start` 使用这些保留的配置恢复服务,不会重新注入路由。要让 Codex 回到自己的账户,请运行 `ocx disconnect`;若已恢复为原生路由,要重新应用连接路由,请运行 `ocx sync --restart-codex`。 diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index d3c3a0c0741..e8eded5bcaf 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -121,6 +121,13 @@ env_key = "OPENCODEX_API_AUTH_TOKEN" 設定。loopback 模式下,其中包含自動注入被移除時可手動合併的根級鍵;non-loopback 模式下,其中包含 專用 provider 形式。外部 provider 模式不會修改此 profile。 +重新產生此檔案時,會保留 OpenCodex 不管理的一般根級鍵,因此你新增的 `stream_idle_timeout_ms` 或 +`stream_max_retries` 等設定在下一次注入後仍會存在。保留的賦值會照原樣複製,包括跨多行的值與 64 位元 +整數,並放在第一個表之前。表內的鍵以及 `a.b = 1` 這類點分根級鍵不會保留。發生衝突時一律以 OpenCodex +為準:它擁有的根級鍵(`model_provider`、`model_catalog_json`、`openai_base_url`)會依目前的路由模式 +重寫;與 profile 寫入的表同名的根級鍵(例如 profile 寫入 `[features]` 時的內嵌 `features = { ... }`) +會被捨棄。若注入等待寫入鎖期間檔案有變動,注入會以可重試的錯誤停止,並保留你的修改。 + :::caution `openai_base_url`、`model_provider`、`model_catalog_json` 等根級鍵**必須**位於第一個 `[table]` 標頭之前。注入器會保證此位置、移除自己留下的舊值或重複項,而且絕不覆寫使用者自有的根級 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md index c76ec26e196..66ba8a3ea3b 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md @@ -28,6 +28,8 @@ ocx start --socks5-off ### `ocx stop` +無人監督的 Link Child 處理結束訊號時,會在每次寫入前核對連線,並在關閉接聽器前還原 Codex 原生設定及受管理的 Grok。服務/Desktop、sibling、重新啟動交接和父程序 stop 收據會保留設定。結束中的重新啟動要求回傳 `409 runtime_stopping`。強制終止可能略過訊號處理,仍需父程序 `ocx stop` 的還原路徑。 + 停止執行中的代理(依 PID)、移除 PID 檔案,並還原原生 Codex。若已安裝受管背景服務,`ocx stop` 也會先停止它,使其無法重新生成代理。網頁儀表板的 **Stop** 按鈕在多數後端執行相同動作(`POST /api/stop`),但 Windows 工作排程器除外:工作結束後包裝程序仍可能重新啟動 Proxy,因此儀表板會以 `respawnable_service` 拒絕、不做任何變更,並請你改用 `ocx stop`。 對已連線的 hub 用戶端,`ocx stop`(包括成功的 API 停止)會保留連線擁有的 Codex 路由、目錄和 Grok 設定,`ocx start` 會再次提供服務。明確停止 Remote Link Child 時,若共用設定還原成功,則會還原原生 Codex 路由、目錄與已備份的續接歷程中繼資料,並移除受管理的 Grok 設定。已儲存的 Home 連線會保留,可用 `ocx disconnect` 移除。 diff --git a/package.json b/package.json index d763390a680..74afddd11e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.97.0", + "version": "2.98.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", diff --git a/scripts/diagnostics/windows-version-control/tests/async_contracts.test.rs b/scripts/diagnostics/windows-version-control/tests/async_contracts.test.rs index dd498b89ff2..618e0aa56f0 100644 --- a/scripts/diagnostics/windows-version-control/tests/async_contracts.test.rs +++ b/scripts/diagnostics/windows-version-control/tests/async_contracts.test.rs @@ -345,7 +345,8 @@ fn invalidation_and_stopped_source_discard_late_catalogs() { async function loadCase(change,phase) { let current = phase!=='before-call', release, entered, executions=0; const enteredPromise=new Promise(resolve=>{entered=resolve}); - const deps={commandCandidates:()=>['C:\\fixture\\codex.exe'], + // Use the real fixture executable so macOS can validate the candidate. + const deps={commandCandidates:()=>[process.env.CODEX_CLI_PATH], execFile:()=>new Promise(resolve=>{executions++;release=resolve;entered()})}; const pending=bundled.loadBundledCodexCatalogAsync(deps,()=>current); const mutate=()=>{if(change==='invalidate')bundled.invalidateBundledCatalogCache();else current=false}; @@ -475,16 +476,19 @@ fn refresh_deadline_settles_an_uncooperative_executor_and_allows_the_next_load() const realTimeout = globalThis.setTimeout; globalThis.setTimeout = (callback,delay,...args)=>realTimeout(callback,delay===45000?20:delay,...args); const keepAlive = setInterval(()=>{},100); -const expired = await bundled.loadBundledCodexCatalogAsync({commandCandidates:()=>['C:\\fixture\\codex.exe'], - execFile:()=>new Promise(()=>{})}); +let entered = false, aborted = false; +const expired = await bundled.loadBundledCodexCatalogAsync({commandCandidates:()=>[process.env.CODEX_CLI_PATH], + execFile:(_file,_args,options)=>{entered=true; options.signal.addEventListener('abort',()=>{aborted=true}); return new Promise(()=>{})}}); clearInterval(keepAlive); globalThis.setTimeout = realTimeout; -const fresh = await bundled.loadBundledCodexCatalogAsync({commandCandidates:()=>['C:\\fixture\\codex.exe'], +const fresh = await bundled.loadBundledCodexCatalogAsync({commandCandidates:()=>[process.env.CODEX_CLI_PATH], execFile:async()=>JSON.stringify(catalog)}); -return {expired:expired===null,fresh:fresh?.models.map(row=>row.slug)}; +return {expired:expired===null,entered,aborted,fresh:fresh?.models.map(row=>row.slug)}; "#, ); assert_eq!(result["expired"], true); + assert_eq!(result["entered"], true); + assert_eq!(result["aborted"], true); assert_eq!( result["fresh"], serde_json::json!(["gpt-5.5", "gpt-5.6-sol"]) diff --git a/scripts/test-layout/layout-clients.json b/scripts/test-layout/layout-clients.json index 01a3abb4020..8934d90bd52 100644 --- a/scripts/test-layout/layout-clients.json +++ b/scripts/test-layout/layout-clients.json @@ -1,5 +1,6 @@ { "omo-responses-loop.test.ts": "clients", + "codex-launcher-endpoint.test.ts": "codex-integration", "claude-translated-tool-restrictions.test.ts": "claude-integration", "windows-config-root-async-writers.test.ts": "windows", "t3code-client-lifecycle.test.ts": "clients", @@ -151,6 +152,7 @@ "client-link-connect.test.ts": "clients", "client-link-relay.test.ts": "clients", "client-link-runtime.test.ts": "clients", + "client-link-stop.test.ts": "clients", "link-routes.test.ts": "clients", "client-link-state.test.ts": "clients", "client-link-status.test.ts": "clients", diff --git a/scripts/test-layout/layout-codex-collision.json b/scripts/test-layout/layout-codex-collision.json index 6ceb8c4b7dd..e66ec93d488 100644 --- a/scripts/test-layout/layout-codex-collision.json +++ b/scripts/test-layout/layout-codex-collision.json @@ -43,5 +43,6 @@ "codex-stored-main-duplicate.test.ts": "codex-integration", "codex-account-slot-identity.test.ts": "codex-integration", "codex-pool-caller-fallback-policy.test.ts": "codex-integration", - "codex-windows-current-sid.test.ts": "codex-integration" + "codex-windows-current-sid.test.ts": "codex-integration", + "codex-profile-preservation.test.ts": "codex-integration" } diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 766e134baff..85994e144e7 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -2,7 +2,7 @@ "version": 1, "root": "tests", "explicitShards": ["layout-file-identity.json", "layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-server-inbound.json", "layout-providers.json", "layout-responses.json", "layout-exec-boundary.json", "layout-persistent-dashboard.json", "layout-start-ownership.json", "layout-config.json", "layout-kiro-usage.json", "layout-usage.json"], - "explicit": { "sync-fanout.test.ts": "cli", "sync-command-fanout.test.ts": "cli", "link-pinned-sync-argv.test.ts": "clients", "sync-link-status.test.ts": "cli", "sync-fanout-process.test.ts": "cli", "codex-installation-acl.test.ts": "codex-integration", "codex-installation-id.test.ts": "codex-integration", "codex-installation-wire.test.ts": "codex-integration", "codex-installation-failover.test.ts": "responses", "service-desktop-recovery.test.ts": "service", "catalog-omit-disabled-native.test.ts": "codex-integration", "provider-input-modalities.test.ts": "providers", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "config-workflow-budget.test.ts": "config", "server-workflow-budget-settings.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration", "coding-agent-windows-dispatch.test.ts": "adapters", "coding-agent-native-empty-argv.test.ts": "windows", + "explicit": { "kimi-monthly-quota.test.ts": "providers", "sync-fanout.test.ts": "cli", "sync-command-fanout.test.ts": "cli", "link-pinned-sync-argv.test.ts": "clients", "sync-link-status.test.ts": "cli", "sync-fanout-process.test.ts": "cli", "codex-installation-acl.test.ts": "codex-integration", "codex-installation-id.test.ts": "codex-integration", "codex-installation-wire.test.ts": "codex-integration", "codex-installation-failover.test.ts": "responses", "service-desktop-recovery.test.ts": "service", "catalog-omit-disabled-native.test.ts": "codex-integration", "provider-input-modalities.test.ts": "providers", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "config-workflow-budget.test.ts": "config", "server-workflow-budget-settings.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration", "coding-agent-windows-dispatch.test.ts": "adapters", "coding-agent-native-empty-argv.test.ts": "windows", "stop-restore-history-ownership.test.ts": "service", "cli-stop-refusal-transport.test.ts": "cli", "service-proxy-env.test.ts": "service", "openai-chat-standalone-bare-block.test.ts": "adapters/openai", @@ -753,7 +753,7 @@ "combo-workspace-data.test.ts": "gui", "combo-workspace-jev-decision.test.ts": "gui", "combos.test.ts": "codex-integration", "combos-kiro-profile-hop.test.ts": "codex-integration", - "command-code-error-finish.test.ts": "providers", + "command-code-error-finish.test.ts": "providers", "command-code-buffer-budget.test.ts": "providers", "command-code-efforts.test.ts": "providers", "command-code-project-context.test.ts": "providers", "command-code-provider.test.ts": "providers", @@ -898,7 +898,7 @@ "devin-cli-authmode-migration.test.ts": "providers", "devin-effort-ladder.test.ts": "providers", "devin-family-resolution.test.ts": "providers", - "devin-hardening.test.ts": "providers", + "devin-hardening.test.ts": "providers", "devin-stream-queue.test.ts": "providers", "devin-image-passthrough.test.ts": "providers", "devin-live-models.test.ts": "providers", "devin-login.test.ts": "providers", @@ -1082,10 +1082,10 @@ "kiro-adapter.test.ts": "providers/kiro", "kiro-auth-context-continuation.test.ts": "providers/kiro", "kiro-builder-id-profile.test.ts": "providers/kiro", - "kiro-calibration.test.ts": "providers/kiro", + "kiro-calibration.test.ts": "providers/kiro", "kiro-content-filter.test.ts": "providers/kiro", "kiro-device-builder.test.ts": "providers/kiro", "kiro-device-iam.test.ts": "providers/kiro", "kiro-device-social.test.ts": "providers/kiro", "kiro-fallback-error-body.test.ts": "providers/kiro", - "kiro-images.test.ts": "providers/kiro", + "kiro-images.test.ts": "providers/kiro", "kiro-completion-retry-wording.test.ts": "providers/kiro", "kiro-leased-responses.test.ts": "providers/kiro", "kiro-model-catalog.test.ts": "providers/kiro", "kiro-model-preference.test.ts": "providers/kiro", "kiro-oauth.test.ts": "providers/kiro", "oauth-kiro-refresh-attention.test.ts": "oauth", "kiro-org-user-identity.test.ts": "providers/kiro", diff --git a/src/adapters/command-code.ts b/src/adapters/command-code.ts index 8a26c300832..e116645cc50 100644 --- a/src/adapters/command-code.ts +++ b/src/adapters/command-code.ts @@ -425,27 +425,29 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera const encoder = new TextEncoder(); let buffer = ""; let bufferBytes = 0; + // One live buffer owns one charge; resizing must not reserve old + replacement together. + const resizeBuffer = (nextBytes: number) => { + if (nextBytes > bufferBytes) { + budget.reserveTransient(nextBytes - bufferBytes, { kind: "live_transient" }).commitRetained(); + } else { + budget.releaseRetained(bufferBytes - nextBytes, { kind: "live_transient" }); + } + bufferBytes = nextBytes; + }; try { for (;;) { const { value, done } = await reader.read(); const next = buffer + decoder.decode(value, { stream: !done }); const nextBytes = encoder.encode(next).byteLength; - const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" }); + resizeBuffer(nextBytes); buffer = next; - reservation.commitRetained(); - budget.releaseRetained(bufferBytes, { kind: "live_transient" }); - bufferBytes = nextBytes; let newline = buffer.indexOf("\n"); while (newline >= 0) { const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1); if (line) yield* decodeEventLine(line); newline = buffer.indexOf("\n"); } - const residualBytes = encoder.encode(buffer).byteLength; - const residualReservation = budget.reserveTransient(residualBytes, { kind: "live_transient" }); - residualReservation.commitRetained(); - budget.releaseRetained(bufferBytes, { kind: "live_transient" }); - bufferBytes = residualBytes; + resizeBuffer(encoder.encode(buffer).byteLength); if (done) break; } const final = buffer.trim(); diff --git a/src/adapters/devin/cloud-direct/chat.ts b/src/adapters/devin/cloud-direct/chat.ts index 435bfca98b0..05a02c58bfb 100644 --- a/src/adapters/devin/cloud-direct/chat.ts +++ b/src/adapters/devin/cloud-direct/chat.ts @@ -1392,8 +1392,8 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator 0 && chunkQueue.length > 0) { - const head = chunkQueue[0]; + let consumed = 0; + while (remaining > 0 && consumed < chunkQueue.length) { + const head = chunkQueue[consumed]; if (head.length <= remaining) { - chunkQueue.shift(); + consumed++; remaining -= head.length; } else { - chunkQueue[0] = head.slice(remaining); + chunkQueue[consumed] = head.slice(remaining); remaining = 0; } } + if (consumed > 0) { + chunkQueue.copyWithin(0, consumed); + chunkQueue.length -= consumed; + } } // Track the idle timer at outer scope so the finally block can clear it diff --git a/src/adapters/kiro-constants.ts b/src/adapters/kiro-constants.ts index 1a095ad8b70..c361614c030 100644 --- a/src/adapters/kiro-constants.ts +++ b/src/adapters/kiro-constants.ts @@ -31,9 +31,23 @@ export const KIRO_CONTINUATION_MESSAGE = * The condition names a decision, information, AND clarification on purpose. A model blocked on a * missing account id or an ambiguous path is stuck exactly as hard as one blocked on a choice, and * "decision" alone would leave that common class of question with no endorsed move again. + * + * It opens with the turn's actual state rather than "Continue the existing task". The retry also + * fires when the held text WAS the complete answer: on a trivial request ("Reply with exactly: OK") + * claude-sonnet-5 answers in plain text with END_TURN instead of calling the completion tool, while + * the 5.5 models call it. Told to continue an "existing task" right after its own finished reply, + * the model read the instruction as the user's next message and called the completion tool with + * "There's no active task in this conversation for me to continue", which then superseded the + * correct "OK" (reproduced live on every attempt). Stating that ordinary text has not reached the + * user, and asking for an already complete reply to be resubmitted unchanged, made the same model + * call the tool with "OK"; replayed progress prose still led to the next real tool call, and a + * retry with no replayed text (reasoning only) still answered the original request. The resubmit + * clause is conditional and rides on a general "if the task is complete" route, because that last + * case appends this message to the user turn: there is no previous reply to resubmit, and a model + * that can answer directly still needs an exit other than a tool call or a blocking question. */ export const KIRO_COMPLETION_RETRY_MESSAGE = - `Continue the existing task without quoting this instruction. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, call ${KIRO_COMPLETION_TOOL_NAME} now with that question as the answer. Otherwise issue the next real tool call now. Do not solicit a new task and do not emit another progress-only message.`; + `Ordinary assistant text does not end the turn, so no final answer has reached the user yet. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer; if your previous reply already was that answer, submit it unchanged. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, call ${KIRO_COMPLETION_TOOL_NAME} now with that question as the answer. Otherwise continue the task with the next real tool call now. Do not solicit a new task, do not emit another progress-only message, and do not quote or mention this instruction.`; export const KIRO_TOOL_RESULT_CARRIER_MESSAGE = "The requested tool result is attached."; export const KIRO_EMPTY_TOOL_RESULT_MESSAGE = "The tool completed without textual output."; diff --git a/src/adapters/kiro-events.ts b/src/adapters/kiro-events.ts index 6f0db2ece29..330a87a4360 100644 --- a/src/adapters/kiro-events.ts +++ b/src/adapters/kiro-events.ts @@ -9,11 +9,17 @@ export type ParsedKiroEvent = | { type: "metering"; unit: string; usage: number; unitPlural?: string } | { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean } | { type: "truncation"; data: string } - | { type: "metadata"; usage?: OcxUsage; contextUsagePercentage?: number; stopReason?: string } + | { type: "metadata"; usage?: OcxUsage; contextUsagePercentage?: number; stopReason?: string; refusal?: KiroRefusalDetails } | { type: "message_metadata"; conversationId?: string } | { type: "invalid_state"; message?: string } | { type: "error"; reason?: string; message?: string }; +/** The model-side refusal Kiro attaches to a CONTENT_FILTERED stop (`stopDetails.refusal`). */ +export interface KiroRefusalDetails { + category?: string; + explanation?: string; +} + const KNOWN_EVENT_TYPES = new Set([ "assistantResponseEvent", "reasoningContentEvent", @@ -61,6 +67,28 @@ function optionalBoolean(eventType: string, obj: Record, key: s return value; } +/** + * `stopDetails.refusal` is advisory copy for the user. Read it leniently: an unexpected shape drops + * the details rather than turning the refusal itself into a protocol error. Upstream-controlled + * text, so each field is bounded. + */ +function refusalDetails(obj: Record): KiroRefusalDetails | undefined { + const stopDetails = obj.stopDetails; + if (!stopDetails || typeof stopDetails !== "object" || Array.isArray(stopDetails)) return undefined; + const refusal = (stopDetails as Record).refusal; + if (!refusal || typeof refusal !== "object" || Array.isArray(refusal)) return undefined; + /** A non-empty trimmed string field of the refusal, cut to `max` characters, or undefined. */ + const field = (key: string, max: number): string | undefined => { + const value = (refusal as Record)[key]; + return typeof value === "string" && value.trim() ? value.trim().slice(0, max) : undefined; + }; + const category = field("category", 64); + const explanation = field("explanation", 400); + return category || explanation + ? { ...(category ? { category } : {}), ...(explanation ? { explanation } : {}) } + : undefined; +} + function tokenCount(eventType: string, obj: Record, key: string, required: boolean): number { const value = obj[key]; if (value === undefined && !required) return 0; @@ -217,6 +245,7 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi return malformed(eventType, "contextUsagePercentage must be a finite number"); } const stopReason = optionalString(eventType, parsed, "stopReason"); + const refusal = refusalDetails(parsed); return { type: "metadata", ...(parseTokenUsage(eventType, parsed.tokenUsage) !== undefined @@ -224,6 +253,7 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi : {}), ...(typeof contextUsagePercentage === "number" ? { contextUsagePercentage } : {}), ...(stopReason !== undefined ? { stopReason } : {}), + ...(refusal ? { refusal } : {}), }; } case "contextUsageEvent": { diff --git a/src/adapters/kiro/stream.ts b/src/adapters/kiro/stream.ts index 7aaa5f768ee..72501ecaa6b 100644 --- a/src/adapters/kiro/stream.ts +++ b/src/adapters/kiro/stream.ts @@ -15,7 +15,7 @@ import { safeKiroHttpErrorMessage, type KiroErrorClassification, } from "../kiro-errors"; -import { parseKiroEvent } from "../kiro-events"; +import { parseKiroEvent, type KiroRefusalDetails } from "../kiro-events"; import { noteKiroTransientThrottle } from "../kiro-retry"; import { InlineThinkTagParser } from "../inline-think-tags"; import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation"; @@ -209,6 +209,37 @@ function retryableKiroIncomplete( }; } +/** + * Kiro's safety-filter stops. For Claude models CONTENT_FILTERED is the model's own safety + * classifier, a deterministic verdict on this exact context, so every completion mode maps it to + * the non-retryable `content_filter` incomplete: Claude outbound renders `stop_reason: "refusal"` + * and Responses a terminal `response.failed` (`src/bridge/terminal-refusal.ts`). Kiro's refusal + * category and explanation are model-side copy, never request content, so they ride the message. + */ +function isKiroContentFilterStop( + stopReason: string | undefined, +): stopReason is "CONTENT_FILTERED" | "GUARDRAIL_INTERVENED" { + return stopReason === "CONTENT_FILTERED" || stopReason === "GUARDRAIL_INTERVENED"; +} + +/** The non-retryable `content_filter` incomplete for a filter stop, carrying Kiro's refusal category and explanation when present. */ +function kiroContentFilterIncomplete( + stopReason: string, + refusal: KiroRefusalDetails | undefined, + usage: OcxUsage, + providerState: { kiro: { conversationId: string } } | undefined, +): AdapterEvent { + const category = refusal?.category ? `, category ${refusal.category}` : ""; + const explanation = refusal?.explanation ? `: ${refusal.explanation}` : ""; + return retryableKiroIncomplete( + "content_filter", + `Kiro stopped with ${stopReason}${category}${explanation}`, + usage, + providerState, + false, + ); +} + /** * Catch-path retryability for #519: only transport/socket failures with no emitted output * are replay-safe. Malformed event payloads (`invalid Kiro …`) and any post-output failure @@ -297,6 +328,11 @@ async function* parseKiroAttempt( } } +/** + * Parse one Kiro attempt's event stream into adapter events and a terminal decision: text, reasoning, + * real tool calls, the private terminal tool, usage and the stop reason. In `required` mode it may + * return `needsFallback` instead of a terminal, which asks the caller for the one bounded retry. + */ async function* parseKiroAttemptEvents( response: Response, budget: TranslatorBudget, @@ -344,6 +380,7 @@ async function* parseKiroAttemptEvents( let authoritativeUsage: OcxUsage | undefined; let providerCredits: number | undefined; let stopReason: string | undefined; + let refusal: KiroRefusalDetails | undefined; const fallbackEvents: AdapterEvent[] = []; const thinking = new InlineThinkTagParser(budget); @@ -639,6 +676,7 @@ async function* parseKiroAttemptEvents( contextUsagePercentage = ev.contextUsagePercentage; } if (ev.stopReason !== undefined) stopReason = ev.stopReason; + if (ev.refusal) refusal = ev.refusal; break; case "message_metadata": if (isValidKiroConversationId(ev.conversationId)) { @@ -833,6 +871,19 @@ async function* parseKiroAttemptEvents( } if (mode === "text_fallback") { + // A validated final answer and a real tool call keep their precedence; only a turn that would + // otherwise end on its text is read as refused. Neither attempt produced an accepted answer, so + // the first attempt's held progress is released rather than superseded (retry text normally + // replaces it), and the filtered retry text follows as the commentary it was staged as. + if (completionAnswer === undefined && !sawRealTool && isKiroContentFilterStop(normalizedStopReason)) { + if (priorAttempt) yield* priorAttempt.drainDeferred(false); + yield* emitRetained(fallbackEvents); + return { + assistantText, + sawReasoning, + terminal: kiroContentFilterIncomplete(normalizedStopReason, refusal, finalUsage, finalProviderState), + }; + } if (priorAttempt) { yield* priorAttempt.drainDeferred(completionAnswer !== undefined || (sawText && !sawRealTool)); } @@ -885,6 +936,17 @@ async function* parseKiroAttemptEvents( }; } + // Ahead of the empty-stream fallback below: a tool-less (`disabled`) turn Kiro refuses carries + // no content at all, and reading it as a replay-safe empty stream made clients resend a refusal + // that repeats deterministically. Partial text before the filter is not a finished answer either. + // A validated final answer and a real tool call keep their precedence, as on the retry path. + if (completionAnswer === undefined && !sawRealTool && isKiroContentFilterStop(normalizedStopReason)) { + return { + assistantText, + sawReasoning, + terminal: kiroContentFilterIncomplete(normalizedStopReason, refusal, finalUsage, finalProviderState), + }; + } if (completionAnswer !== undefined) { yield { type: "text_delta", text: completionAnswer, phase: "final_answer" }; return { @@ -953,9 +1015,6 @@ async function* parseKiroAttemptEvents( }; } if (normalizedStopReason === "MAX_TOKENS") return incomplete("max_output_tokens", true); - if (normalizedStopReason === "CONTENT_FILTERED" || normalizedStopReason === "GUARDRAIL_INTERVENED") { - return incomplete("content_filter", false); - } if (normalizedStopReason === "MALFORMED_TOOL_USE") return incomplete("kiro_malformed_tool_use", false); if (normalizedStopReason === "MALFORMED_MODEL_OUTPUT") return incomplete("kiro_malformed_model_output", false); // TOOL_USE here means Kiro claimed a tool call it never emitted. diff --git a/src/adapters/openai-responses/reasoning.ts b/src/adapters/openai-responses/reasoning.ts index 4ba2f26503c..c0b76d05a53 100644 --- a/src/adapters/openai-responses/reasoning.ts +++ b/src/adapters/openai-responses/reasoning.ts @@ -174,10 +174,15 @@ export function sanitizeReasoningInputContent( /** Assistant-authored history items: what DeepSeek merges into one assistant message. */ function isAssistantTurnItem(item: unknown): boolean { - if (!isPlainObject(item) || typeof item.type !== "string") return false; - if (item.type === "reasoning") return true; - if (item.type === "message") return item.role === "assistant"; - return item.type.endsWith("_call"); + if (!isPlainObject(item)) return false; + // Responses input accepts the message shorthand without `type`. ZCode sends its assistant + // text that way, directly ahead of the same turn's function calls; reading only a typed + // `message` would split the turn and place the placeholder backfill after the text, which + // DeepSeek refuses with the same reasoning_text 400. + const type = typeof item.type === "string" ? item.type : "role" in item ? "message" : undefined; + if (type === "reasoning") return true; + if (type === "message") return item.role === "assistant"; + return type?.endsWith("_call") === true; } function isClientToolCallItem(item: unknown): boolean { diff --git a/src/bridge/sse.ts b/src/bridge/sse.ts index c1390ceebb0..978dd29c3b0 100644 --- a/src/bridge/sse.ts +++ b/src/bridge/sse.ts @@ -24,7 +24,7 @@ import { mayBecomePatchEnvelope, repairFreeformToolInput, } from "../responses/apply-patch-envelope"; -import { progressiveFreeformInput } from "../responses/progressive-freeform-input"; +import { ProgressiveFreeformInputReader } from "../responses/progressive-freeform-input"; import { encodeCompactionSummary, releaseCompactionCiphertextLease } from "../responses/compaction"; import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat"; import { mayBecomeCodeModeShellInput } from "../responses/code-mode-shell-input"; @@ -480,7 +480,7 @@ export function bridgeToResponsesSSE( // Full assistant text of a compaction turn (across message boundaries) — becomes the // synthetic compaction item's payload on done. let compaction = emptyChunks(); - let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; codeModeHelperName?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null; + let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; freeformReader?: ProgressiveFreeformInputReader; codeModeHelperName?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null; // Open native web-search cell (between begin and end). Holds the output index allocated on // begin so the matching done reuses it; closed as `failed` if the stream terminates early. let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null; @@ -1157,7 +1157,9 @@ export function bridgeToResponsesSSE( // is the same disagreement in the other direction. const ownsFreeformGrammar = currentToolCall.namespace === undefined || currentToolCall.namespace === "functions"; - const full = progressiveFreeformInput( + // One reader per call resumes where the previous delta stopped. + currentToolCall.freeformReader ??= new ProgressiveFreeformInputReader(); + const full = currentToolCall.freeformReader.update( currentToolCall.args, ownsFreeformGrammar ? currentToolCall.name : "", ); diff --git a/src/cli/codex-launch-auth.ts b/src/cli/codex-launch-auth.ts index 9d825b81454..3d0dde1e568 100644 --- a/src/cli/codex-launch-auth.ts +++ b/src/cli/codex-launch-auth.ts @@ -7,10 +7,34 @@ import { readBoundedCodexConfig } from "../codex/inject/bounded-config-reader"; import { buildProviderTableBlockForTarget } from "../codex/inject/config-toml"; import { standaloneCodexRoutingTarget, validateCodexRoutingTarget } from "../codex/inject/routing-target"; import { readClientConnectionState } from "../client/state"; +import { normalizeBindHostname } from "../server/bind-host"; +import { probeHostname, proveLiveProxyOwnedByHome, type LiveProxy } from "../server/proxy-liveness"; import { codexHasOtherTokenConsumers, codexTokenConfigShapeIsSafe } from "./codex-launch-policy"; import { codexLaunchHomeIsOwned, codexLaunchProjectAliasIsOwned, codexLaunchSystemNamespaceIsSafe } from "./codex-launch-ownership"; import type { CodexTokenRefusal, CodexTokenRefusalReporter } from "./codex-launch-diagnostics"; +export type CodexRuntimeAttestation = Readonly<{ origin: string }>; + +/** Retain the exact dial endpoint that answered the ownership challenge, not just its verdict. */ +export async function attestCodexRuntime( + live: LiveProxy, + prove: typeof proveLiveProxyOwnedByHome = proveLiveProxyOwnedByHome, +): Promise { + // Probe the address the listener actually binds: a configured localhost binds 127.0.0.1, + // which is also what the standalone Codex target dials. + const snapshot = Object.freeze({ ...live, probeHostname: normalizeBindHostname(live.probeHostname ?? live.hostname) }); + try { + const origin = new URL(`http://${probeHostname(snapshot.probeHostname)}:${snapshot.port}`).origin; + return await prove(snapshot) === "proven" ? Object.freeze({ origin }) : null; + } catch { return null; } +} + +/** IPv4 and IPv6 listeners are distinct recipients, even when their ports are equal. */ +export function codexRuntimeAttestationMatchesTarget(baseUrl: string, proof: CodexRuntimeAttestation | null | undefined): boolean { + try { return proof !== null && proof !== undefined && new URL(baseUrl).origin === proof.origin; } + catch { return false; } +} + /** Resolve once; the credential verifier and child must use this exact directory. */ export function resolveCodexLaunchHome(env: Record): string | null { try { @@ -28,7 +52,7 @@ export function resolveCodexLaunchHome(env: Record): } /** Config classification is a startup hint; credentials need an independently bound destination. */ -export function codexCredentialDestinationIsTrusted(home: string, systemNamespaceDirectory?: string, report?: CodexTokenRefusalReporter): boolean { +export function codexCredentialDestinationIsTrusted(home: string, systemNamespaceDirectory?: string, report?: CodexTokenRefusalReporter, runtimeAttestation?: CodexRuntimeAttestation | null): boolean { const refuse = (reason: CodexTokenRefusal): false => { report?.(reason); return false; }; try { if (!codexLaunchHomeIsOwned(home)) return refuse("ownership"); @@ -43,15 +67,20 @@ export function codexCredentialDestinationIsTrusted(home: string, systemNamespac if (diagnostics.error || diagnostics.source === "fallback") return refuse("installationConfig"); const cfg = diagnostics.config; let target = standaloneCodexRoutingTarget(cfg.port, cfg); + // A direct client's proof comes from its own machine listener, never from the hub it + // dials, so only a standalone destination can be compared with the proven origin. + let destinationIsProvenListener = true; if (cfg.runtimeRole === "client") { // Direct clients bind to persisted connection state; link clients use an authless relay. const connection = readClientConnectionState(); if (connection.kind !== "connected" || connection.value.transport === "link") return refuse("connection"); target = validateCodexRoutingTarget({ baseUrl: `${connection.value.serverUrl}/v1`, requiresAdmissionToken: true, tokenEnv: "OPENCODEX_API_AUTH_TOKEN" }); + destinationIsProvenListener = false; } const url = new URL(target.baseUrl); if (!target.requiresAdmissionToken) return refuse("admission"); if (url.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(url.hostname)) return refuse("loopback"); + if (destinationIsProvenListener && !codexRuntimeAttestationMatchesTarget(target.baseUrl, runtimeAttestation)) return refuse("destination"); const content = readBoundedCodexConfig(join(home, "config.toml")); if (content === null) return refuse("configRead"); const config = Bun.TOML.parse(content) as Record; diff --git a/src/cli/codex-launcher.ts b/src/cli/codex-launcher.ts index d8fce9489db..7b328602308 100644 --- a/src/cli/codex-launcher.ts +++ b/src/cli/codex-launcher.ts @@ -34,7 +34,7 @@ import { selfLaunchArgv } from "../lib/self-launch-argv"; import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets"; import { findExecutableOnPath } from "../remote-control/workspace-executable"; import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness"; -import { codexCredentialDestinationIsTrusted, resolveCodexLaunchHome } from "./codex-launch-auth"; +import { attestCodexRuntime, codexCredentialDestinationIsTrusted, resolveCodexLaunchHome, type CodexRuntimeAttestation } from "./codex-launch-auth"; import { codexCommandCanBypass, CODEX_UNVERIFIED_SHIM } from "./codex-launch-command"; import { codexTokenArgsAreSafe, codexTokenEnvironmentIsSafe } from "./codex-launch-policy"; import { codexTokenWithheldDiagnostic, type CodexTokenRefusal, type CodexTokenRefusalReporter } from "./codex-launch-diagnostics"; @@ -158,7 +158,9 @@ export type CodexLaunchEnvDeps = { args?: readonly string[]; /** Test seam; production reads the hardened service token file. */ serviceToken?: (base: CodexLaunchEnv) => string | null; - credentialDestination?: (home: string, report?: CodexTokenRefusalReporter) => boolean; + credentialDestination?: (home: string, report?: CodexTokenRefusalReporter, runtimeAttestation?: CodexRuntimeAttestation | null) => boolean; + /** Exact origin that proved this home's runtime-record secret. */ + runtimeAttestation?: CodexRuntimeAttestation | null; }; /** Read the client admission credential from this installation's service-token file. */ @@ -186,6 +188,7 @@ export function buildCodexLaunchEnv( }; if (!codexTokenArgsAreSafe(deps.args ?? [])) return withhold("args"); if (!codexTokenEnvironmentIsSafe(env)) return withhold("environment"); + if (!deps.runtimeAttestation) return withhold("destination"); // NO_PROXY parsing and precedence vary across native HTTP/WebSocket transports. // Without proving bypass, preserve the user's transport but never load a secret. const home = resolveCodexLaunchHome(env); @@ -193,8 +196,8 @@ export function buildCodexLaunchEnv( let refusal: CodexTokenRefusal = "destination"; const report: CodexTokenRefusalReporter = reason => { refusal = reason; }; const trusted = deps.credentialDestination - ? deps.credentialDestination(home, report) - : codexCredentialDestinationIsTrusted(home, undefined, report); + ? deps.credentialDestination(home, report, deps.runtimeAttestation) + : codexCredentialDestinationIsTrusted(home, undefined, report, deps.runtimeAttestation); if (!trusted) return withhold(refusal); const token = (deps.serviceToken ?? defaultServiceToken)(env); if (!token) return withhold("credential"); @@ -318,5 +321,9 @@ export async function cmdCodex(args: string[]): Promise { // what the session does next; `ocx doctor` names the autostart failure. console.error(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC); } - return spawnCodex(args, buildCodexLaunchEnv(process.env, { routingKind, args })); + // A public health marker proves liveness, not ownership of the listener. Never read the + // persisted service token until the exact runtime-record endpoint answers a fresh challenge. + const runtimeAttestation = plan.kind === "routed" && live !== null + ? await attestCodexRuntime(live) : null; + return spawnCodex(args, buildCodexLaunchEnv(process.env, { routingKind, args, runtimeAttestation })); } diff --git a/src/cli/index.ts b/src/cli/index.ts index e2f98925666..bcddd09f7ff 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1139,7 +1139,9 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) { .filter(read => isPendingTeardownAbandoned(read, teardownOwnerStillRunning)); let teardownNonce: string | undefined; // #7423: an update that restarts the proxy asks this stop to leave Codex routing for the replacement. - const handoverRequested = updateTeardownHandoverRequested(process.env, () => shouldSyncCodexOnStart(loadConfig())) && !stoppingSibling && !connectedClientSkip; + // Client start never discharges a standalone update-handover receipt or reapplies routing. + const handoverRequested = updateTeardownHandoverRequested(process.env, () => shouldSyncCodexOnStart(loadConfig())) + && !stoppingSibling && readClientConnectionState().kind !== "connected"; let deferralHonoredByProxy = false; const claimTeardown = (endpoint: { hostname: string; port: number }, endpointSource: "exact" | "guessed") => { if (teardownNonce || stoppingSibling) return; diff --git a/src/client/connect.ts b/src/client/connect.ts index 81f7f8ff516..00bb058891c 100644 --- a/src/client/connect.ts +++ b/src/client/connect.ts @@ -18,7 +18,8 @@ import { withClientLifecycle, withClientLifecycleSync, type ClientLifecycleHeld, type ClientLifecycleLockDeps, } from "./lifecycle-lock"; -import { invalidateCodexModelsCache } from "../codex/catalog/sync"; +import { invalidateCodexModelsCache, invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync"; +import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization"; import { injectCodexConfig, currentExternalCodexModelProvider, @@ -30,7 +31,7 @@ import { journalOwner, restoreJournalState, } from "../codex/journal"; -import { DEFAULT_CATALOG_PATH } from "../codex/paths"; +import { DEFAULT_CATALOG_PATH, getCodexHome } from "../codex/paths"; import { readServiceApiTokenState, readTokenBackupState, @@ -95,6 +96,7 @@ import { import { hubStateCachePath } from "./hub-state"; import { ClientLinkStateError, clearClientLinkState, readClientLinkState } from "./link-state"; import { isLinkPort } from "../link/ports"; +import { clientSyncRuntimeGuard, ClientSyncRuntimeError } from "./sync-runtime"; class RotationRecoveryRequiredError extends Error { constructor(message: string, options?: ErrorOptions) { @@ -901,7 +903,15 @@ async function syncConnectedClientUnlocked( if (state.kind !== "connected" || state.value.pendingOperation) throw new Error("client_sync_unavailable"); const token = readServiceApiTokenState(); if (token.kind !== "present" || token.fingerprint !== state.value.tokenFingerprint) throw new Error("client_token_changed"); - return { connection: state.value, token }; + let assertRuntimeCurrent = () => {}; + try { if (isLinkConnection(state.value)) assertRuntimeCurrent = clientSyncRuntimeGuard(); } + catch (error) { + if (options.expect && error instanceof ClientSyncRuntimeError && error.code === "client_runtime_stopping") { + throw new ClientSyncExpectationError(options.expect, "pending_operation"); + } + throw error; + } + return { connection: state.value, token, assertRuntimeCurrent }; }), deps.lifecycleLockDeps); let downloaded: Awaited> | undefined; let stale = false; @@ -918,6 +928,7 @@ async function syncConnectedClientUnlocked( // they were, which is the known-good state. if (downloaded) assertClientCatalogCompatible(downloaded.body, deps.catalogCompatibility); const next = withClientLifecycleSync(() => withConfigMutationLockSync(() => { + initial.assertRuntimeCurrent(); assertClientConnectionUnchanged(initial.connection); const token = readServiceApiTokenState(); if (token.kind !== "present" || token.fingerprint !== initial.token.fingerprint) throw new Error("client_token_changed"); @@ -933,10 +944,12 @@ async function syncConnectedClientUnlocked( }), deps.lifecycleLockDeps); // Read-only: injection invokes this while N/C are held. Acquiring L here would invert C→L. const beforeClientWrite = () => { + initial.assertRuntimeCurrent(); assertClientConnectionUnchanged(next); const token = readServiceApiTokenState(); if (next.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint) throw new Error("client_token_changed"); }; + const withClientWrite = (write: () => T): T => withConfigMutationLockSync(() => { beforeClientWrite(); return write(); }); let injected = false; let homeSettings: HomeCodexSettingsSource | undefined; let appliedRecord: HomeCodexSettingsRecord | undefined; @@ -945,12 +958,15 @@ async function syncConnectedClientUnlocked( let config = loadConfig(); // #6957: a link Child applies the Home's OpenCodex-owned Codex settings with the catalog. if (isLinkConnection(next)) { - homeSettings = await resolveHomeCodexSettings(next, initial.token.token, deps.linkHome); + homeSettings = await resolveHomeCodexSettings(next, initial.token.token, { ...deps.linkHome, withWriteGuard: withClientWrite }); if (homeSettings.source !== "local") config = overlayHomeCodexSettings(config, homeSettings.document.settings); // About to write different settings: until their record commits, report no applied digest // rather than the previous one (OCX-6). A failed injection then reads "unknown", never a // digest of values Codex no longer has or never got. - if (homeSettings.source === "home" && next.link) withdrawChangingHomeCodexSettingsRecord(next.link.linkId, homeSettings.document); + if (homeSettings.source === "home" && next.link) { + const { linkId } = next.link, { document } = homeSettings; + withClientWrite(() => withdrawChangingHomeCodexSettingsRecord(linkId, document)); + } } const result = await injectCodexConfig(config.port, { ...config, syncResumeHistory: false }, { routingTarget: routingTarget(next.serverUrl, isLinkConnection(next) ? config.port : undefined, config), catalogPath: DEFAULT_CATALOG_PATH, @@ -962,23 +978,24 @@ async function syncConnectedClientUnlocked( const linkId = next.link.linkId; const settings = homeSettings; try { - appliedRecord = withClientLifecycleSync(() => { - beforeClientWrite(); - return recordHomeCodexSettings(linkId, settings, deps.now) ?? readHomeCodexSettingsRecord(linkId) ?? undefined; - }, deps.lifecycleLockDeps); + appliedRecord = withClientLifecycleSync(() => withClientWrite(() => + recordHomeCodexSettings(linkId, settings, deps.now) ?? readHomeCodexSettingsRecord(linkId) ?? undefined), deps.lifecycleLockDeps); } catch (error) { // An ownership change is not a disk problem: report it as itself. - if (error instanceof Error && (error.message === "client_token_changed" || error.message === "client_connection_changed")) throw error; + if (error instanceof ClientSyncRuntimeError || (error instanceof Error && (error.message === "client_token_changed" || error.message === "client_connection_changed"))) throw error; // The injection committed, so the cache refresh below still runs; the caller reports this. - try { clearHomeCodexSettingsRecord(); } catch { /* status already reads no recorded apply or reports the write */ } + try { withClientWrite(clearHomeCodexSettingsRecord); } catch { /* status already reads no recorded apply or reports the write */ } applyRecordFailed = true; } } } const cacheSynced = withClientLifecycleSync(() => { - beforeClientWrite(); - // Cache invalidation acquires K itself (N -> K -> C); never call it while C is held. - return invalidateCodexModelsCache({ allowWhenDesiredDisabled: true }); + // Acquire K before the guarded C commit; never acquire K from inside C. + const owningHome = getCodexHome(); + const result = withCatalogWriteSerialization(owningHome, permit => withClientWrite(() => + invalidateCodexModelsCacheWithPermit(permit, owningHome, { allowWhenDesiredDisabled: true })), + { intent: "cache", writer: "cache-invalidate" }); + return result.kind === "completed" && result.value; }, deps.lifecycleLockDeps); return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale, diff --git a/src/client/link-home.ts b/src/client/link-home.ts index a3cf4b49343..3452e602ee1 100644 --- a/src/client/link-home.ts +++ b/src/client/link-home.ts @@ -299,7 +299,7 @@ export type HomeCodexSettingsSource = export async function resolveHomeCodexSettings( connection: LinkHomeConnection, admissionKey: string, - deps: LinkHomeDeps & { now?: () => Date } = {}, + deps: LinkHomeDeps & { now?: () => Date; withWriteGuard?: (write: () => T) => T } = {}, ): Promise { const linkId = connection.link?.linkId; if (!linkId) return { source: "local", reason: "not a Remote Link Child" }; @@ -308,7 +308,8 @@ export async function resolveHomeCodexSettings( return { source: "home", document: fetched.document, fetchedAt: (deps.now ?? (() => new Date()))().toISOString() }; } if (fetched.kind === "unsupported") { - clearHomeCodexSettingsRecord(); + if (deps.withWriteGuard) deps.withWriteGuard(clearHomeCodexSettingsRecord); + else clearHomeCodexSettingsRecord(); return { source: "local", reason: "the Home does not publish Codex settings; upgrade the Home" }; } const cached = readHomeCodexSettingsRecord(linkId); diff --git a/src/client/machine-listener.ts b/src/client/machine-listener.ts index 0f6ca2681b4..a697089b975 100644 --- a/src/client/machine-listener.ts +++ b/src/client/machine-listener.ts @@ -324,6 +324,9 @@ async function handleBoundRestart( ); if (!authorized) return Response.json({ success: false, error: "restart capability required" }, { status: 401, headers: noStore }); const admission = await restart.request(); + if (admission.kind === "stopping") { + return Response.json({ success: false, error: { code: "runtime_stopping" } }, { status: 409, headers: noStore }); + } if (admission.kind === "supervised") { return Response.json({ success: false, diff --git a/src/client/restore-ownership.ts b/src/client/restore-ownership.ts index e1d190cb9f0..4ca5d24741d 100644 --- a/src/client/restore-ownership.ts +++ b/src/client/restore-ownership.ts @@ -1,5 +1,6 @@ import { withConfigMutationLockSync } from "../config"; import { assertNoClientDisconnectPending, pendingClientConnectMayOwnToken, readClientConnectionState } from "./state"; +import type { OcxClientConnectionConfig } from "../types"; export type RestoreBoundaryOwnership = "restore" | "hub-owned" | "unproven"; export type RestoreOwnershipRefusal = Exclude; @@ -21,8 +22,16 @@ export function inspectStopRestoreOwnership(read = readClientConnectionState): R } catch { return "unproven"; } } +function restoreConnectionSnapshot(connection: OcxClientConnectionConfig) { + // Normal sync changes these observations, not restoration ownership. All other + // fields, including priorCatalog and future connection fields, remain guarded. + const { catalogFingerprint: _fingerprint, catalogSyncedAt: _syncedAt, ...snapshot } = connection; + return snapshot; +} + /** C is reentrant inside N/C; this never acquires L or N. No lease crosses an await. */ -export function stopRestoreWriteGuard() { +export function stopRestoreWriteGuard(expectedConnection?: OcxClientConnectionConfig) { + const expected = expectedConnection && restoreConnectionSnapshot(structuredClone(expectedConnection)); let boundary: RestoreBoundaryOwnership = "restore"; return { boundary: () => boundary, @@ -32,6 +41,15 @@ export function stopRestoreWriteGuard() { return withConfigMutationLockSync(() => { entered = true; if (boundary === "restore") boundary = inspectStopRestoreOwnership(); + if (boundary === "restore" && expected) { + try { + if (expected.pendingOperation) throw new Error("client_rotation_pending"); + const current = readClientConnectionState(); + if (current.kind !== "connected" || !Bun.deepEquals(restoreConnectionSnapshot(current.value), expected, true)) { + throw new Error("client_connection_changed"); + } + } catch { boundary = "unproven"; } + } if (boundary !== "restore") throw new StopRestoreOwnershipError(boundary); return write(); }); diff --git a/src/client/runtime-restart.ts b/src/client/runtime-restart.ts index f5ad4417908..899fb165112 100644 --- a/src/client/runtime-restart.ts +++ b/src/client/runtime-restart.ts @@ -30,9 +30,12 @@ export type ClientRestartSupervisor = "service" | "desktop"; export type ClientRestartAdmission = | { kind: "accepted"; alreadyRestarting: boolean } + | { kind: "stopping" } | { kind: "supervised"; supervisor: ClientRestartSupervisor }; export interface ClientRestartIo { + /** A signal shutdown has claimed this runtime; it cannot admit a replacement. */ + stopping?: () => boolean; /** Which supervisor owns this process, or null when it starts its own replacement. */ supervisor(): Promise; /** Stop the tunnel supervisor and the listener, giving in-flight requests up to `drainMs`. */ @@ -41,7 +44,7 @@ export interface ClientRestartIo { spawnReplacement(deadlineAt: number): Promise; /** Serve again on this process: listener, fresh tunnel supervisor, records. */ resume(): Promise; - exitProcess(code: number): void; + exitProcess(code: number): void | Promise; now?: () => number; sleep?: (ms: number) => Promise; warn?: (message: string) => void; @@ -49,7 +52,9 @@ export interface ClientRestartIo { export interface ClientRestartController { request(): Promise; - /** Settles when the restart in flight (if any) has finished or rolled back. Tests only. */ + /** Only an accepted flight owns routing; a supervisor check does not. */ + hasHandoff(): boolean; + /** Join the accepted flight before signal teardown; may reject on a failed exit callback. */ settled(): Promise; } @@ -81,16 +86,19 @@ export function createClientRestartController(io: ClientRestartIo): ClientRestar await io.resume(); } catch (resumeError) { warn(`[client] restart: this runtime could not serve again (${failureCode(resumeError)}); run 'ocx start'`); - io.exitProcess(1); + await io.exitProcess(1); } return; } - io.exitProcess(0); + await io.exitProcess(0); }; return { + hasHandoff: () => flight !== null, async request() { + if (io.stopping?.()) return { kind: "stopping" }; const supervisor = await io.supervisor(); + if (io.stopping?.()) return { kind: "stopping" }; if (supervisor) return { kind: "supervised", supervisor }; if (flight) return { kind: "accepted", alreadyRestarting: true }; flight = run(now()).finally(() => { flight = null; }); diff --git a/src/client/runtime.ts b/src/client/runtime.ts index 7677170f3ae..75e3462f147 100644 --- a/src/client/runtime.ts +++ b/src/client/runtime.ts @@ -32,12 +32,17 @@ import { isLinkConnection, readClientConnectionState } from "./state"; import { withClientLifecycleSync } from "./lifecycle-lock"; import { clientRestartRollbackAction, clientRestartSupervisor, createClientRestartController, type ClientRestartController } from "./runtime-restart"; import { createLinkParityReporter, type LinkParityReporter } from "./link-parity-reporter"; +import { restoreLinkClientRouting } from "./stop-routing"; +import { markClientRuntimeStopping, markClientRuntimeStoppingWithRetry } from "./sync-runtime"; let activeServer: Server | null = null; let activePort: number | null = null; let activeSupervisor: ClientLinkSupervisor | null = null; let activeReporter: LinkParityReporter | null = null; let recycleScheduled = false; +let recycleTimer: ReturnType | undefined; +let recycleFlight: Promise | null = null; +let clientStopping = false; /** * When this runtime's process started (OCX-6, P5): what `/healthz` reports as `startedAt` and the @@ -97,7 +102,7 @@ export function clientRuntimeRecord( ): RuntimePortState { let command: string[] | undefined; try { command = currentServingCommand(); } catch { /* no reliable relaunch identity; update admission fails closed */ } - return { pid, port, hostname: "127.0.0.1", attestationSecret, ...(command ? { command } : {}), ...siblingRuntimeField() }; + return { pid, port, hostname: "127.0.0.1", attestationSecret, ...(command ? { command } : {}), ...siblingRuntimeField(), ...(clientStopping ? { clientStopping: true as const } : {}) }; } export function standaloneRecycleEnv( @@ -126,12 +131,15 @@ export function standaloneRecycleEnv( } export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string): void { - if (recycleScheduled) return; + if (recycleScheduled || clientStopping) return; recycleScheduled = true; - const timer = setTimeout(() => { - void recycleStandalone(disconnectedTokenFingerprint); + recycleTimer = setTimeout(() => { + recycleTimer = undefined; + if (clientStopping) return; + recycleFlight = recycleStandalone(disconnectedTokenFingerprint).finally(() => { recycleFlight = null; }); + void recycleFlight.catch(error => { if (!clientStopping) throw error; }); }, 50); - if (typeof timer === "object" && "unref" in timer) timer.unref(); + if (typeof recycleTimer === "object" && "unref" in recycleTimer) recycleTimer.unref(); } function configuredTcpPort(): number | undefined { @@ -421,7 +429,9 @@ export async function startClientRuntime( try { await server.stop(true); } catch { /* already closed */ } }; + let shuttingDown = false; const restartController: ClientRestartController = createClientRestartController({ + stopping: () => shuttingDown, supervisor: io.restartSupervisor ?? clientRestartSupervisor, stop: stopServing, spawnReplacement: async deadlineAt => { @@ -445,8 +455,8 @@ export async function startClientRuntime( } }, exitProcess: async code => { - await flushLinkDiagnostics(); - process.exit(code); + try { await flushLinkDiagnostics(); } + finally { process.exit(code); } }, }); @@ -455,11 +465,24 @@ export async function startClientRuntime( await serve(); options.afterPublish?.(); - let shuttingDown = false; const shutdown = (signal?: NodeJS.Signals) => { if (shuttingDown) return; shuttingDown = true; + clientStopping = true; + if (recycleTimer) { clearTimeout(recycleTimer); recycleTimer = undefined; } + const fenceSync = () => { + if (!linkMode) return true; + try { markClientRuntimeStopping({ pid: process.pid, port: servedPort, attestationSecret }); return true; } + catch { return false; } + }; + let syncFenced = fenceSync(); void (async () => { + // An accepted restart owns its replacement and rollback. A successful handoff exits + // inside that flight; a rollback finishes before this signal tears the runtime down. + if (restartController.hasHandoff()) await restartController.settled().catch(() => {}); + if (recycleFlight) await recycleFlight.catch(() => {}); + // A rollback may have republished its runtime record while shutdown joined it. + syncFenced = !linkMode || await markClientRuntimeStoppingWithRetry({ pid: process.pid, port: servedPort, attestationSecret }); credentialObservation?.stop(); credentialObservation = null; activeReporter?.stop(); @@ -470,15 +493,18 @@ export async function startClientRuntime( console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`); } activeSupervisor = null; + const restored = linkMode ? restoreLinkClientRouting(state.value, { syncFenced }) : null; + if (restored && !restored.success) console.error(`[client] ${restored.message}`); try { activeServer?.stop(true); } catch (error) { console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`); } finally { - cleanup(); - await flushLinkDiagnostics(); + // Keep stopping admission visible through the last await. Cleanup and exit are adjacent. + try { await flushLinkDiagnostics(); } finally { cleanup(); } // launchd relaunches only unsuccessful exits; an external signal must still count. - process.exit(handledSignalExitCode(signal)); + if (restored?.success === false) process.exit(1); + else process.exit(handledSignalExitCode(signal)); } })(); }; diff --git a/src/client/stop-routing.ts b/src/client/stop-routing.ts new file mode 100644 index 00000000000..f3a5377e9ee --- /dev/null +++ b/src/client/stop-routing.ts @@ -0,0 +1,62 @@ +import { existsSync } from "node:fs"; +import { restoreNativeCodex } from "../codex/inject"; +import { hasInjectedCodexRouting } from "../codex/injected-marker"; +import { readBoundedCodexConfig } from "../codex/inject/bounded-config-reader"; +import { journalOwner, JOURNAL_PATH } from "../codex/journal"; +import { CODEX_CONFIG_PATH } from "../codex/paths"; +import { siblingOfLivePort, siblingSkipMessage } from "../codex/sibling-start"; +import type { OcxClientConnectionConfig } from "../types"; +import { pendingTeardownOutstanding } from "../config/pending-teardown"; +import { stripGrokConfig } from "../grok/inject"; +import { isDesktopSupervised } from "../lib/system-restart-contract"; +import { serviceEnvironmentOwnedHere } from "../service/guards"; +import { stopRestoreWriteGuard } from "./restore-ownership"; +import { isLinkConnection } from "./state"; + +export type LinkRoutingRestoreResult = { success: boolean; restored: boolean; message: string }; + +const resumeGuidance = "After starting the Child again, run `ocx sync --restart-codex` to restore its Home routing."; +const refusal = (reason: string): LinkRoutingRestoreResult => ({ + success: false, restored: false, + message: `Link Child Codex routing restore refused: ${reason}. No further writes; inspect remaining recovery evidence.`, +}); + +/** Unsupervised signal cleanup; explicit parent stop and restart retain their own restoration. */ +export function restoreLinkClientRouting(expected: OcxClientConnectionConfig, options: { syncFenced: boolean }): LinkRoutingRestoreResult { + if (siblingOfLivePort() !== null) return { success: true, restored: false, message: siblingSkipMessage() }; + if (!isLinkConnection(expected)) { + return { success: true, restored: false, message: "Hub connection-owned integrations preserved." }; + } + const preserve = () => process.env.OCX_SERVICE === "1" || isDesktopSupervised() + || pendingTeardownOutstanding(); + if (preserve()) return { success: true, restored: false, message: "Supervised restart or pending teardown owns the client integrations." }; + if (options?.syncFenced !== true) return refusal("the runtime sync fence could not be verified"); + const guard = stopRestoreWriteGuard(expected); + const withWriteGuard = (write: () => T): T => guard.run(() => { + if (preserve()) throw new Error("restart or teardown ownership changed"); + const owner = journalOwner({ readOnly: true }); + if (existsSync(JOURNAL_PATH) && (owner?.kind !== "client" || owner.apiKeyId !== expected.apiKeyId)) { + throw new Error("the Codex journal belongs to another owner or is unverified"); + } + return write(); + }); + try { + withWriteGuard(() => { + const owner = journalOwner({ readOnly: true }); + if (owner === null) { + const config = readBoundedCodexConfig(CODEX_CONFIG_PATH); + if (existsSync(JOURNAL_PATH) || (config !== null && hasInjectedCodexRouting(config))) throw new Error("the client journal is missing or unverified"); + } + }); + const result = restoreNativeCodex({ withWriteGuard }); + if (result.artifacts.config.state === "failed" || result.historyPreflightRefusal) { + return refusal("native Codex restoration was not authorized or could not complete"); + } + const grok = serviceEnvironmentOwnedHere() ? withWriteGuard(() => stripGrokConfig()) : { ok: true, changed: false }; + if (!result.success || !grok.ok) return refusal("native Codex or managed Grok restoration did not complete"); + return { success: true, restored: result.artifacts.config.changed || result.artifacts.catalog.changed || grok.changed, + message: `Link Child native Codex and managed Grok restored; connection credentials preserved. ${resumeGuidance}` }; + } catch (error) { + return refusal(error instanceof Error ? error.message : "restoration failed"); + } +} diff --git a/src/client/sync-runtime.ts b/src/client/sync-runtime.ts new file mode 100644 index 00000000000..883b013b6e9 --- /dev/null +++ b/src/client/sync-runtime.ts @@ -0,0 +1,65 @@ +/** Cross-process admission for Link sync while its local runtime shuts down. */ +import { lstatSync } from "node:fs"; +import { ConfigMutationLockError, withConfigMutationLockSync } from "../config"; +import { getRuntimePortPath, isValidRuntimePortState, writeRuntimePort, type RuntimePortState } from "../config/process-state"; +import { readBoundedRegularFileSync } from "../lib/bounded-file-read"; + +export class ClientSyncRuntimeError extends Error { + constructor(readonly code: "client_runtime_stopping" | "client_runtime_changed" | "client_runtime_unverified") { + super(`${code}: retry sync after the Child has started; ocx start/stop can reconcile a stale runtime record`); + } +} + +function readSyncRuntime(): RuntimePortState | null { + const path = getRuntimePortPath(); + try { lstatSync(path); } + catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw new ClientSyncRuntimeError("client_runtime_unverified"); + } + try { + const value: unknown = JSON.parse(readBoundedRegularFileSync(path, 16 * 1024).toString("utf8")); + if (!isValidRuntimePortState(value)) throw new Error("invalid runtime record"); + return value; + } catch { throw new ClientSyncRuntimeError("client_runtime_unverified"); } +} + +/** Capture and recheck under C. Proven no-runtime sync keeps its existing offline behavior. */ +export function clientSyncRuntimeGuard(): () => void { + const expected = readSyncRuntime(); + if (expected?.clientStopping) throw new ClientSyncRuntimeError("client_runtime_stopping"); + return () => { + const current = readSyncRuntime(); + if (current?.clientStopping) throw new ClientSyncRuntimeError("client_runtime_stopping"); + if (!Bun.deepEquals(current, expected, true)) throw new ClientSyncRuntimeError("client_runtime_changed"); + }; +} + +/** The same C transaction excludes every sync commit; never mark a replacement runtime. */ +export function markClientRuntimeStopping(expected: Required>): void { + withConfigMutationLockSync(() => { + const current = readSyncRuntime(); + if (!current || current.pid !== expected.pid || current.port !== expected.port + || current.attestationSecret !== expected.attestationSecret) { + throw new ClientSyncRuntimeError("client_runtime_changed"); + } + if (!current.clientStopping) writeRuntimePort({ ...current, clientStopping: true }); + }); +} + +/** Yield only for proved SQLite contention; storage/identity failures remain immediate refusals. */ +export async function markClientRuntimeStoppingWithRetry( + expected: Required>, + deps: { mark?: () => void; sleep?: (ms: number) => Promise } = {}, +): Promise { + for (let attempt = 0; attempt < 20; attempt += 1) { + try { (deps.mark ?? (() => markClientRuntimeStopping(expected)))(); return true; } + catch (error) { + const cause = error instanceof ConfigMutationLockError ? error.cause : undefined; + const code = cause && typeof cause === "object" && "code" in cause ? cause.code : undefined; + if ((code !== "SQLITE_BUSY" && code !== "SQLITE_LOCKED") || attempt === 19) return false; + await (deps.sleep ?? Bun.sleep)(50); + } + } + return false; +} diff --git a/src/codex/forward-transport-headers.ts b/src/codex/forward-transport-headers.ts index e87d575f085..ff532293eeb 100644 --- a/src/codex/forward-transport-headers.ts +++ b/src/codex/forward-transport-headers.ts @@ -20,24 +20,53 @@ export const FORWARD_HEADERS = [ const MAX_ROUTING_MODEL_BYTES = 256; const MAX_ROUTING_TIER_BYTES = 64; const MAX_TURN_METADATA_BYTES = 32 * 1024; +// Each escaped code unit (U+007F+) becomes 6 ASCII bytes: at most 3x the UTF-8 size of +// non-ASCII text (DEL alone grows 6x), so 96 KiB holds any DEL-free canonical value. +const MAX_ESCAPED_TURN_METADATA_BYTES = 3 * MAX_TURN_METADATA_BYTES; /** Compatibility export: caller values never contribute to the selected account identity. */ export function deriveCodexAccountInstallationId(chatgptAccountId: string, _callerInstallationId: string): string { return codexInstallationIdForAccount(chatgptAccountId); } +/** + * Rewrites the `installation_id` inside Codex turn metadata while preserving all other fields. + * + * Structure contract: JSON with a nonblank string installation_id is returned as printable ASCII — every code unit outside + * U+0020–U+007E is escaped (JSON.stringify already escapes U+0000–U+001F, with short forms + * such as `\\n`; U+007F and above become `\\uXXXX` here). Both consumers of rewritten turn metadata (the + * `x-codex-turn-metadata` HTTP header and the embedded `client_metadata` value) + * receive this ASCII-safe form, so header setters never see literal Unicode. + * + * The 32 KiB bound applies to the canonical (unescaped UTF-8) rewritten JSON, and the escaped + * result is capped at 96 KiB. Feeding an accepted result back in is therefore a no-op: it + * parses to the same canonical JSON, so repeated isolation passes cannot refuse it. + * Invalid JSON, non-objects, and objects without a nonblank string installation_id pass + * through unchanged up to 32 KiB; their original bytes are not guaranteed header-safe. + */ export function rewriteTurnMetadataInstallationId(rawMetadata: string, chatgptAccountId: string): string { if (!chatgptAccountId.trim()) throw new CodexInstallationIdUnavailableError(); // A size bound must refuse the Pool send, never bypass identity projection. - if (Buffer.byteLength(rawMetadata, "utf8") > MAX_TURN_METADATA_BYTES) throw new CodexInstallationIdUnavailableError(); + const rawBytes = Buffer.byteLength(rawMetadata, "utf8"); + if (rawBytes > MAX_ESCAPED_TURN_METADATA_BYTES) throw new CodexInstallationIdUnavailableError(); + // Values returned unchanged keep the raw bound; only rewritten JSON is measured canonically. + const passThrough = () => { + if (rawBytes > MAX_TURN_METADATA_BYTES) throw new CodexInstallationIdUnavailableError(); + return rawMetadata; + }; let parsed: unknown; - try { parsed = JSON.parse(rawMetadata); } catch { return rawMetadata; } - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return rawMetadata; + try { parsed = JSON.parse(rawMetadata); } catch { return passThrough(); } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return passThrough(); const record = parsed as Record; - if (typeof record.installation_id !== "string" || !record.installation_id.trim()) return rawMetadata; + if (typeof record.installation_id !== "string" || !record.installation_id.trim()) return passThrough(); const nextId = deriveCodexAccountInstallationId(chatgptAccountId, record.installation_id); - if (nextId === record.installation_id) return rawMetadata; - return JSON.stringify({ ...record, installation_id: nextId }); + const canonical = JSON.stringify({ ...record, installation_id: nextId }); + if (Buffer.byteLength(canonical, "utf8") > MAX_TURN_METADATA_BYTES) throw new CodexInstallationIdUnavailableError(); + // Turn metadata is also sent as an HTTP header; DEL and non-ASCII are invalid field-value bytes. + const escaped = canonical + .replace(/[\u007f-\uffff]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`); + if (escaped.length > MAX_ESCAPED_TURN_METADATA_BYTES) throw new CodexInstallationIdUnavailableError(); + return escaped; } export function isolateCodexPoolAccountHeaders(headers: Headers, chatgptAccountId: string): void { diff --git a/src/codex/inject-coordination.ts b/src/codex/inject-coordination.ts index 04a959a5633..01e1f85e6f0 100644 --- a/src/codex/inject-coordination.ts +++ b/src/codex/inject-coordination.ts @@ -189,6 +189,9 @@ export function captureCodexPreImages(): CodexPreImages { }; } +/** The profile bytes one injection published, set just before it writes them. */ +export interface CodexProfilePublish { published?: string } + /** * How many transactions of evidence the ledger keeps. * @@ -366,10 +369,16 @@ export function recordCodexNativeTransactionProvenance( * * Every surface is attempted even after one fails: a second failure is worth * knowing about, and stopping early would leave more unrestored than necessary. + * + * `profilePublish`, when given, limits the profile surface to bytes this + * operation wrote: it is reverted only if `published` is set and the file still + * holds exactly those bytes. A refusal before the publish, or a profile another + * writer replaced since, is left as it is. */ export function restoreCodexPreImages( pre: CodexPreImages, heldConfigWriteLock?: LockHandle, + profilePublish?: CodexProfilePublish, ): { complete: boolean; unrestored: readonly string[] } { const locked = withConfigWriteLockHeld(CODEX_CONFIG_PATH, heldConfigWriteLock, held => { const unrestored: string[] = []; @@ -382,6 +391,8 @@ export function restoreCodexPreImages( // The current journal remains recovery authority until both artifacts are restored. if (name === "journal" && unrestored.length > 0) { unrestored.push(name); continue; } try { + if (name === "profile" && profilePublish + && (profilePublish.published === undefined || readOrNull(path) !== profilePublish.published)) continue; publishCodexArtifact(path, held, (destination, hooks) => { if (bytes === null) { hooks.validateBeforeRename?.(destination); diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 634f7314144..fd773b101b6 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -25,6 +25,7 @@ import { captureCodexPreImages, codexInjectLockOutcome, codexWriteCoordinationEligibility, + type CodexProfilePublish, CodexPartialWriteError, CodexWriteConflictError, DEFAULT_INJECT_LOCK_TIMEOUT_MS, @@ -82,6 +83,7 @@ import { deriveCodexInjectionPlan, type CodexInjectionPlanContext, type CodexInjectionPlanOk, + readCodexProfileSource, } from "./inject/plan"; export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target"; @@ -378,9 +380,7 @@ async function injectCodexConfigImpl( && rootTomlString(nativeInput, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl); return !hasInjectedCodexRouting(nativeInput) && !looksInjectedByValue; }; - const readCurrentProfile = (): string | null => existsSync(CODEX_PROFILE_PATH) - ? readFileSync(CODEX_PROFILE_PATH, "utf-8") - : null; + const readCurrentProfile = readCodexProfileSource; const unverifiedJournalMessage = "Codex configuration was not written: the journal has no verified baseline for the current config/profile. Current files and the journal were preserved."; // When the reconcile will rewrite config.toml under the lock, the baseline it // must be journaled against does not exist yet — this check runs inside the @@ -440,6 +440,8 @@ async function injectCodexConfigImpl( message: "Codex config injection refused: config.toml changed while the write lock was being acquired. Retry to inject on the latest bytes.", }); } + // The profile carries unmanaged root keys from the bytes the plan read; a profile edit since then would be overwritten. + if (readCurrentProfile() !== admittedPlan.profileSource) throw new CodexInjectRefusal({ success: false, retryable: true, message: "Codex config injection refused: opencodex.config.toml changed while the write lock was being acquired. Retry to inject on the latest bytes." }); if (missingConfig) publishCodexArtifact(CODEX_CONFIG_PATH, heldConfigWriteLock, (_path, hooks) => { hooks.validateBeforeRename?.(CODEX_CONFIG_PATH); createEmptyCodexConfigInBoundary(); hooks.afterRename?.(CODEX_CONFIG_PATH); }); let nativeInput = rawContent; let plan = admittedPlan; @@ -490,6 +492,7 @@ async function injectCodexConfigImpl( publishCodexArtifact(CODEX_CONFIG_PATH, heldConfigWriteLock, (path, hooks) => atomicWriteFile(path, plan.content, undefined, hooks)); historyArtifactStageForTests?.("after-config"); assertCodexHomeOwner(getCodexHome()); + profilePublish.published = plan.profileContent; publishCodexArtifact(CODEX_PROFILE_PATH, heldConfigWriteLock, (path, hooks) => atomicWriteFile(path, plan.profileContent, undefined, hooks)); publishCodexArtifact(JOURNAL_PATH, heldConfigWriteLock, (_path, hooks) => markJournalInjectedState(plan.content, plan.profileContent, { // A root override is ours whenever we wrote one and no user-owned value won. That is @@ -529,6 +532,7 @@ async function injectCodexConfigImpl( * legacy-uncoordinated home, which publishes no transition to resolve. */ let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined; + const profilePublish: CodexProfilePublish = {}; // rollback reverts only profile bytes this operation published /* * The file-level write lock every config.toml writer shares (see @@ -594,7 +598,7 @@ async function injectCodexConfigImpl( effectivePlan = resolved.plan; } catch (error) { if (error instanceof ConfigWriteDestinationChanged || codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error; - const restored = restoreCodexPreImages(preImages, heldConfigWriteLock); + const restored = restoreCodexPreImages(preImages, heldConfigWriteLock, profilePublish); if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored); throw error; } @@ -614,7 +618,7 @@ async function injectCodexConfigImpl( ...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}), onPostCallbackFailure: () => { if (!coordinatedPreImages) throw new Error("Codex injection preimages were not captured."); - const restored = restoreCodexPreImages(coordinatedPreImages, heldConfigWriteLock); + const restored = restoreCodexPreImages(coordinatedPreImages, heldConfigWriteLock, profilePublish); if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored); }, admitted: { authoritySnapshotId: witness.comparisonId }, @@ -691,7 +695,7 @@ async function injectCodexConfigImpl( if (error instanceof ConfigWriteDestinationChanged || codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error; // Compensate, then ALWAYS throw. Returning a partial result would let the // lock commit a row describing an apply that did not finish. - const restored = restoreCodexPreImages(preImages, heldConfigWriteLock); + const restored = restoreCodexPreImages(preImages, heldConfigWriteLock, profilePublish); if (!restored.complete) { throw new CodexPartialWriteError(restored.unrestored); } diff --git a/src/codex/inject/config-toml.ts b/src/codex/inject/config-toml.ts index 7c4a69065a2..293a6ad7347 100644 --- a/src/codex/inject/config-toml.ts +++ b/src/codex/inject/config-toml.ts @@ -672,6 +672,94 @@ export function buildProfileFileForTarget( return lines.join("\n"); } +/** + * Root keys this build owns in either profile shape. They are never carried + * forward: leaving provider-table mode drops `model_provider` from the template, + * and a caller that passes no catalog path drops `model_catalog_json`, so a + * carried copy would point the profile at routing this build no longer writes. + */ +const MANAGED_PROFILE_ROOT_KEYS = new Set([ + "model_provider", + "model_catalog_json", + "openai_base_url", +]); + +/** + * Root keys the previous profile carried that this build does not write. + * + * Operators add Codex settings such as `stream_idle_timeout_ms` and + * `stream_max_retries` to `opencodex.config.toml`. Rebuilding the file from the + * template dropped them without a word (#8066), so any root key this build does + * not own is carried over, ahead of the first table. Values that span several + * lines are copied whole, because one line of a multi-line value would not + * parse, and every carried assignment keeps its original bytes: nothing is + * decoded and reserialized, so a 64-bit integer Bun cannot represent survives. + * + * A carried root key named like a table the generated profile writes (an inline + * `features = { ... }` against a generated `[features]`) is dropped: TOML + * forbids defining the same table twice, and the generated table wins because + * it carries the settings this build manages. + */ +export function preserveUnmanagedProfileKeys(existing: string | null, generated: string): string { + if (!existing) return generated; + const generatedKeys = rootKeysOf(generated); + if (generatedKeys.size === 0) return generated; + const generatedTables = topLevelTableNamesOf(generated); + + const existingSource = rootSourceLines(existing); + const carried: string[] = []; + const carriedKeys = new Set(); + for (let index = 0; index < existingSource.rootEnd; index += 1) { + if (!existingSource.lines[index]?.structural) continue; + const span = sourceAssignmentSpan(existingSource.lines, index, existingSource.rootEnd); + if (!span) continue; + index = span.end - 1; + if (generatedKeys.has(span.key) || MANAGED_PROFILE_ROOT_KEYS.has(span.key)) continue; + if (generatedTables.has(span.key)) continue; + if (carriedKeys.has(span.key)) continue; + carriedKeys.add(span.key); + // Drop only trailing blank lines; the assignment's own bytes, trailing spaces included, stay. + carried.push(span.text.replace(/(?:\r?\n[ \t]*)+$/, "")); + } + if (carried.length === 0) return generated; + + const lines = generated.split("\n"); + const firstTable = lines.findIndex((line) => /^\s*\[/.test(line)); + lines.splice(firstTable === -1 ? lines.length : firstTable, 0, [ + "# Root keys OpenCodex does not manage, kept from the previous profile:", + ...carried, + "", + ].join("\n")); + return lines.join("\n"); +} + +/** Names of the root assignments in a TOML document, before its first table. */ +function rootKeysOf(content: string): Set { + const keys = new Set(); + const source = rootSourceLines(content); + for (let index = 0; index < source.rootEnd; index += 1) { + if (!source.lines[index]?.structural) continue; + const span = sourceAssignmentSpan(source.lines, index, source.rootEnd); + if (!span) continue; + keys.add(span.key); + index = span.end - 1; + } + return keys; +} + +/** First path segment of every table header in a generated document. */ +function topLevelTableNamesOf(content: string): Set { + const names = new Set(); + for (const line of rootSourceLines(content).lines) { + if (!line.structural || !/^\s*\[/.test(line.text)) continue; + try { + const name = Object.keys(Bun.TOML.parse(line.text))[0]; + if (name !== undefined) names.add(name); + } catch { /* the template only writes valid headers */ } + } + return names; +} + export function chooseCatalogPathForInjection( content: string, requested?: string | null, diff --git a/src/codex/inject/plan.ts b/src/codex/inject/plan.ts index 10918796d1e..4b027342229 100644 --- a/src/codex/inject/plan.ts +++ b/src/codex/inject/plan.ts @@ -9,6 +9,7 @@ * post-transition bytes is what keeps the committed file from re-enabling the * flag the reconcile just turned off. */ +import { existsSync, readFileSync } from "node:fs"; import { websocketsEnabled } from "../../config"; import { HISTORY_RELABEL_STANDS_DOWN, @@ -22,7 +23,7 @@ import { journaledReplacedRootWebSearch, } from "../journal"; import { stripJournaledOpenaiBaseUrl } from "../injected-marker"; -import { CODEX_CONFIG_PATH, resolveCodexStateDbPath } from "../paths"; +import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, resolveCodexStateDbPath } from "../paths"; import { transformManagedSubagentDefaults, type WrittenSubagentDefaults } from "../subagent-defaults"; import type { OcxConfig } from "../../types"; import type { CodexWriteCandidate } from "../write-coordination"; @@ -35,6 +36,7 @@ import { ensureFastModeFeature, ensureRootWebSearchDisabled, normalizeServiceTier, + preserveUnmanagedProfileKeys, removeProfileSection, setRootModelCatalogPath, setRootModelProvider, @@ -71,6 +73,11 @@ export interface CodexInjectionPlanOk { content: string; /** The exact string about to replace the profile file. */ profileContent: string; + /** + * The profile bytes `profileContent` preserved keys from (null when absent). + * Re-checked under the write lock so an edit landing meanwhile is not overwritten. + */ + profileSource: string | null; /** The resolved catalog path, never the raw option. */ catalogPath: string | null; providerTableMode: boolean; @@ -323,12 +330,16 @@ export function deriveCodexInjectionPlan( managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`; } - const profileContent = buildProfileFileForTarget( - routingTarget, - catalogPath, - websocketsForRoutingTarget(config, routingTarget), - config?.fastMode, - config?.codexProviderDisplayName, + const profileSource = readCodexProfileSource(); + const profileContent = preserveUnmanagedProfileKeys( + profileSource, + buildProfileFileForTarget( + routingTarget, + catalogPath, + websocketsForRoutingTarget(config, routingTarget), + config?.fastMode, + config?.codexProviderDisplayName, + ), ); content = applyEol(content, eol); @@ -395,6 +406,7 @@ export function deriveCodexInjectionPlan( baselineContent, content, profileContent, + profileSource, catalogPath, providerTableMode, keepRootOverrideAlongsideTable, @@ -414,3 +426,18 @@ export function deriveCodexInjectionPlan( }, }; } + + +/** + * Current profile bytes, or null when absent. Read directly: a profile removed between an + * existence check and the read is simply absent. If it reappears before the write lock, the + * lock-time comparison refuses retryably; other read errors propagate. + */ +export function readCodexProfileSource(): string | null { + try { + return readFileSync(CODEX_PROFILE_PATH, "utf-8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + return null; + } +} diff --git a/src/codex/toml-source-lines.ts b/src/codex/toml-source-lines.ts index 1085bd1e3be..ba9d1e71963 100644 --- a/src/codex/toml-source-lines.ts +++ b/src/codex/toml-source-lines.ts @@ -84,22 +84,73 @@ export function sourceAssignmentSpan(lines: readonly SourceLine[], start: number const decoded = sourceAssignment(lines, start, limit); if (decoded) return decoded; const span = lexicalAssignmentSpan(lines, start, limit); - // Bun rejects valid TOML integers outside the JS safe-integer range. Only a complete - // scalar integer gets this exception: collections, strings and other parse failures do not. - if (!span || span.end !== start + 1) return null; - const keyToken = assignmentKeyToken.exec(span.text)!; - const scalar = /^([ \t]*)([+-]?(?:0|[1-9](?:_?[0-9])*)|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*)([ \t]*(?:#[^\r\n]*)?)$/.exec(span.text.slice(keyToken[0].length)); - if (!scalar) return null; - const integer = BigInt(scalar[2]!.replaceAll("_", "")); - if (integer < -(1n << 63n) || integer >= (1n << 63n)) return null; - // Keep the parser responsible for the key, separator and comment grammar. Replacing - // this already validated integer for validation never edits or decodes the source value. + if (!span) return null; + // Bun rejects valid TOML integers outside the JS safe-integer range, as scalars and + // inside arrays or inline tables alike. Validate a copy in which each such integer that + // fits signed 64 bits reads `0`; the parser stays responsible for every other piece of + // grammar, and the returned span keeps the original bytes untouched. + const masked = maskWideIntegers(span.text); + if (masked === null) return null; try { - const parsed = Bun.TOML.parse(`${keyToken[0]}${scalar[1]}0${scalar[3]}`); + const parsed = Bun.TOML.parse(masked); return Object.keys(parsed).length === 1 && Object.hasOwn(parsed, span.key) ? span : null; } catch { return null; } } +const integerToken = /^(?:[+-]?(?:0|[1-9](?:_?[0-9])*)|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*)$/; +const bareToken = /[A-Za-z0-9_+.:-]/; + +/** + * The assignment with every integer outside the JS safe range replaced by `0`, skipping + * strings and comments. Null when an integer does not fit signed 64 bits (TOML requires a + * reader to reject it) or when nothing needed masking, so the original parse failure stands. + */ +function maskWideIntegers(text: string): string | null { + let out = ""; + let masked = false; + let index = 0; + const copyUntil = (end: number) => { out += text.slice(index, end); index = end; }; + while (index < text.length) { + const char = text[index]!; + if (text.startsWith('"""', index) || text.startsWith("'''", index)) { + const delimiter = text.slice(index, index + 3); + let cursor = index + 3; + while (cursor < text.length && !text.startsWith(delimiter, cursor)) cursor += delimiter === '"""' && text[cursor] === "\\" ? 2 : 1; + cursor = Math.min(text.length, cursor + 3); + // A closing delimiter may carry up to two more quote characters of content. + for (let extra = 0; extra < 2 && text[cursor] === delimiter[0]; extra += 1) cursor += 1; + copyUntil(cursor); + } else if (char === '"' || char === "'") { + let cursor = index + 1; + while (cursor < text.length && text[cursor] !== char && text[cursor] !== "\n") cursor += char === '"' && text[cursor] === "\\" ? 2 : 1; + copyUntil(Math.min(text.length, cursor + 1)); + } else if (char === "#") { + const newline = text.indexOf("\n", index); + copyUntil(newline === -1 ? text.length : newline); + } else if (bareToken.test(char)) { + let cursor = index + 1; + while (cursor < text.length && bareToken.test(text[cursor]!)) cursor += 1; + const token = text.slice(index, cursor); + // A digit-only bare key (`{ 9223372036854775808 = 1 }`) is a name, not a number. + const isKey = /^[ \t]*[=.]/.test(text.slice(cursor)); + if (!isKey && integerToken.test(token)) { + const integer = BigInt(token.replaceAll("_", "")); + if (integer < -(1n << 63n) || integer >= (1n << 63n)) return null; + if (integer > BigInt(Number.MAX_SAFE_INTEGER) || integer < -BigInt(Number.MAX_SAFE_INTEGER)) { + out += "0"; + index = cursor; + masked = true; + continue; + } + } + copyUntil(cursor); + } else { + copyUntil(index + 1); + } + } + return masked ? out : null; +} + /** Cosmetic document whitespace only; opaque value lines retain every byte. */ export function normalizeStructuralWhitespace(content: string): string { const { bom, lines } = rootSourceLines(content); @@ -122,6 +173,13 @@ export function normalizeStructuralWhitespace(content: string): string { type MultilineStringKind = "basic" | "literal" | null; +/** Length of a closing multiline delimiter: TOML allows a run of three to five quotes. */ +function quoteRunLength(text: string, index: number, quote: string): number { + let end = index; + while (end < text.length && text[end] === quote && end - index < 5) end += 1; + return end - index; +} + /** * TOML table-looking text inside a multiline string is data, not syntax. Keep * a deliberately small lexical state machine so the format-preserving editor @@ -139,8 +197,9 @@ function markStructuralLines(lines: SourceLine[]): void { for (let index = 0; index < line.text.length;) { if (multiline === "basic") { if (line.text.startsWith('"""', index)) { + // A closing delimiter may carry up to two content quotes before it. multiline = null; - index += 3; + index += quoteRunLength(line.text, index, '"'); } else if (line.text[index] === "\\") { index += 2; } else { @@ -151,7 +210,7 @@ function markStructuralLines(lines: SourceLine[]): void { if (multiline === "literal") { if (line.text.startsWith("'''", index)) { multiline = null; - index += 3; + index += quoteRunLength(line.text, index, "'"); } else { index += 1; } diff --git a/src/config/process-state.ts b/src/config/process-state.ts index b7fa2349112..5b71fd4f430 100644 --- a/src/config/process-state.ts +++ b/src/config/process-state.ts @@ -39,6 +39,8 @@ export type RuntimePortState = { command?: string[]; /** Per-process proof key; protected by the config directory and never served. */ attestationSecret?: string; + /** A Link runtime is draining; new and in-flight sync commits must stand down. */ + clientStopping?: true; /** * The live proxy's port when this runtime is a sibling instance started beside it * (`src/codex/sibling-start.ts`). `ocx stop` reads it to leave shared client routing alone. @@ -61,6 +63,7 @@ export function isValidRuntimePortState(value: unknown): value is RuntimePortSta && Number(state.port) <= 65535 && hostnameOk && attestationOk + && (state.clientStopping === undefined || state.clientStopping === true) && siblingOk; } diff --git a/src/providers/quota/vendor-probes-key.ts b/src/providers/quota/vendor-probes-key.ts index 6b2c713f5b9..e34d581d787 100644 --- a/src/providers/quota/vendor-probes-key.ts +++ b/src/providers/quota/vendor-probes-key.ts @@ -1125,18 +1125,17 @@ export function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean { return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`; } -/** Prefer the nested `data` shell when the outer object is only an envelope. */ -function unwrapKimiQuotaPayload(value: unknown): Record | null { - const body = asRecord(value); - if (!body) return null; - const nested = asRecord(body.data); - if (!nested) return body; - // A null/non-usable outer field is a placeholder, not data — an envelope like - // { usage: null, data: { usage: {...} } } must still unwrap to the nested payload. +/** + * Pick the envelope carrying the legacy windows (weekly, 5h, total credits). The monthly + * `usages` container is located separately, so its presence never hides a nested window. + */ +function kimiLegacyWindowBody(root: Record): Record { + const nested = asRecord(root.data); + if (!nested) return root; + // A null outer field is a placeholder, not data — { usage: null, data: { usage: {...} } } unwraps. const usable = (field: unknown): boolean => field !== undefined && field !== null; - const outerHasUsage = usable(body.usage) || usable(body.limits) || usable(body.totalQuota); - const nestedHasUsage = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota); - return !outerHasUsage && nestedHasUsage ? nested : body; + const hasLegacy = (body: Record): boolean => usable(body.usage) || usable(body.limits) || usable(body.totalQuota); + return !hasLegacy(root) && hasLegacy(nested) ? nested : root; } function kimiLimitLabel(item: Record, detail: Record): string { @@ -1146,21 +1145,43 @@ function kimiLimitLabel(item: Record, detail: Record): number | undefined { + const limit = toFiniteNumber(row.limit); + if (limit === undefined || limit <= 0) return undefined; + const used = toFiniteNumber(row.used); + const remaining = toFiniteNumber(row.remaining); + if ((used !== undefined && used >= limit) || (remaining !== undefined && remaining <= 0)) return 100; + if ((used !== undefined && used < 0) || (remaining !== undefined && remaining > limit)) return undefined; + if (used !== undefined) return (used / limit) * 100; + if (remaining === undefined) return undefined; + return Math.min(((limit - remaining) / limit) * 100, KIMI_BELOW_EXHAUSTED_PERCENT); +} + +/** Parse consumed usage, preferring valid counters, then a valid [0, 1] ratio, then direct percentages. */ function parseKimiQuotaRow(value: unknown, resetFallback?: Record): { percent: number; resetAt?: number } | null { const row = asRecord(value); if (!row) return null; const resetAt = quotaResetAt(row) ?? (resetFallback ? quotaResetAt(resetFallback) : undefined); - const limit = toFiniteNumber(row.limit); - if (limit !== undefined && limit > 0) { - let used = toFiniteNumber(row.used); - if (used === undefined) { - const remaining = toFiniteNumber(row.remaining); - if (remaining !== undefined) used = limit - remaining; - } - if (used !== undefined) { - const percent = normalizePercent((used / limit) * 100); - if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) }; - } + const counterPercent = kimiCounterPercent(row); + if (counterPercent !== undefined) { + const percent = normalizePercent(counterPercent); + if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) }; + } + // Newer Kimi responses expose utilization as a fraction in [0, 1]. + const ratio = toFiniteNumber(row.used_ratio ?? row.usedRatio); + if (ratio !== undefined && ratio >= 0 && ratio <= 1) { + const percent = normalizePercent(ratio * 100); + if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) }; } // Some payloads expose utilisation directly when limit/used arithmetic is absent. const direct = normalizePercent(row.utilization ?? row.percent ?? row.usedPercent ?? row.used_percent); @@ -1181,11 +1202,22 @@ function isKimiWeeklyLimit(item: Record, detail: Record; + const keys = Object.keys(record); + if (keys.length === 1 && keys[0] === "input" && typeof record.input === "string") text = record.input; + } + } catch { + // Ordinary JavaScript and patches are not JSON wrappers. + } + return stripMarkdownCodeFence(text, toolName); +} + /** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */ export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string { if (typeof argumentsText !== "string") return ""; @@ -48,13 +64,13 @@ export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): const record = parsed as Record; if (Object.prototype.hasOwnProperty.call(record, "input")) { return typeof record.input === "string" - ? stripMarkdownCodeFence(record.input, toolName) + ? repairUnwrappedBody(record.input, toolName) : argumentsText; } const fallbackKeys = freeformFallbackKeys(toolName); const candidates = fallbackKeys.filter(key => typeof record[key] === "string"); if (candidates.length === 1) { - return stripMarkdownCodeFence(record[candidates[0]] as string, toolName); + return repairUnwrappedBody(record[candidates[0]] as string, toolName); } } } catch { diff --git a/src/responses/freeform-wrapper-scan.ts b/src/responses/freeform-wrapper-scan.ts index 581e9a5b1d6..27d7aeca471 100644 --- a/src/responses/freeform-wrapper-scan.ts +++ b/src/responses/freeform-wrapper-scan.ts @@ -56,7 +56,9 @@ const NEVER = -2; export type FreeformWrapperScan = | { kind: "hold" } | { kind: "raw" } - | { kind: "input"; valueStart: number }; + // `canonical` is false for a fallback field, which a closed object resolved and trailing text + // can still withdraw; an own `input` is decided by its prefix and stays decided. + | { kind: "input"; valueStart: number; canonical: boolean }; /** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */ function skipWhitespace(text: string, from: number): number { @@ -249,7 +251,7 @@ export function scanFreeformWrapper(text: string, fallbackKeys: readonly string[ // non-string value is decidable too, in the other direction: completion hands back the // argument text unchanged rather than unwrapping a value it cannot use. return bounded[valueAt] === '"' - ? { kind: "input", valueStart: valueAt + 1 } + ? { kind: "input", valueStart: valueAt + 1, canonical: true } : { kind: "raw" }; } @@ -300,7 +302,7 @@ function afterTopLevelClose( if (!wholeText) return { kind: "hold" }; const candidates = fallbackKeys.filter(key => members.get(key)?.stringValue === true); if (candidates.length === 1) { - return { kind: "input", valueStart: members.get(candidates[0]!)!.valueStart }; + return { kind: "input", valueStart: members.get(candidates[0]!)!.valueStart, canonical: false }; } return { kind: "raw" }; } diff --git a/src/responses/progressive-freeform-input.ts b/src/responses/progressive-freeform-input.ts index e400c780941..5c31c422f6d 100644 --- a/src/responses/progressive-freeform-input.ts +++ b/src/responses/progressive-freeform-input.ts @@ -1,5 +1,10 @@ import { freeformFallbackKeys } from "./apply-patch-envelope"; -import { JSON_ESCAPES, scanFreeformWrapper } from "./freeform-wrapper-scan"; +import { + JSON_ESCAPES, + JSON_WHITESPACE, + MAX_FREEFORM_WRAPPER_SCAN_CHARS, + scanFreeformWrapper, +} from "./freeform-wrapper-scan"; /** * Whether a body could still grow into one complete outer Markdown fence. @@ -18,66 +23,291 @@ function mayBecomeFencedBody(text: string, toolName: string): boolean { } const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/; +const HEX_DIGIT = /^[0-9a-fA-F]$/; +const HEX_QUAD = /^[0-9a-fA-F]{4}$/; +const JSON_NUMBER_CHAR = /^[-+0-9.eE]$/; +const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/; +const JSON_LITERALS = ["true", "false", "null"] as const; +const CANONICAL_KEY = "input"; +const STRING_END = Symbol("string end"); + +const ownsBodyRepair = (toolName: string): boolean => toolName === "exec" || toolName === "apply_patch"; + +type NestedState = + | "start" | "keyOpen" | "key" | "colon" | "valueOpen" | "string" + | "scalar" | "container" | "afterValue" | "trailing" | "never"; /** - * The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve. + * Whether a decoded executable body can still grow into the one extra `{"input":string}` + * wrapper that completion removes, answered one character at a time so a streamed body is walked + * once rather than once per delta. + * + * Ordinary object-leading JavaScript such as `{ let x` or `({` fails within a few characters and + * streams from there; only a prefix that is still a possible wrapper holds. A false answer never + * turns true again as the body grows, so the held prefix is flushed once and streaming continues. + * + * Every member name must be `input`, because completion unwraps only a single-key object, and + * duplicates are followed because `JSON.parse` keeps the last. A non-string value is skipped to its + * end so a later duplicate can still replace it, and an object that closes on a non-string last + * value is ruled out. Scalars are checked against the JSON grammar; containers are skipped by + * bracket and string tracking alone. That skip is looser than `JSON.parse` but never unsound: it + * finds the true end of every valid container, and any text it misreads is invalid JSON, which + * completion would not unwrap either way. + */ +class NestedWrapperMatcher { + scanned = 0; + private state: NestedState = "start"; + private key = ""; + /** Pending escape inside the current string: "", a backslash, or `\\u` and the hex read so far. */ + private escape = ""; + private scalar = ""; + private readonly closers: string[] = []; + private inContainerString = false; + private containerEscape = false; + private lastValueString = false; + + get possible(): boolean { + return this.state !== "never"; + } + + feed(text: string): boolean { + for (let i = 0; i < text.length && this.state !== "never"; i++) { + this.scanned++; + this.step(text[i]!); + } + return this.possible; + } + + private step(c: string): void { + switch (this.state) { + case "start": + if (!JSON_WHITESPACE.has(c)) this.state = c === "{" ? "keyOpen" : "never"; + return; + case "keyOpen": + if (JSON_WHITESPACE.has(c)) return; + this.state = c === '"' ? "key" : "never"; + this.key = ""; + this.escape = ""; + return; + case "key": { + const part = this.stringStep(c); + if (part === null) this.state = "never"; + else if (part === STRING_END) this.state = this.key === CANONICAL_KEY ? "colon" : "never"; + else if (!CANONICAL_KEY.startsWith(this.key += part)) this.state = "never"; + return; + } + case "colon": + if (!JSON_WHITESPACE.has(c)) this.state = c === ":" ? "valueOpen" : "never"; + return; + case "valueOpen": + if (JSON_WHITESPACE.has(c)) return; + this.lastValueString = c === '"'; + this.escape = ""; + if (c === '"') this.state = "string"; + else this.startOtherValue(c); + return; + case "string": { + const part = this.stringStep(c); + if (part === null) this.state = "never"; + else if (part === STRING_END) this.state = "afterValue"; + return; + } + case "scalar": + this.stepScalar(c); + return; + case "container": + this.stepContainer(c); + return; + case "afterValue": + if (JSON_WHITESPACE.has(c)) return; + if (c === ",") this.state = "keyOpen"; + else this.state = c === "}" && this.lastValueString ? "trailing" : "never"; + return; + case "trailing": + if (!JSON_WHITESPACE.has(c)) this.state = "never"; + return; + case "never": + return; + } + } + + /** + * One character of a JSON string: the text it decodes to ("" while an escape is pending), + * `STRING_END` at the closing quote, or `null` when no continuation can parse. Only key + * text needs the decoded value, so escapes other than `\\u` keep a marker. + */ + private stringStep(c: string): string | typeof STRING_END | null { + if (this.escape === "") { + if (c === '"') return STRING_END; + if (c.charCodeAt(0) <= 0x1f) return null; + if (c !== "\\") return c; + this.escape = "\\"; + return ""; + } + if (this.escape === "\\") { + if (c === "u") { + this.escape = "\\u"; + return ""; + } + if (!JSON_ESCAPES.has(c)) return null; + this.escape = ""; + return "\\" + c; + } + if (!HEX_DIGIT.test(c)) return null; + this.escape += c; + if (this.escape.length < 6) return ""; + const code = parseInt(this.escape.slice(2), 16); + this.escape = ""; + return String.fromCharCode(code); + } + + private startOtherValue(c: string): void { + if (c === "{" || c === "[") { + this.closers.length = 0; + this.closers.push(c === "{" ? "}" : "]"); + this.inContainerString = false; + this.containerEscape = false; + this.state = "container"; + return; + } + if (JSON_NUMBER_CHAR.test(c) || c === "t" || c === "f" || c === "n") { + this.scalar = c; + this.state = "scalar"; + return; + } + this.state = "never"; + } + + private stepScalar(c: string): void { + if (JSON_WHITESPACE.has(c) || c === "," || c === "}") { + const complete = (JSON_LITERALS as readonly string[]).includes(this.scalar) || JSON_NUMBER.test(this.scalar); + if (!complete) { + this.state = "never"; + return; + } + this.state = "afterValue"; + this.step(c); + return; + } + this.scalar += c; + const literal = JSON_LITERALS.find(word => word[0] === this.scalar[0]); + const viable = literal ? literal.startsWith(this.scalar) : JSON_NUMBER_CHAR.test(c); + if (!viable) this.state = "never"; + } + + private stepContainer(c: string): void { + if (this.inContainerString) { + if (this.containerEscape) this.containerEscape = false; + else if (c === "\\") this.containerEscape = true; + else if (c === '"') this.inContainerString = false; + return; + } + if (c === '"') this.inContainerString = true; + else if (c === "{" || c === "[") this.closers.push(c === "{" ? "}" : "]"); + else if (c === "}" || c === "]") { + if (this.closers.pop() !== c) this.state = "never"; + else if (this.closers.length === 0) this.state = "afterValue"; + } + } +} + +type DecodeStop = "open" | "closed" | "stalled" | "invalid"; + +/** + * Decode the JSON string body in `text` from `from`, stopping at the first byte it cannot + * resolve. `next` is where decoding resumes once more text arrives; `scanned` counts the characters + * read, including a split escape that will be read again. * * Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value * and anything invented here would be retracted. Three of them are not obvious: * * An escape JSON does not define makes the whole wrapper unparseable no matter what arrives - * next, so completion falls back to the raw text. Returning `null` for that case stops the - * preview rather than continuing to decode a value the completed item will never carry. - * `\\b` and `\\f` are defined, and were previously decoded to the letters b and f. + * next, so completion falls back to the raw text. `invalid` stops the preview rather than + * continuing to decode a value the completed item will never carry. `\\b` and `\\f` are defined, + * and were previously decoded to the letters b and f. A literal control character is not legal + * inside a JSON string either, and stops the same way. * * A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a - * delta the client has to decode by itself, so the pair is emitted together or not at all. + * delta the client has to decode by itself, so the pair is emitted together or not at all; a + * complete escape that is not its low half leaves the preview `stalled` for the rest of the call. */ -function decodeJsonStringPrefix(body: string): string | null { - let out = ""; - for (let i = 0; i < body.length; i++) { - const c = body[i]; - if (c === '"') break; // unescaped closing quote: value complete +function decodeJsonStringChunk( + text: string, + from: number, +): { chunk: string; next: number; stop: DecodeStop; scanned: number } { + let chunk = ""; + let scanned = 0; + let i = from; + let stop: DecodeStop = "open"; + while (i < text.length) { + const c = text[i]!; + scanned++; + if (c === '"') { + stop = "closed"; + break; + } if (c !== "\\") { - // A literal control character is not legal inside a JSON string, so `JSON.parse` will - // reject this wrapper and completion will return the raw text. Emitting the decoded value - // first is the disagreement this decoder exists to prevent, so stop instead. - if (c!.charCodeAt(0) <= 0x1f) return null; - out += c; + if (c.charCodeAt(0) <= 0x1f) { + stop = "invalid"; + break; + } + chunk += c; + i++; continue; } - const n = body[i + 1]; + const n = text[i + 1]; if (n === undefined) break; // escape split across chunks: wait for more + scanned++; if (n === "u") { - const hex = body.slice(i + 2, i + 6); + const hex = text.slice(i + 2, i + 6); + scanned += hex.length; if (hex.length < 4) break; // split across chunks: wait for more - if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses + if (!HEX_QUAD.test(hex)) { + stop = "invalid"; + break; + } const code = parseInt(hex, 16); if (code >= 0xd800 && code <= 0xdbff) { - const low = body.slice(i + 6, i + 12); - if (!LOW_SURROGATE_ESCAPE.test(low)) break; - out += String.fromCharCode(code, parseInt(low.slice(2), 16)); - i += 11; + const low = text.slice(i + 6, i + 12); + scanned += low.length; + if (!LOW_SURROGATE_ESCAPE.test(low)) { + if (low.length === 6) stop = "stalled"; + break; + } + chunk += String.fromCharCode(code, parseInt(low.slice(2), 16)); + i += 12; continue; } - out += String.fromCharCode(code); - i += 5; + chunk += String.fromCharCode(code); + i += 6; continue; } const escaped = JSON_ESCAPES.get(n); - if (escaped === undefined) return null; // never parses - out += escaped; - i += 1; + if (escaped === undefined) { + stop = "invalid"; + break; + } + chunk += escaped; + i += 2; } - return out; + return { chunk, next: i, stop, scanned }; } /** - * The value to stream so far, or `null` to HOLD because nothing can be decided yet. + * The value to stream so far for one call's growing arguments, or `null` to HOLD because nothing + * can be decided yet. + * + * A reader belongs to one call and is fed that call's whole accumulated arguments on each delta. + * They only grow, so it resumes where the previous delta stopped: the canonical value is decoded + * and checked for a nested wrapper once overall, which keeps a long body streamed in small deltas + * linear instead of quadratic. A shorter buffer or a different tool name starts over. + * * * `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the * key is present, whatever else the object carries, so its value can be unescaped - * progressively and never retracted. + * progressively and never retracted, except for executable bodies that may contain one + * extra canonical wrapper. Such a body holds only while its prefix can still become that + * wrapper (`mayBecomeNestedWrapper`); the first byte that rules it out flushes the held text. * * One case escapes that claim and is accepted rather than fixed: a duplicate `input` key. * `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with @@ -106,19 +336,98 @@ function decodeJsonStringPrefix(body: string): string | null { * cost of holding is preview on an object body that turns out not to be a wrapper; the cost of * not holding was publishing bytes the completed item removes. */ -export function progressiveFreeformInput(args: string, toolName: string): string | null { - const scan = scanFreeformWrapper(args, freeformFallbackKeys(toolName)); - if (scan.kind === "input") { - const decoded = decodeJsonStringPrefix(args.slice(scan.valueStart)); - if (decoded === null) return null; - return mayBecomeFencedBody(decoded, toolName) ? null : decoded; +export class ProgressiveFreeformInputReader { + private toolName: string | undefined; + private seen = 0; + private mode: "scan" | "raw" | "hold" | "input" = "scan"; + private next = 0; + private decoded = ""; + private stop: DecodeStop = "open"; + private nested: NestedWrapperMatcher | null = null; + private fenceRuledOut = false; + private decodeScanned = 0; + + /** Characters this reader has walked so far; a test seam for the linear-work bound. */ + get scannedChars(): number { + return this.decodeScanned + (this.nested?.scanned ?? 0); + } + + update(args: string, toolName: string): string | null { + if (toolName !== this.toolName || args.length < this.seen) this.reset(toolName); + this.seen = args.length; + if (this.mode === "scan") { + const scan = scanFreeformWrapper(args, freeformFallbackKeys(toolName)); + if (scan.kind === "raw") { + // Prefix-decided: no extension of this text becomes a wrapper again. + this.mode = "raw"; + } else if (scan.kind === "hold") { + // Past the bound the scan only ever sees the same clamped prefix, so it holds for good. + if (args.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS) this.mode = "hold"; + return null; + } else if (scan.canonical) { + this.mode = "input"; + this.next = scan.valueStart; + this.nested = ownsBodyRepair(toolName) ? new NestedWrapperMatcher() : null; + } else { + // A closed fallback wrapper can still be withdrawn by trailing text, so it is decided + // afresh on each delta. Its object fits inside the scan bound, which bounds this too. + return this.closedFallbackValue(args, scan.valueStart, toolName); + } + } + if (this.mode === "hold") return null; + if (this.mode === "raw") return this.mayBecomeFenced(args) ? null : args; + return this.canonicalValue(args); + } + + private reset(toolName: string): void { + this.toolName = toolName; + this.seen = 0; + this.mode = "scan"; + this.next = 0; + this.decoded = ""; + this.stop = "open"; + this.nested = null; + this.fenceRuledOut = false; } - if (scan.kind === "raw") return mayBecomeFencedBody(args, toolName) ? null : args; - - // Undecided: an incomplete object, or a close whose tail is clamped out of view. Both stay - // held — the first is provably incomplete, and re-walking a budget-exhausted buffer on every - // delta is work for a preview that arrives in the same instant as the authoritative - // completion behind it. A closed object the scan could see whole never reaches here: its - // member table already resolved it to `input` or `raw`. - return null; + + private canonicalValue(args: string): string | null { + if (this.stop === "open") { + const step = decodeJsonStringChunk(args, this.next); + this.decodeScanned += step.scanned; + this.next = step.next; + this.stop = step.stop; + this.decoded += step.chunk; + if (this.nested?.possible) this.nested.feed(step.chunk); + } + if (this.stop === "invalid") return null; + // Completion may remove one extra {input:string} body wrapper, so hold while it can. + if (this.nested?.possible) return null; + return this.mayBecomeFenced(this.decoded) ? null : this.decoded; + } + + private closedFallbackValue(args: string, valueStart: number, toolName: string): string | null { + const step = decodeJsonStringChunk(args, valueStart); + this.decodeScanned += step.scanned; + if (step.stop === "invalid") return null; + if (ownsBodyRepair(toolName)) { + const nested = new NestedWrapperMatcher(); + const possible = nested.feed(step.chunk); + this.decodeScanned += nested.scanned; + if (possible) return null; + } + return mayBecomeFencedBody(step.chunk, toolName) ? null : step.chunk; + } + + /** `mayBecomeFencedBody`, remembering a false answer: a ruled-out fence never returns. */ + private mayBecomeFenced(text: string): boolean { + if (this.fenceRuledOut) return false; + if (mayBecomeFencedBody(text, this.toolName ?? "")) return true; + this.fenceRuledOut = true; + return false; + } +} + +/** One-shot form of `ProgressiveFreeformInputReader` for a buffer read once. */ +export function progressiveFreeformInput(args: string, toolName: string): string | null { + return new ProgressiveFreeformInputReader().update(args, toolName); } diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 85a80da579b..a3f27971255 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -562,26 +562,28 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise runCodexFeaturesCommand(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, env, validate, invocation); - } catch (error) { - return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${error instanceof Error ? error.message : String(error)}` }, 502); + const selectionWrite = await beginCodexCliSelectionParentWriteAsync({ signal: req.signal }); + if (!selectionWrite.entered) return jsonResponse({ error: "codex_cli_selection_busy", retryable: true }, 503); + try { + const before = isDefaultModeRequestUserInputEnabled(); + let toggle = deps.toggleDefaultModeRequestUserInput; + if (!toggle) { + try { + const { codexFeaturesInvocation, runCodexFeaturesCommand } = await import("../../cli/v2"); + const action = body.enabled ? "enable" : "disable"; + const invocation = selectionWrite.commit(() => codexFeaturesInvocation(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, process.platform, { requireAvailable: true })); + toggle = (_enabled, env, validate) => runCodexFeaturesCommand(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, env, validate, invocation); + } catch (error) { + if (error instanceof CodexCliSelectionMutationBusyError) throw error; + return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${error instanceof Error ? error.message : String(error)}` }, 502); + } } - } - // The `codex features` subprocess rewrites config.toml itself — run it - // under the shared write lock so it cannot interleave with an opencodex - // scalar edit or injection mid-write on either side. - const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage, runConfigWriteChild, ConfigWriteDestinationChanged } = await import("../../codex/config-write-lock"); - const configPath = activeCodexConfigPath(); - // Preserve native home creation only after executable resolution succeeds. - prepareCodexHome(dirname(configPath)); - { + // Keep selection ownership across the config-lock await and the whole + // synchronous child execution: an updater must not replace this command. + const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage, runConfigWriteChild, ConfigWriteDestinationChanged } = await import("../../codex/config-write-lock"); + const configPath = activeCodexConfigPath(); + // Preserve native home creation only after executable resolution succeeds. + selectionWrite.commit(() => prepareCodexHome(dirname(configPath))); const configLock = await acquireConfigWriteLock(configPath); if (!configLock.ok) { return jsonResponse({ error: configWriteLockFailureMessage(configLock), retryable: configLock.error === "locked" }, 502); @@ -589,8 +591,9 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise toggle!(body.enabled as boolean, env, validate)); + selectionWrite.commit(() => runConfigWriteChild(configPath, configLock.handle, (env, validate) => toggle!(body.enabled as boolean, env, validate))); } catch (error) { + if (error instanceof CodexCliSelectionMutationBusyError) throw error; destinationChanged = error instanceof ConfigWriteDestinationChanged; const err = error as { stderr?: unknown; message?: string }; const raw = err.stderr; @@ -613,7 +616,7 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise